.dockerignore DELETED
@@ -1,41 +0,0 @@
1
- # Python
2
- __pycache__/
3
- *.py[cod]
4
- *$py.class
5
- *.so
6
- .Python
7
- *.egg-info/
8
- dist/
9
- build/
10
-
11
- # Virtual environments
12
- venv/
13
- env/
14
- ENV/
15
-
16
- # IDE
17
- .vscode/
18
- .idea/
19
- *.swp
20
- *.swo
21
-
22
- # Git
23
- .git/
24
- .gitignore
25
-
26
- # Logs
27
- logs/*.log
28
-
29
- # Environment (will be set via HF Secrets)
30
- .env
31
-
32
- # Archive (not needed in deployment)
33
- archive/
34
-
35
- # Documentation
36
- *.md
37
- !README.md
38
-
39
- # Test files
40
- tests/
41
- *.test.py
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.env.example CHANGED
@@ -1,50 +1,28 @@
1
- # ============================================================================
2
- # FleetMind MCP Server - Environment Configuration
3
- # For HuggingFace Space (Track 1) deployment
4
- # ============================================================================
5
 
6
- # ============================================================================
7
- # Google Maps API (REQUIRED)
8
- # ============================================================================
9
- # Used for geocoding addresses and calculating routes
10
- # Get your API key at: https://console.cloud.google.com/google/maps-apis
11
- # Enable these APIs in your Google Cloud Console:
12
- # - Geocoding API (required for address lookup)
13
- # - Routes API (recommended - new, more accurate)
14
- # - Directions API (legacy fallback)
15
- # The system will try Routes API first, then fall back to Directions API
16
- GOOGLE_MAPS_API_KEY=your_google_maps_api_key_here
17
 
18
- # ============================================================================
19
- # OpenWeatherMap API (OPTIONAL - for intelligent routing)
20
- # ============================================================================
21
- # Used for weather-aware routing decisions
22
- # Get free API key at: https://openweathermap.org/api
23
- # Free tier: 1,000 calls/day, 60 calls/minute
24
- OPENWEATHERMAP_API_KEY=your_openweathermap_api_key_here
25
 
26
- # ============================================================================
27
- # PostgreSQL Database Configuration (REQUIRED)
28
- # ============================================================================
29
- # For local development, use localhost
30
- # For HuggingFace Space, use Neon or other cloud PostgreSQL
31
- # Get free PostgreSQL at: https://neon.tech
32
- DB_HOST=your-postgres-host.neon.tech
33
  DB_PORT=5432
34
  DB_NAME=fleetmind
35
- DB_USER=your_db_user
36
- DB_PASSWORD=your_db_password
37
 
38
- # ============================================================================
39
- # Server Configuration (OPTIONAL)
40
- # ============================================================================
41
- # HuggingFace Space will set these automatically
42
- # Only needed for local SSE mode testing
43
- PORT=7860
44
- HOST=0.0.0.0
45
 
46
- # ============================================================================
47
- # Logging (OPTIONAL)
48
- # ============================================================================
 
 
49
  LOG_LEVEL=INFO
50
  LOG_FILE=logs/fleetmind.log
 
1
+ # AI Provider Selection (choose one: "anthropic" or "gemini")
2
+ AI_PROVIDER=anthropic
 
 
3
 
4
+ # API Keys for AI Providers
5
+ ANTHROPIC_API_KEY=your_anthropic_api_key_here
6
+ GOOGLE_API_KEY=your_google_api_key_here
 
 
 
 
 
 
 
 
7
 
8
+ # HERE Maps API Key (for geocoding)
9
+ HERE_API_KEY=your_here_api_key_here
 
 
 
 
 
10
 
11
+ # PostgreSQL Database Configuration
12
+ DB_HOST=localhost
 
 
 
 
 
13
  DB_PORT=5432
14
  DB_NAME=fleetmind
15
+ DB_USER=postgres
16
+ DB_PASSWORD=your_password_here
17
 
18
+ # MCP Server
19
+ MCP_SERVER_NAME=dispatch-coordinator-mcp
20
+ MCP_SERVER_VERSION=1.0.0
 
 
 
 
21
 
22
+ # Gradio
23
+ GRADIO_SERVER_PORT=7860
24
+ GRADIO_SHARE=false
25
+
26
+ # Logging
27
  LOG_LEVEL=INFO
28
  LOG_FILE=logs/fleetmind.log
.gitattributes ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
.github/workflows/check-file-size.yml DELETED
@@ -1,16 +0,0 @@
1
- name: Check file size
2
- on:
3
- pull_request:
4
- branches: [main]
5
-
6
- # Allow manual trigger from Actions tab
7
- workflow_dispatch:
8
-
9
- jobs:
10
- check-file-size:
11
- runs-on: ubuntu-latest
12
- steps:
13
- - name: Check large files
14
- uses: ActionsDesk/lfs-warning@v2.0
15
- with:
16
- filesizelimit: 10485760 # 10MB limit for HF Spaces sync
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.github/workflows/sync-to-huggingface.yml DELETED
@@ -1,24 +0,0 @@
1
- name: Sync to Hugging Face Space
2
- on:
3
- push:
4
- branches: [main]
5
-
6
- # Allow manual trigger from Actions tab
7
- workflow_dispatch:
8
-
9
- jobs:
10
- sync-to-hub:
11
- runs-on: ubuntu-latest
12
- steps:
13
- - uses: actions/checkout@v3
14
- with:
15
- fetch-depth: 0
16
- lfs: true
17
-
18
- - name: Push to Hugging Face Space
19
- env:
20
- HF_TOKEN: ${{ secrets.HF_TOKEN }}
21
- run: |
22
- git config --global user.email "github-actions[bot]@users.noreply.github.com"
23
- git config --global user.name "github-actions[bot]"
24
- git push https://MCP-1st-Birthday:$HF_TOKEN@huggingface.co/spaces/MCP-1st-Birthday/fleetmind-dispatch-ai main --force
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.gitignore CHANGED
@@ -33,16 +33,10 @@ data/*.db
33
 
34
  # Environment Variables
35
  .env
36
- .env.*
37
- *.env
38
- .env.local
39
- .env.production
40
- .env.development
41
 
42
  # IDE
43
  .vscode/
44
  .idea/
45
- .claude/
46
  *.swp
47
  *.swo
48
  *~
 
33
 
34
  # Environment Variables
35
  .env
 
 
 
 
 
36
 
37
  # IDE
38
  .vscode/
39
  .idea/
 
40
  *.swp
41
  *.swo
42
  *~
DRIVER_CREATION_GUIDE.md ADDED
@@ -0,0 +1,318 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Driver Creation Feature Guide
2
+
3
+ ## βœ… **Feature Status: READY**
4
+
5
+ The driver creation feature has been successfully implemented and tested!
6
+
7
+ ---
8
+
9
+ ## πŸš€ **How to Use**
10
+
11
+ ### **Step 1: Restart Your Application**
12
+
13
+ Since we updated Gemini's system prompt and tools, restart the app:
14
+
15
+ ```bash
16
+ # Stop the current app (Ctrl+C)
17
+ python ui/app.py
18
+ ```
19
+
20
+ ---
21
+
22
+ ### **Step 2: Create Drivers Using Natural Language**
23
+
24
+ Open the chat at http://127.0.0.1:7860 and type naturally!
25
+
26
+ ---
27
+
28
+ ## πŸ“ **Example Commands**
29
+
30
+ ### **Example 1: Complete Driver Info**
31
+
32
+ ```
33
+ Add new driver Tom Wilson, phone 555-0101, drives a van, plate ABC-123
34
+ ```
35
+
36
+ **Gemini will create:**
37
+ - Driver ID: DRV-20251114HHMMSS (auto-generated)
38
+ - Name: Tom Wilson
39
+ - Phone: 555-0101
40
+ - Vehicle: van
41
+ - Plate: ABC-123
42
+ - Status: active (default)
43
+ - Capacity: 1000 kg (default for van)
44
+
45
+ ---
46
+
47
+ ### **Example 2: Driver with Skills**
48
+
49
+ ```
50
+ Create driver Sarah Martinez, phone 555-0202, refrigerated truck, plate XYZ-789,
51
+ skills: medical_certified, refrigerated
52
+ ```
53
+
54
+ **Gemini will create:**
55
+ - Name: Sarah Martinez
56
+ - Phone: 555-0202
57
+ - Vehicle: truck
58
+ - Plate: XYZ-789
59
+ - Skills: ["medical_certified", "refrigerated"]
60
+ - Capacity: 1000 kg (default)
61
+
62
+ ---
63
+
64
+ ### **Example 3: Minimal Info (Name Only)**
65
+
66
+ ```
67
+ Add driver Mike Chen
68
+ ```
69
+
70
+ **Gemini will create:**
71
+ - Name: Mike Chen
72
+ - Vehicle: van (default)
73
+ - Capacity: 1000 kg (default)
74
+ - Status: active (default)
75
+ - Skills: [] (empty by default)
76
+
77
+ Gemini might ask: "Would you like to provide phone number or vehicle details?"
78
+
79
+ ---
80
+
81
+ ### **Example 4: Motorcycle Courier**
82
+
83
+ ```
84
+ New driver: Lisa Anderson, phone 555-0303, motorcycle, express delivery specialist
85
+ ```
86
+
87
+ **Gemini will create:**
88
+ - Name: Lisa Anderson
89
+ - Phone: 555-0303
90
+ - Vehicle: motorcycle
91
+ - Skills: ["express_delivery"]
92
+ - Capacity: 50 kg (you can specify)
93
+
94
+ ---
95
+
96
+ ## 🎯 **Available Fields**
97
+
98
+ ### **Required:**
99
+ - βœ… **name** - Driver's full name
100
+
101
+ ### **Optional:**
102
+ - **phone** - Contact number (e.g., "+1-555-0101")
103
+ - **email** - Email address (e.g., "driver@fleet.com")
104
+ - **vehicle_type** - van | truck | car | motorcycle (default: van)
105
+ - **vehicle_plate** - License plate (e.g., "ABC-1234")
106
+ - **capacity_kg** - Cargo weight capacity in kg (default: 1000.0)
107
+ - **capacity_m3** - Cargo volume capacity in mΒ³ (default: 12.0)
108
+ - **skills** - List of certifications/skills:
109
+ - `refrigerated` - Can handle cold storage
110
+ - `medical_certified` - Medical deliveries
111
+ - `fragile_handler` - Fragile items expert
112
+ - `overnight` - Overnight/late deliveries
113
+ - `express_delivery` - Express/rush deliveries
114
+ - **status** - active | busy | offline | unavailable (default: active)
115
+
116
+ ---
117
+
118
+ ## πŸ§ͺ **Testing**
119
+
120
+ ### **Test 1: Verify Creation**
121
+
122
+ After creating a driver, check the database:
123
+
124
+ ```bash
125
+ python verify_drivers.py
126
+ ```
127
+
128
+ This will show all drivers including the newly created one.
129
+
130
+ ---
131
+
132
+ ### **Test 2: Check in UI**
133
+
134
+ Go to the **Orders** tab in the UI and you should see new drivers available for assignment.
135
+
136
+ ---
137
+
138
+ ## πŸ“Š **What Happens Behind the Scenes**
139
+
140
+ ### **User Input:**
141
+ ```
142
+ "Add new driver Tom Wilson, phone 555-0101, drives a van, plate ABC-123"
143
+ ```
144
+
145
+ ### **Gemini Processing:**
146
+ 1. **Parses** your natural language input
147
+ 2. **Extracts** driver information:
148
+ - name: "Tom Wilson"
149
+ - phone: "555-0101"
150
+ - vehicle_type: "van"
151
+ - vehicle_plate: "ABC-123"
152
+ 3. **Calls** `create_driver` tool with extracted data
153
+ 4. **Database** inserts the driver with auto-generated ID
154
+ 5. **Returns** confirmation message
155
+
156
+ ### **Database Record Created:**
157
+ ```sql
158
+ INSERT INTO drivers (
159
+ driver_id, -- DRV-20251114113250 (auto)
160
+ name, -- Tom Wilson
161
+ phone, -- 555-0101
162
+ vehicle_type, -- van
163
+ vehicle_plate, -- ABC-123
164
+ status, -- active (default)
165
+ capacity_kg, -- 1000.0 (default)
166
+ capacity_m3, -- 12.0 (default)
167
+ skills, -- [] (empty)
168
+ current_lat, -- 37.7749 (default SF)
169
+ current_lng, -- -122.4194 (default SF)
170
+ last_location_update -- 2025-11-14 11:32:50
171
+ ) VALUES (...)
172
+ ```
173
+
174
+ ---
175
+
176
+ ## πŸ”„ **Comparison: Orders vs Drivers**
177
+
178
+ ### **Creating an Order:**
179
+ ```
180
+ User: "Create order for John Doe, 123 Main St SF, phone 555-1234"
181
+ ↓
182
+ Gemini: [geocode_address] β†’ [create_order] β†’ "βœ… Order created!"
183
+ ```
184
+ **2 tools called** (geocoding required for addresses)
185
+
186
+ ### **Creating a Driver:**
187
+ ```
188
+ User: "Add driver Tom Wilson, phone 555-0101, van"
189
+ ↓
190
+ Gemini: [create_driver] β†’ "βœ… Driver created!"
191
+ ```
192
+ **1 tool called** (no geocoding needed)
193
+
194
+ ---
195
+
196
+ ## ⚑ **Quick Reference**
197
+
198
+ ### **Conversational Examples:**
199
+
200
+ βœ… "I need to onboard a new driver named Alex"
201
+ βœ… "Add Sarah to the fleet, she drives a truck"
202
+ βœ… "New driver: Mike, phone 555-9999, motorcycle"
203
+ βœ… "Create driver with medical certification"
204
+ βœ… "Add a refrigerated truck driver named Bob"
205
+
206
+ ### **Structured Examples:**
207
+
208
+ βœ… "Create driver: Name: Tom Wilson, Phone: 555-0101, Vehicle: van, Plate: ABC-123"
209
+ βœ… "New driver - Name: Sarah, Email: sarah@fleet.com, Vehicle type: truck, Skills: refrigerated, medical_certified"
210
+
211
+ ---
212
+
213
+ ## 🎨 **Sample Responses from Gemini**
214
+
215
+ ### **Successful Creation:**
216
+ ```
217
+ βœ… Driver DRV-20251114113250 created successfully!
218
+
219
+ Driver Details:
220
+ β€’ Driver ID: DRV-20251114113250
221
+ β€’ Name: Tom Wilson
222
+ β€’ Phone: 555-0101
223
+ β€’ Vehicle: van (ABC-123)
224
+ β€’ Capacity: 1000 kg
225
+ β€’ Status: Active
226
+ β€’ Skills: None
227
+
228
+ The driver has been added to your fleet and is ready for order assignments!
229
+ ```
230
+
231
+ ### **Missing Information:**
232
+ ```
233
+ I can create a driver for you! I have:
234
+ β€’ Name: Tom Wilson
235
+
236
+ To complete the driver profile, please provide (optional):
237
+ β€’ Phone number
238
+ β€’ Vehicle type (van/truck/car/motorcycle)
239
+ β€’ License plate number
240
+ β€’ Any special skills or certifications
241
+
242
+ Or I can create the driver now with default settings?
243
+ ```
244
+
245
+ ---
246
+
247
+ ## πŸ› οΈ **Technical Details**
248
+
249
+ ### **Function:** `handle_create_driver()`
250
+ **Location:** `chat/tools.py:245-331`
251
+
252
+ ### **Tool Definition:**
253
+ **Location:** `chat/providers/gemini_provider.py:140-186`
254
+
255
+ ### **System Prompt:**
256
+ **Location:** `chat/providers/gemini_provider.py:32-89`
257
+
258
+ ---
259
+
260
+ ## ✨ **Next Steps**
261
+
262
+ After creating drivers, you can:
263
+
264
+ 1. **Assign orders to drivers** (coming soon)
265
+ 2. **View driver list** in the UI
266
+ 3. **Update driver status** (active/busy/offline)
267
+ 4. **Track driver locations** (coming soon)
268
+
269
+ ---
270
+
271
+ ## πŸ› **Troubleshooting**
272
+
273
+ ### **Issue: "Unknown tool: create_driver"**
274
+
275
+ **Solution:** Restart the application to reload the new tools:
276
+ ```bash
277
+ # Stop app (Ctrl+C)
278
+ python ui/app.py
279
+ ```
280
+
281
+ ### **Issue: Driver created but not showing in database**
282
+
283
+ **Solution:** Check database connection and verify:
284
+ ```bash
285
+ python verify_drivers.py
286
+ ```
287
+
288
+ ### **Issue: "Missing required field: name"**
289
+
290
+ **Solution:** Make sure you provide at least the driver's name:
291
+ ```
292
+ "Add driver John Smith" βœ…
293
+ "Add a new driver" ❌ (no name)
294
+ ```
295
+
296
+ ---
297
+
298
+ ## πŸ“ˆ **Feature Comparison**
299
+
300
+ | Feature | Orders | Drivers |
301
+ |---------|--------|---------|
302
+ | **Required Fields** | 3 (name, address, contact) | 1 (name) |
303
+ | **Geocoding Needed** | βœ… Yes | ❌ No |
304
+ | **Tools Called** | 2 (geocode + create) | 1 (create) |
305
+ | **Default Values** | Priority, weight | Vehicle type, capacity, status |
306
+ | **Complex Data** | Time windows, coordinates | Skills array, JSON |
307
+
308
+ ---
309
+
310
+ ## πŸŽ‰ **Ready to Use!**
311
+
312
+ Your FleetMind system can now:
313
+ - βœ… Create customer orders
314
+ - βœ… Create delivery drivers
315
+ - βœ… Geocode addresses
316
+ - βœ… Store everything in PostgreSQL
317
+
318
+ Just talk naturally to Gemini and it handles the rest! πŸš€
Dockerfile DELETED
@@ -1,34 +0,0 @@
1
- # FleetMind MCP Server - HuggingFace Space Dockerfile
2
- # Read the doc: https://huggingface.co/docs/hub/spaces-sdks-docker
3
-
4
- FROM python:3.11-slim
5
-
6
- # Create non-root user (HuggingFace requirement)
7
- RUN useradd -m -u 1000 user
8
- USER user
9
- ENV PATH="/home/user/.local/bin:$PATH"
10
-
11
- # Set working directory
12
- WORKDIR /app
13
-
14
- # Copy requirements first (for better caching)
15
- COPY --chown=user requirements.txt /app/requirements.txt
16
-
17
- # Install dependencies
18
- RUN pip install --no-cache-dir --upgrade -r requirements.txt
19
-
20
- # Copy application files
21
- COPY --chown=user . /app
22
-
23
- # Create logs directory
24
- RUN mkdir -p /app/logs
25
-
26
- # Expose port 7860 (HuggingFace Space default)
27
- EXPOSE 7860
28
-
29
- # Health check (check proxy health endpoint)
30
- HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
31
- CMD python -c "import requests; requests.get('http://localhost:7860/health')" || exit 1
32
-
33
- # Run the MCP server with authentication proxy
34
- CMD ["python", "start_with_proxy.py"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
README.md CHANGED
@@ -1,309 +1,195 @@
1
  ---
2
- title: FleetMind MCP Server
3
  emoji: 🚚
4
  colorFrom: blue
5
  colorTo: purple
6
- sdk: docker
 
7
  app_file: app.py
8
- pinned: true
9
- short_description: AI delivery dispatch MCP server with 29 management tools
10
  tags:
11
  - mcp
12
- - building-mcp-track-enterprise
13
  - model-context-protocol
 
 
 
14
  - delivery-management
15
  - postgresql
16
- - fastmcp
17
- - gradio
18
- - enterprise
19
- - logistics
20
- - gemini
21
- - google-maps
22
- - ai-routing
23
  ---
24
 
25
- <div align="center">
26
 
27
- # 🚚 FleetMind MCP Server
28
 
29
- ### AI-Powered Delivery Dispatch for the Modern Enterprise
30
 
31
- **The Problem:** Delivery dispatch is complexβ€”coordinating drivers, optimizing routes, handling weather delays, and meeting SLAs requires constant human oversight.
32
 
33
- **The Solution:** FleetMind exposes **29 AI-accessible tools** that let Claude (or any MCP client) manage your entire delivery operation through natural conversation.
34
 
35
- **The Impact:** Dispatch managers can now say *"Assign this urgent fragile delivery to the best available driver considering weather and traffic"* and watch AI handle the complexity.
36
 
37
- [![Demo Video](https://img.shields.io/badge/β–Ά_Watch_Demo-FF0000?style=for-the-badge&logo=youtube&logoColor=white)](https://www.youtube.com/watch?v=IA9iD0VPvro)
38
- [![Try It Live](https://img.shields.io/badge/πŸš€_Try_It_Live-7C3AED?style=for-the-badge)](https://mcp-1st-birthday-fleetmind-dispatch-ai.hf.space)
39
- [![GitHub](https://img.shields.io/badge/GitHub-181717?style=for-the-badge&logo=github&logoColor=white)](https://github.com/mashrur-rahman-fahim/fleetmind-mcp)
40
- [![LinkedIn Post](https://img.shields.io/badge/LinkedIn_Post-0A66C2?style=for-the-badge&logo=linkedin&logoColor=white)](https://www.linkedin.com/posts/kazi-wasif-amin-shammo-3a0307197_mcpbirthdayhackathon-ai-mcp-activity-7400938958521675776-Trax)
41
- [![Facebook Post](https://img.shields.io/badge/Facebook_Post-1877F2?style=for-the-badge&logo=facebook&logoColor=white)](https://www.facebook.com/share/v/1BdfnLbuEY/)
 
42
 
43
- </div>
44
 
45
- ---
46
 
47
- ## πŸ“Ί See It In Action
48
-
49
- <div align="center">
50
- <a href="https://www.youtube.com/watch?v=IA9iD0VPvro">
51
- <img src="https://img.youtube.com/vi/IA9iD0VPvro/maxresdefault.jpg" alt="FleetMind Demo Video" width="560">
52
- </a>
53
- <p><b>▢️ Click to watch the demo video</b></p>
54
- </div>
55
 
56
- > Watch Gemini 2.0 Flash AI analyze weather, traffic, and driver capabilities to make intelligent assignment decisions with full reasoning transparency.
57
 
58
- ---
59
 
60
- ## ⚑ Quick Start (2 Minutes)
61
-
62
- ### Step 1: Get Your API Key
63
- πŸ‘‰ **[Generate API Key](https://mcp-1st-birthday-fleetmind-dispatch-ai.hf.space/generate-key)** (copy immediatelyβ€”shown once!)
64
-
65
- ### Step 2: Configure Claude Desktop
66
- Add to your `claude_desktop_config.json`:
67
- ```json
68
- {
69
- "mcpServers": {
70
- "fleetmind": {
71
- "command": "npx",
72
- "args": [
73
- "mcp-remote",
74
- "https://mcp-1st-birthday-fleetmind-dispatch-ai.hf.space/sse?api_key=YOUR_KEY_HERE"
75
- ]
76
- }
77
- }
78
- }
79
- ```
80
 
81
- ### Step 3: Start Dispatching!
 
 
 
82
  ```
83
- You: "Create an urgent delivery for Sarah at 456 Oak Ave, San Francisco,
84
- then use AI to find the best driver considering the current rain"
85
 
86
- Claude: Uses geocode_address β†’ create_order β†’ intelligent_assign_order
87
- Returns: "Order ORD-... assigned to Mike (DRV-...).
88
- AI selected Mike because: closest driver with cold-storage van,
89
- safest route in current weather, 15 min ETA."
90
  ```
91
 
92
- ---
93
-
94
- ## πŸ† Why FleetMind?
95
-
96
- | Feature | What It Does | Why It Matters |
97
- |---------|--------------|----------------|
98
- | **πŸ€– Gemini 2.0 Flash AI** | Analyzes 10+ parameters for intelligent driver assignment | Better decisions than rule-based systems |
99
- | **🌦️ Weather-Aware Routing** | OpenWeatherMap integration for safety-first planning | Reduces weather-related delivery failures |
100
- | **πŸ” Multi-Tenant Auth** | Complete data isolation via deterministic user_id | Enterprise-ready security out of the box |
101
- | **πŸ“Š SLA Tracking** | Automatic on-time/late/very-late classification | Built-in performance analytics |
102
- | **🚦 Real-Time Traffic** | Google Routes API with live traffic data | Accurate ETAs, not guesstimates |
103
- | **🏍️ Vehicle Optimization** | Motorcycle, bicycle, car, van, truck routing | Right routes for each vehicle type |
104
-
105
- ---
106
-
107
- ## πŸ› οΈ The 29 Tools
108
-
109
- <details>
110
- <summary><b>πŸ“ Geocoding & Routing (3 tools)</b></summary>
111
-
112
- | Tool | Description |
113
- |------|-------------|
114
- | `geocode_address` | Convert address β†’ GPS coordinates |
115
- | `calculate_route` | Vehicle-specific routing with traffic, tolls, fuel estimates |
116
- | `calculate_intelligent_route` | AI-powered routing with weather + traffic analysis |
117
-
118
- </details>
119
-
120
- <details>
121
- <summary><b>πŸ“¦ Order Management (8 tools)</b></summary>
122
-
123
- | Tool | Description |
124
- |------|-------------|
125
- | `create_order` | Create delivery with mandatory deadline & SLA tracking |
126
- | `count_orders` | Count by status/priority with breakdowns |
127
- | `fetch_orders` | Paginated list with filters |
128
- | `get_order_details` | Full order info including timing data |
129
- | `search_orders` | Find by customer name/email/phone/ID |
130
- | `get_incomplete_orders` | Active deliveries shortcut |
131
- | `update_order` | Modify order with cascading updates |
132
- | `delete_order` | Safe deletion with assignment checks |
133
-
134
- </details>
135
-
136
- <details>
137
- <summary><b>πŸ‘₯ Driver Management (8 tools)</b></summary>
138
-
139
- | Tool | Description |
140
- |------|-------------|
141
- | `create_driver` | Onboard with vehicle type, skills, capacity |
142
- | `count_drivers` | Count by status/vehicle with breakdowns |
143
- | `fetch_drivers` | Paginated list with filters |
144
- | `get_driver_details` | Full info with reverse-geocoded location |
145
- | `search_drivers` | Find by name/plate/phone/ID |
146
- | `get_available_drivers` | Ready-for-dispatch shortcut |
147
- | `update_driver` | Modify with location auto-timestamp |
148
- | `delete_driver` | Safe deletion with assignment checks |
149
-
150
- </details>
151
-
152
- <details>
153
- <summary><b>πŸ”— Assignment Management (8 tools) ⭐</b></summary>
154
-
155
- | Tool | Description |
156
- |------|-------------|
157
- | `create_assignment` | Manual driver selection |
158
- | **`auto_assign_order`** | **Nearest driver + capacity/skill validation** |
159
- | **`intelligent_assign_order`** | **πŸ€– Gemini 2.0 Flash AI with full reasoning** |
160
- | `get_assignment_details` | Query by assignment/order/driver ID |
161
- | `update_assignment` | Status transitions with cascading |
162
- | `unassign_order` | Revert to pending safely |
163
- | `complete_delivery` | Mark done + auto-update driver location |
164
- | `fail_delivery` | Track failure with GPS + structured reason |
165
-
166
- </details>
167
-
168
- <details>
169
- <summary><b>πŸ—‘οΈ Bulk Operations (2 tools)</b></summary>
170
-
171
- | Tool | Description |
172
- |------|-------------|
173
- | `delete_all_orders` | Mass delete with status filter & safety checks |
174
- | `delete_all_drivers` | Mass delete with assignment safety blocks |
175
-
176
- </details>
177
 
178
- ---
 
 
179
 
180
- ## πŸ—οΈ Architecture
 
181
 
 
 
182
  ```
183
- β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
184
- β”‚ MCP Clients β”‚
185
- β”‚ Claude Desktop β€’ Continue β€’ Cline β€’ Custom Apps β”‚
186
- β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
187
- β”‚ SSE: /sse?api_key=fm_xxx
188
- β–Ό
189
- β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
190
- β”‚ Authentication Proxy (proxy.py) β”‚
191
- β”‚ β€’ Captures API key from SSE connection β”‚
192
- β”‚ β€’ Links session_id β†’ api_key for tool calls β”‚
193
- β”‚ β€’ Keep-alive pings every 30s β”‚
194
- β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
195
- β–Ό
196
- β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
197
- β”‚ FastMCP Server (server.py) β”‚
198
- β”‚ β€’ ApiKeyAuthMiddleware validates every call β”‚
199
- β”‚ β€’ 29 tools with user_id filtering β”‚
200
- β”‚ β€’ 2 real-time resources (orders://all, drivers://all) β”‚
201
- β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
202
- β–Ό β–Ό β–Ό β–Ό
203
- β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
204
- β”‚PostgreSQLβ”‚ β”‚ Google β”‚ β”‚ Gemini β”‚ β”‚OpenWeatherβ”‚
205
- β”‚ (Neon) β”‚ β”‚Maps API β”‚ β”‚2.0 Flash β”‚ β”‚ Map API β”‚
206
- β”‚ user_id β”‚ β”‚Geocodingβ”‚ β”‚Intelligentβ”‚β”‚ Weather β”‚
207
- β”‚isolation β”‚ β”‚ Routing β”‚ β”‚Assignmentβ”‚ β”‚ Data β”‚
208
- β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
209
- ```
210
-
211
- ---
212
 
213
- ## πŸ” Authentication & Security
214
 
215
- FleetMind uses a **3-layer authentication system** for enterprise-grade security:
216
-
217
- | Layer | Component | Function |
218
- |-------|-----------|----------|
219
- | 1 | **Proxy** | Captures API key from URL, links sessions |
220
- | 2 | **Middleware** | Validates every tool call, injects user context |
221
- | 3 | **Database** | All queries filter by `user_id` (complete isolation) |
222
-
223
- **Security Features:**
224
- - βœ… SHA-256 hashed key storage (never plaintext)
225
- - βœ… One-time key display (can't retrieve later)
226
- - βœ… Deterministic user_id from email (key rotation preserves data)
227
- - βœ… Production safeguards (SKIP_AUTH blocked when ENV=production)
228
 
229
- <details>
230
- <summary><b>View Complete Auth Flow</b></summary>
231
 
232
- ```
233
- 1. Generate API Key β†’ user_id = user_{MD5(email)[:12]}
234
- 2. Configure Claude β†’ URL includes ?api_key=fm_xxx
235
- 3. SSE Connection β†’ Proxy captures and stores key
236
- 4. Tool Call β†’ Proxy injects key, middleware validates
237
- 5. Database Query β†’ WHERE user_id = 'user_xxx'
238
- 6. Result β†’ Only user's own data returned
239
  ```
240
 
241
- </details>
242
 
243
- ---
 
 
 
244
 
245
- ## πŸš€ Deployment
246
 
247
- ### Production (HuggingFace Space)
248
- Already deployed at: **https://mcp-1st-birthday-fleetmind-dispatch-ai.hf.space**
249
 
250
- ### Local Development
251
  ```bash
252
- git clone <repository-url>
253
- cd fleetmind-mcp
254
- pip install -r requirements.txt
255
-
256
- # Configure .env with your API keys
257
- python start_with_proxy.py # Runs proxy (7860) + FastMCP (7861)
258
  ```
259
 
260
- ---
261
 
262
- ## πŸ“Š Technical Specifications
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
263
 
264
- | Metric | Value |
265
- |--------|-------|
266
- | **Tools** | 29 |
267
- | **Resources** | 2 (orders://all, drivers://all) |
268
- | **Database** | PostgreSQL (Neon serverless) |
269
- | **AI Model** | Gemini 2.0 Flash (gemini-2.0-flash-exp) |
270
- | **Transport** | SSE (Server-Sent Events) |
271
- | **Framework** | FastMCP 2.13.0 |
272
- | **Language** | Python 3.10+ |
273
 
274
- ---
275
 
276
- ## πŸ”— Links
277
 
278
- | Resource | URL |
279
- |----------|-----|
280
- | **Live Demo** | https://mcp-1st-birthday-fleetmind-dispatch-ai.hf.space |
281
- | **MCP Endpoint** | https://mcp-1st-birthday-fleetmind-dispatch-ai.hf.space/sse |
282
- | **Generate API Key** | https://mcp-1st-birthday-fleetmind-dispatch-ai.hf.space/generate-key |
283
- | **HuggingFace Space** | https://huggingface.co/spaces/MCP-1st-Birthday/fleetmind-dispatch-ai |
284
 
285
- ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
286
 
287
- ## πŸ‘₯ Team
288
 
289
- **FleetMind Development Team**
 
 
 
 
290
 
291
- *MCP 1st Birthday Hackathon - Track 1: Building MCP Servers (Enterprise Category)*
292
 
293
- ---
294
 
295
- ## πŸ“„ License
 
296
 
297
- MIT License - see [LICENSE](LICENSE) for details.
 
298
 
299
- ---
 
 
 
 
 
 
 
 
 
 
300
 
301
- <div align="center">
302
 
303
- **Built with ❀️ using [FastMCP](https://gofastmcp.com) for the MCP 1st Birthday Hackathon**
304
 
305
- [![MCP](https://img.shields.io/badge/MCP-1.0-orange)](https://modelcontextprotocol.io)
306
- [![FastMCP](https://img.shields.io/badge/FastMCP-2.13.0-blue)](https://gofastmcp.com)
307
- [![Python](https://img.shields.io/badge/Python-3.10+-brightgreen)](https://python.org)
308
 
309
- </div>
 
1
  ---
2
+ title: FleetMind AI Dispatch Coordinator
3
  emoji: 🚚
4
  colorFrom: blue
5
  colorTo: purple
6
+ sdk: gradio
7
+ sdk_version: 5.9.0
8
  app_file: app.py
9
+ pinned: false
 
10
  tags:
11
  - mcp
12
+ - mcp-in-action-track-01
13
  - model-context-protocol
14
+ - multi-agent
15
+ - autonomous-ai
16
+ - gemini-2.0-flash
17
  - delivery-management
18
  - postgresql
 
 
 
 
 
 
 
19
  ---
20
 
21
+ # FleetMind MCP - Autonomous Dispatch Coordinator
22
 
23
+ **πŸ† MCP 1st Birthday Hackathon Submission - Track: MCP in Action**
24
 
25
+ An autonomous AI coordinator that handles delivery exceptions using multi-agent orchestration powered by Google Gemini 2.0 Flash and the Model Context Protocol (MCP).
26
 
27
+ ---
28
 
29
+ ## πŸ‘₯ Team
30
 
31
+ **Team Name:** [Your Team Name]
32
 
33
+ **Team Members:**
34
+ - **[Your Name]** - [@your-hf-username](https://huggingface.co/your-hf-username) - Lead Developer & Repository Manager
35
+ - **[Partner 2 Name]** - [@partner2-username](https://huggingface.co/partner2-username) - [Role - e.g., Backend Developer, Testing]
36
+ - **[Partner 3 Name]** - [@partner3-username](https://huggingface.co/partner3-username) - [Role - e.g., Documentation, Video]
37
+ - **[Partner 4 Name]** - [@partner4-username](https://huggingface.co/partner4-username) - [Role - e.g., UI/UX Designer]
38
+ - **[Partner 5 Name]** - [@partner5-username](https://huggingface.co/partner5-username) - [Role - e.g., Project Manager]
39
 
40
+ **Collaboration:** Team collaborates via [GitHub repository / Pull Requests / Task Division] with contributions managed by the lead developer.
41
 
42
+ *(Note: Replace placeholders with actual team member information. All members must have Hugging Face accounts and be listed here for valid hackathon submission.)*
43
 
44
+ ---
 
 
 
 
 
 
 
45
 
46
+ ## πŸš€ Quick Start
47
 
48
+ ### 1. Install PostgreSQL
49
 
50
+ **Windows:**
51
+ - Download from https://www.postgresql.org/download/windows/
52
+ - Install with default settings
53
+ - Remember your postgres password
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
 
55
+ **macOS:**
56
+ ```bash
57
+ brew install postgresql
58
+ brew services start postgresql
59
  ```
 
 
60
 
61
+ **Linux:**
62
+ ```bash
63
+ sudo apt-get install postgresql postgresql-contrib
64
+ sudo systemctl start postgresql
65
  ```
66
 
67
+ ### 2. Create Database
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
 
69
+ ```bash
70
+ # Login to PostgreSQL
71
+ psql -U postgres
72
 
73
+ # Create the database
74
+ CREATE DATABASE fleetmind;
75
 
76
+ # Exit
77
+ \q
78
  ```
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
 
80
+ ### 3. Set Up Environment
81
 
82
+ ```bash
83
+ # Install Python dependencies
84
+ pip install -r requirements.txt
 
 
 
 
 
 
 
 
 
 
85
 
86
+ # Copy environment template
87
+ cp .env.example .env
88
 
89
+ # Edit .env with your database credentials
90
+ # DB_HOST=localhost
91
+ # DB_PORT=5432
92
+ # DB_NAME=fleetmind
93
+ # DB_USER=postgres
94
+ # DB_PASSWORD=your_password_here
 
95
  ```
96
 
97
+ ### 4. Initialize Database Schema
98
 
99
+ ```bash
100
+ # Run database initialization script
101
+ python scripts/init_db.py
102
+ ```
103
 
104
+ This will create all necessary tables in the PostgreSQL database.
105
 
106
+ ### 3. Run Application
 
107
 
 
108
  ```bash
109
+ # Start the Gradio UI (coming soon)
110
+ python ui/app.py
 
 
 
 
111
  ```
112
 
113
+ ## πŸ“ Project Structure
114
 
115
+ ```
116
+ fleetmind-mcp/
117
+ β”œβ”€β”€ database/ # Database connection and schema
118
+ β”‚ β”œβ”€β”€ __init__.py
119
+ β”‚ β”œβ”€β”€ connection.py # Database connection utilities
120
+ β”‚ └── schema.py # Database schema definitions
121
+ β”œβ”€β”€ data/ # Database and data files
122
+ β”‚ └── fleetmind.db # SQLite database (auto-generated)
123
+ β”œβ”€β”€ mcp_server/ # MCP server implementation
124
+ β”œβ”€β”€ agents/ # Multi-agent system
125
+ β”œβ”€β”€ workflows/ # Orchestration workflows
126
+ β”œβ”€β”€ ui/ # Gradio interface
127
+ β”œβ”€β”€ tests/ # Test suite
128
+ β”œβ”€β”€ scripts/ # Utility scripts
129
+ β”‚ └── init_db.py # Database initialization
130
+ β”œβ”€β”€ requirements.txt # Python dependencies
131
+ β”œβ”€β”€ .env.example # Environment variables template
132
+ └── README.md # This file
133
+ ```
134
 
135
+ ## πŸ“Š Database Schema (PostgreSQL)
 
 
 
 
 
 
 
 
136
 
137
+ The system uses PostgreSQL with the following tables:
138
 
139
+ ### Orders Table
140
 
141
+ The `orders` table stores all delivery order information:
 
 
 
 
 
142
 
143
+ | Column | Type | Description |
144
+ |--------|------|-------------|
145
+ | order_id | VARCHAR(50) | Primary key |
146
+ | customer_name | VARCHAR(255) | Customer name |
147
+ | customer_phone | VARCHAR(20) | Contact phone |
148
+ | customer_email | VARCHAR(255) | Contact email |
149
+ | delivery_address | TEXT | Delivery address |
150
+ | delivery_lat/lng | DECIMAL(10,8) | GPS coordinates |
151
+ | time_window_start/end | TIMESTAMP | Delivery time window |
152
+ | priority | VARCHAR(20) | standard/express/urgent |
153
+ | weight_kg | DECIMAL(10,2) | Package weight |
154
+ | status | VARCHAR(20) | pending/assigned/in_transit/delivered/failed/cancelled |
155
+ | assigned_driver_id | VARCHAR(50) | Assigned driver |
156
+ | created_at | TIMESTAMP | Creation timestamp |
157
+ | updated_at | TIMESTAMP | Auto-updated timestamp |
158
 
159
+ ### Additional Tables
160
 
161
+ - **drivers** - Driver information and status
162
+ - **assignments** - Order-driver assignments with routing
163
+ - **exceptions** - Exception tracking and resolution
164
+ - **agent_decisions** - AI agent decision logging
165
+ - **metrics** - Performance metrics and analytics
166
 
167
+ ## πŸ”§ Development
168
 
169
+ ### Database Operations
170
 
171
+ ```python
172
+ from database.connection import get_db_connection, execute_query, execute_write
173
 
174
+ # Get all pending orders (note: PostgreSQL uses %s for parameters)
175
+ orders = execute_query("SELECT * FROM orders WHERE status = %s", ("pending",))
176
 
177
+ # Create new order
178
+ order_id = execute_write(
179
+ "INSERT INTO orders (order_id, customer_name, delivery_address, status) VALUES (%s, %s, %s, %s)",
180
+ ("ORD-001", "John Doe", "123 Main St", "pending")
181
+ )
182
+
183
+ # Test connection
184
+ from database.connection import test_connection
185
+ if test_connection():
186
+ print("Database connected successfully!")
187
+ ```
188
 
189
+ ## πŸ“ License
190
 
191
+ MIT License
192
 
193
+ ## 🀝 Contributing
 
 
194
 
195
+ Contributions welcome! Please read the contributing guidelines first.
TEAM_COLLABORATION_GUIDE.md ADDED
@@ -0,0 +1,305 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Team Collaboration Guide - MCP 1st Birthday Hackathon
2
+
3
+ ## Overview
4
+
5
+ This guide explains how to add team partners to your FleetMind MCP hackathon submission on Hugging Face Spaces.
6
+
7
+ **Hackathon Details:**
8
+ - **Team Size:** 2-5 members allowed
9
+ - **Your Space:** https://huggingface.co/spaces/MCP-1st-Birthday/fleetmind-dispatch-ai
10
+ - **Submission Deadline:** November 30, 2025 (11:59 PM UTC)
11
+ - **Track:** MCP in Action (Track 01)
12
+
13
+ ---
14
+
15
+ ## Method 1: Add Team Members via README Documentation (EASIEST)
16
+
17
+ For hackathon submission purposes, you MUST document your team in the Space's README.md file.
18
+
19
+ ### Steps:
20
+
21
+ 1. **Edit the Team Section in README.md**
22
+
23
+ The Team section is already added to your README. Update it with your actual team information:
24
+
25
+ ```markdown
26
+ ## πŸ‘₯ Team
27
+
28
+ **Team Name:** FleetMind AI Team
29
+
30
+ **Team Members:**
31
+ - **John Doe** - [@johndoe](https://huggingface.co/johndoe) - Lead Developer & AI Architect
32
+ - **Jane Smith** - [@janesmith](https://huggingface.co/janesmith) - Database Engineer
33
+ - **Alex Chen** - [@alexchen](https://huggingface.co/alexchen) - UI/UX Developer
34
+ ```
35
+
36
+ 2. **Replace Placeholders:**
37
+ - `[Your Team Name]` β†’ Your actual team name
38
+ - `[Your Name]` β†’ Team member's real name
39
+ - `@your-hf-username` β†’ Their Hugging Face username
40
+ - `[Role]` β†’ Their role in the project
41
+
42
+ 3. **Commit and Push:**
43
+
44
+ ```bash
45
+ cd F:\fleetmind-mcp\fleetmind-dispatch-ai
46
+ git add README.md
47
+ git commit -m "Update team member information"
48
+ git push
49
+ ```
50
+
51
+ ---
52
+
53
+ ## Method 2: Grant Git Access to Team Partners (TECHNICAL COLLABORATION)
54
+
55
+ If your team partners need to push code directly to the Space, they need Git access.
56
+
57
+ ### Option A: Via Organization Membership
58
+
59
+ Since your Space is owned by the **MCP-1st-Birthday organization**, team members can:
60
+
61
+ 1. **Join the Organization:**
62
+ - Go to https://huggingface.co/MCP-1st-Birthday
63
+ - Click **"Request to join this org"** (top right)
64
+ - Fill out the registration form
65
+ - Wait for admin approval
66
+
67
+ 2. **Verify Access:**
68
+ - Once approved, they'll automatically have access based on organization permissions
69
+ - Organization members with "write" or "contributor" roles can collaborate
70
+
71
+ ### Option B: Direct Collaborator Access
72
+
73
+ If you have admin rights to your Space:
74
+
75
+ 1. **Go to Space Settings:**
76
+ - Visit: https://huggingface.co/spaces/MCP-1st-Birthday/fleetmind-dispatch-ai/settings
77
+ - Look for "Collaborators" or "Access" section
78
+
79
+ 2. **Add Collaborators by Username:**
80
+ - Enter their Hugging Face username
81
+ - Set their permission level (read/write/admin)
82
+ - Send invitation
83
+
84
+ ---
85
+
86
+ ## Method 3: Collaborate via Pull Requests (SAFEST)
87
+
88
+ Team members can contribute without direct write access using Pull Requests.
89
+
90
+ ### Steps:
91
+
92
+ 1. **Team Partner Forks/Duplicates the Space:**
93
+ - They go to your Space page
94
+ - Click the three dots (top right) β†’ "Duplicate this Space"
95
+ - Make changes in their forked version
96
+
97
+ 2. **Create Pull Request:**
98
+ - After making changes, they create a Pull Request
99
+ - You review and merge their changes
100
+
101
+ 3. **Enable Pull Requests:**
102
+ - Go to Space Settings
103
+ - Ensure "Pull Requests" are enabled
104
+
105
+ ---
106
+
107
+ ## Method 4: Share Git Credentials (NOT RECOMMENDED)
108
+
109
+ While technically possible, sharing your Git credentials is NOT recommended for security reasons. Use Methods 1-3 instead.
110
+
111
+ ---
112
+
113
+ ## Technical Setup for Team Partners
114
+
115
+ Once your team partners have access, they need to set up their local environment:
116
+
117
+ ### 1. Clone the Space
118
+
119
+ ```bash
120
+ # Navigate to desired directory
121
+ cd F:\
122
+
123
+ # Clone the Space
124
+ git clone https://huggingface.co/spaces/MCP-1st-Birthday/fleetmind-dispatch-ai
125
+
126
+ # Enter directory
127
+ cd fleetmind-dispatch-ai
128
+ ```
129
+
130
+ ### 2. Authenticate with Hugging Face
131
+
132
+ They need a Hugging Face access token:
133
+
134
+ 1. **Get Token:**
135
+ - Go to https://huggingface.co/settings/tokens
136
+ - Click "New token"
137
+ - Create a token with "write" permissions
138
+
139
+ 2. **Login via CLI:**
140
+ ```bash
141
+ # Install Hugging Face CLI
142
+ pip install huggingface_hub
143
+
144
+ # Login (they'll be prompted for token)
145
+ huggingface-cli login
146
+ ```
147
+
148
+ 3. **Or Configure Git Credentials:**
149
+ ```bash
150
+ git config credential.helper store
151
+ ```
152
+
153
+ When they push for the first time, Git will ask for:
154
+ - Username: Their HF username
155
+ - Password: Their HF access token (NOT their account password)
156
+
157
+ ### 3. Make Changes and Push
158
+
159
+ ```bash
160
+ # Make changes to files
161
+ # ...
162
+
163
+ # Stage changes
164
+ git add .
165
+
166
+ # Commit
167
+ git commit -m "Add feature X"
168
+
169
+ # Push to Space
170
+ git push
171
+ ```
172
+
173
+ ---
174
+
175
+ ## Hugging Face Spaces Access Levels
176
+
177
+ Understanding permission levels helps you decide what access to grant:
178
+
179
+ | Role | Can View | Can Clone | Can Push | Can Manage Settings |
180
+ |------|----------|-----------|----------|---------------------|
181
+ | **Public** | βœ… | βœ… | ❌ | ❌ |
182
+ | **Read** | βœ… | βœ… | ❌ | ❌ |
183
+ | **Contributor** | βœ… | βœ… | Via PR only | ❌ |
184
+ | **Write** | βœ… | βœ… | βœ… | ❌ |
185
+ | **Admin** | βœ… | βœ… | βœ… | βœ… |
186
+
187
+ ---
188
+
189
+ ## Hackathon Submission Requirements
190
+
191
+ For your team submission to be valid, ensure:
192
+
193
+ ### Required in README.md:
194
+ - βœ… **Team section** with all member names and HF usernames
195
+ - βœ… **Track tag:** `mcp-in-action-track-01` (already added)
196
+ - βœ… **Demo video link** (1-5 minutes) - TODO
197
+ - βœ… **Social media post link** - TODO
198
+
199
+ ### Required in Space:
200
+ - βœ… Published as a Space in MCP-1st-Birthday organization
201
+ - βœ… App.py entry point (already created)
202
+ - βœ… Working Gradio interface
203
+ - βœ… All code created during hackathon period (Nov 14-30, 2025)
204
+
205
+ ---
206
+
207
+ ## Troubleshooting
208
+
209
+ ### "Permission denied" when team partner tries to push
210
+
211
+ **Solution:**
212
+ 1. Verify they're added as collaborators with write access
213
+ 2. Check they're using the correct HF access token (not account password)
214
+ 3. Ensure token has "write" permissions
215
+
216
+ ### "Repository not found" error
217
+
218
+ **Solution:**
219
+ 1. Verify the Space URL is correct
220
+ 2. Check they have at least "read" access
221
+ 3. Ensure they're logged in: `huggingface-cli whoami`
222
+
223
+ ### Team member can't see the Space
224
+
225
+ **Solution:**
226
+ 1. If Space is private, add them as collaborators
227
+ 2. If Space is public (recommended for hackathon), they should see it
228
+ 3. Check organization membership status
229
+
230
+ ---
231
+
232
+ ## Best Practices for Team Collaboration
233
+
234
+ 1. **Communication:**
235
+ - Use Discord channel: agents-mcp-hackathon-winter25
236
+ - Create a team group chat
237
+ - Document decisions in README
238
+
239
+ 2. **Code Management:**
240
+ - Pull before making changes: `git pull`
241
+ - Commit frequently with clear messages
242
+ - Test locally before pushing
243
+
244
+ 3. **Task Distribution:**
245
+ - Assign specific files/features to team members
246
+ - Avoid editing the same files simultaneously
247
+ - Use TODO comments in code
248
+
249
+ 4. **Version Control:**
250
+ - Create branches for major features (optional)
251
+ - Use descriptive commit messages
252
+ - Review each other's code
253
+
254
+ ---
255
+
256
+ ## Quick Reference Commands
257
+
258
+ ```bash
259
+ # Clone the Space
260
+ git clone https://huggingface.co/spaces/MCP-1st-Birthday/fleetmind-dispatch-ai
261
+
262
+ # Check current status
263
+ git status
264
+
265
+ # Pull latest changes
266
+ git pull
267
+
268
+ # Add all changes
269
+ git add .
270
+
271
+ # Commit with message
272
+ git commit -m "Description of changes"
273
+
274
+ # Push to Space
275
+ git push
276
+
277
+ # Check who you're logged in as
278
+ huggingface-cli whoami
279
+
280
+ # Login to HF
281
+ huggingface-cli login
282
+ ```
283
+
284
+ ---
285
+
286
+ ## Support & Resources
287
+
288
+ - **Hackathon Discord:** agents-mcp-hackathon-winter25 channel
289
+ - **Office Hours:** November 17-28 with Gradio team
290
+ - **HF Documentation:** https://huggingface.co/docs/hub/spaces
291
+ - **Git Documentation:** https://git-scm.com/doc
292
+
293
+ ---
294
+
295
+ ## Timeline Reminder
296
+
297
+ - **Start Date:** November 14, 2025
298
+ - **Submission Deadline:** November 30, 2025 (11:59 PM UTC)
299
+ - **Days Remaining:** Check dashboard regularly
300
+
301
+ Make sure all team members are added to README.md before the deadline!
302
+
303
+ ---
304
+
305
+ **Good luck with your hackathon submission! πŸš€**
app.py CHANGED
@@ -1,461 +1,27 @@
1
  """
2
- FleetMind MCP Server - Hugging Face Space Entry Point (Track 1)
3
-
4
- This file serves as the entry point for HuggingFace Space deployment.
5
- Exposes 29 MCP tools via Server-Sent Events (SSE) endpoint for AI clients.
6
-
7
- Architecture:
8
- User β†’ MCP Client (Claude Desktop, Continue, etc.)
9
- β†’ SSE Endpoint (this file)
10
- β†’ FleetMind MCP Server (server.py)
11
- β†’ Tools (chat/tools.py)
12
- β†’ Database (PostgreSQL)
13
-
14
- For Track 1: Building MCP Servers - Enterprise Category
15
- https://huggingface.co/MCP-1st-Birthday
16
-
17
- Compatible with:
18
- - Claude Desktop (via SSE transport)
19
- - Continue.dev (VS Code extension)
20
- - Cline (VS Code extension)
21
- - Any MCP client supporting SSE protocol
22
  """
23
 
24
- import os
25
  import sys
26
- import logging
27
  from pathlib import Path
28
 
29
- # Add project root to path
30
  sys.path.insert(0, str(Path(__file__).parent))
31
 
32
- # Configure logging for HuggingFace Space
33
- logging.basicConfig(
34
- level=logging.INFO,
35
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
36
- handlers=[logging.StreamHandler()]
37
- )
38
- logger = logging.getLogger(__name__)
39
-
40
- # Import the MCP server instance
41
- from server import mcp
42
-
43
- # ============================================================================
44
- # HUGGING FACE SPACE CONFIGURATION
45
- # ============================================================================
46
-
47
- # HuggingFace Space default port
48
- # NOTE: When using proxy.py, FastMCP runs on 7861 (internal port)
49
- # The proxy runs on 7860 (public) and forwards requests here
50
- HF_SPACE_PORT = int(os.getenv("PORT", 7861))
51
- HF_SPACE_HOST = os.getenv("HOST", "0.0.0.0")
52
-
53
- # ============================================================================
54
- # MAIN ENTRY POINT
55
- # ============================================================================
56
 
57
  if __name__ == "__main__":
58
- logger.info("=" * 70)
59
- logger.info("FleetMind MCP Server - HuggingFace Space (Track 1)")
60
- logger.info("=" * 70)
61
- logger.info("MCP Server: FleetMind Dispatch Coordinator v1.0.0")
62
- logger.info("Protocol: Model Context Protocol (MCP)")
63
- logger.info("Transport: Server-Sent Events (SSE)")
64
- logger.info(f"SSE Endpoint: https://mcp-1st-birthday-fleetmind-dispatch-ai.hf.space/sse")
65
- logger.info("=" * 70)
66
- logger.info("Features:")
67
- logger.info(" βœ“ 29 AI Tools (Order + Driver + Assignment Management)")
68
- logger.info(" βœ“ 2 Real-Time Resources (orders://all, drivers://all)")
69
- logger.info(" βœ“ Gemini 2.0 Flash AI - Intelligent Assignment")
70
- logger.info(" βœ“ Google Maps API Integration (Routes + Geocoding)")
71
- logger.info(" βœ“ Weather-Aware Routing (OpenWeatherMap)")
72
- logger.info(" βœ“ PostgreSQL Database (Neon)")
73
- logger.info("=" * 70)
74
- logger.info("Compatible Clients:")
75
- logger.info(" β€’ Claude Desktop")
76
- logger.info(" β€’ Continue.dev (VS Code)")
77
- logger.info(" β€’ Cline (VS Code)")
78
- logger.info(" β€’ Any MCP-compatible client")
79
- logger.info("=" * 70)
80
- logger.info("How to Connect (Claude Desktop):")
81
- logger.info(' Add to claude_desktop_config.json:')
82
- logger.info(' {')
83
- logger.info(' "mcpServers": {')
84
- logger.info(' "fleetmind": {')
85
- logger.info(' "command": "npx",')
86
- logger.info(' "args": [')
87
- logger.info(' "mcp-remote",')
88
- logger.info(' "https://mcp-1st-birthday-fleetmind-dispatch-ai.hf.space/sse"')
89
- logger.info(' ]')
90
- logger.info(' }')
91
- logger.info(' }')
92
- logger.info(' }')
93
- logger.info("=" * 70)
94
- logger.info(f"Starting SSE server on {HF_SPACE_HOST}:{HF_SPACE_PORT}...")
95
- logger.info("Waiting for MCP client connections...")
96
- logger.info("=" * 70)
97
-
98
- try:
99
- # Add web routes for landing page and API key generation
100
- from starlette.responses import HTMLResponse, JSONResponse
101
- from starlette.requests import Request
102
- from database.api_keys import generate_api_key as db_generate_api_key
103
-
104
- # =====================================================================
105
- # PROXY-BASED AUTHENTICATION
106
- # Authentication is handled by proxy.py (port 7860)
107
- # The proxy captures API keys from SSE connections and injects them
108
- # into tool requests before forwarding to FastMCP (port 7861)
109
- # =====================================================================
110
- logger.info("[Auth] Using proxy-based authentication")
111
- logger.info("[Auth] Proxy captures API keys and injects into tool requests")
112
-
113
- @mcp.custom_route("/", methods=["GET"])
114
- async def landing_page(request):
115
- """Landing page with MCP connection information"""
116
- return HTMLResponse("""
117
- <!DOCTYPE html>
118
- <html>
119
- <head>
120
- <title>FleetMind MCP Server</title>
121
- <style>
122
- body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif; max-width: 1000px; margin: 50px auto; padding: 20px; background: #0f172a; color: #e2e8f0; }
123
- .container { background: #1e293b; padding: 40px; border-radius: 12px; box-shadow: 0 8px 16px rgba(0,0,0,0.4); }
124
- h1 { color: #f1f5f9; margin-top: 0; }
125
- h2 { color: #e2e8f0; border-bottom: 2px solid #334155; padding-bottom: 10px; }
126
- h3 { color: #cbd5e1; }
127
- code { background: #334155; color: #60a5fa; padding: 3px 8px; border-radius: 4px; font-family: 'Courier New', monospace; }
128
- pre { background: #0f172a; color: #f1f5f9; padding: 20px; border-radius: 8px; overflow-x: auto; border: 1px solid #334155; }
129
- .endpoint { background: #1e3a5f; padding: 15px; margin: 15px 0; border-left: 4px solid #3b82f6; border-radius: 4px; }
130
- .feature { background: #134e4a; padding: 15px; margin: 10px 0; border-left: 4px solid #10b981; border-radius: 4px; }
131
- .badge { display: inline-block; background: #3b82f6; color: white; padding: 4px 12px; border-radius: 12px; font-size: 12px; margin: 5px; }
132
- a { color: #60a5fa; text-decoration: none; }
133
- a:hover { text-decoration: underline; color: #93c5fd; }
134
- ol { line-height: 1.8; }
135
- ul { line-height: 1.8; }
136
- p { color: #cbd5e1; }
137
- </style>
138
- </head>
139
- <body>
140
- <div class="container">
141
- <h1>🚚 FleetMind MCP Server</h1>
142
- <p><strong>Enterprise Model Context Protocol Server for AI-Powered Delivery Dispatch</strong></p>
143
- <p><span class="badge">MCP 1st Birthday Hackathon</span> <span class="badge">Track 1: Building MCP</span> <span class="badge">Enterprise Category</span></p>
144
-
145
- <hr style="margin: 30px 0; border: none; border-top: 1px solid #e5e7eb;">
146
-
147
- <h2>πŸ”Œ MCP Server Connection</h2>
148
-
149
- <div class="endpoint">
150
- <strong>SSE Endpoint URL:</strong><br>
151
- <code>https://mcp-1st-birthday-fleetmind-dispatch-ai.hf.space/sse</code>
152
- </div>
153
-
154
- <h3>πŸ”‘ Step 1: Get Your API Key</h3>
155
- <p style="text-align: center; margin: 20px 0;">
156
- <a href="/generate-key" style="display: inline-block; background: #3b82f6; color: white; padding: 15px 30px; border-radius: 8px; text-decoration: none; font-weight: bold; font-size: 18px;">
157
- Generate API Key β†’
158
- </a>
159
- </p>
160
- <p>Click the button above to generate your unique API key. You'll need this to authenticate with the server.</p>
161
-
162
- <h3>βš™οΈ Step 2: Configure Claude Desktop</h3>
163
- <p>Add this to your <code>claude_desktop_config.json</code> file:</p>
164
- <pre>{
165
- "mcpServers": {
166
- "fleetmind": {
167
- "command": "npx",
168
- "args": [
169
- "mcp-remote",
170
- "https://mcp-1st-birthday-fleetmind-dispatch-ai.hf.space/sse<strong style="color: #60a5fa;">?api_key=fm_your_api_key_here</strong>"
171
- ]
172
- }
173
- }
174
- }</pre>
175
- <p style="background: #1e3a5f; padding: 10px; border-radius: 6px; margin: 10px 0; border-left: 4px solid #3b82f6;">
176
- πŸ’‘ <strong>Important:</strong> Add your API key as a query parameter (<code>?api_key=...</code>) in the URL, not in the <code>env</code> section!
177
- </p>
178
-
179
- <h3>πŸ“‹ Step 3: Connect</h3>
180
- <ol>
181
- <li><strong>Generate your API key</strong> using the button above</li>
182
- <li>Install <a href="https://claude.ai/download" target="_blank">Claude Desktop</a></li>
183
- <li>Locate your <code>claude_desktop_config.json</code> file</li>
184
- <li>Add the configuration, replacing <code>fm_your_api_key_here</code> with your actual API key <strong>in the URL</strong></li>
185
- <li>Restart Claude Desktop</li>
186
- <li>Look for "FleetMind" in the πŸ”Œ tools menu</li>
187
- </ol>
188
-
189
- <hr style="margin: 30px 0; border: none; border-top: 1px solid #e5e7eb;">
190
-
191
- <h2>πŸ› οΈ Available Tools (29 Total)</h2>
192
-
193
- <div class="feature">
194
- <strong>πŸ“ Geocoding & Routing (3 tools):</strong><br>
195
- geocode_address, calculate_route, calculate_intelligent_route
196
- </div>
197
-
198
- <div class="feature">
199
- <strong>πŸ“¦ Order Management (8 tools):</strong><br>
200
- create_order, count_orders, fetch_orders, get_order_details, search_orders, get_incomplete_orders, update_order, delete_order
201
- </div>
202
-
203
- <div class="feature">
204
- <strong>πŸ‘₯ Driver Management (8 tools):</strong><br>
205
- create_driver, count_drivers, fetch_drivers, get_driver_details, search_drivers, get_available_drivers, update_driver, delete_driver
206
- </div>
207
-
208
- <div class="feature">
209
- <strong>πŸ”— Assignment Management (8 tools):</strong><br>
210
- create_assignment, <strong>auto_assign_order</strong>, <strong>intelligent_assign_order</strong>, get_assignment_details, update_assignment, unassign_order, complete_delivery, fail_delivery
211
- </div>
212
-
213
- <div class="feature">
214
- <strong>πŸ—‘οΈ Bulk Operations (2 tools):</strong><br>
215
- delete_all_orders, delete_all_drivers
216
- </div>
217
-
218
- <hr style="margin: 30px 0; border: none; border-top: 1px solid #e5e7eb;">
219
-
220
- <h2>⭐ Key Features</h2>
221
- <ul>
222
- <li><strong>πŸ”‘ API Key Authentication</strong> - Secure multi-tenant access with personal API keys (URL-based)</li>
223
- <li><strong>πŸ‘₯ Multi-Tenant Isolation</strong> - Complete data separation via user_id (deterministic from email)</li>
224
- <li><strong>🧠 Gemini 2.0 Flash AI</strong> - Intelligent order assignment with detailed reasoning</li>
225
- <li><strong>🌦️ Weather-Aware Routing</strong> - Safety-first delivery planning with OpenWeatherMap</li>
226
- <li><strong>🚦 Real-Time Traffic</strong> - Google Routes API integration with live traffic data</li>
227
- <li><strong>πŸ“Š SLA Tracking</strong> - Automatic on-time performance monitoring</li>
228
- <li><strong>πŸ—„οΈ PostgreSQL Database</strong> - Production-grade data storage with user_id filtering (Neon)</li>
229
- <li><strong>πŸš€ Multi-Client Support</strong> - Works with Claude Desktop, Continue, Cline, any MCP client</li>
230
- </ul>
231
-
232
- <h2>πŸ”’ Authentication & Security</h2>
233
- <div class="feature">
234
- <strong>How Authentication Works:</strong><br>
235
- 1. Generate API key via <a href="/generate-key">/generate-key</a><br>
236
- 2. API key hashed (SHA-256) before storage<br>
237
- 3. User ID generated: <code>user_&#123;MD5(email)[:12]&#125;</code><br>
238
- 4. Add key to URL: <code>?api_key=fm_...</code><br>
239
- 5. Server validates on each SSE connection<br>
240
- 6. All queries filter by user_id for isolation
241
- </div>
242
- <div class="feature">
243
- <strong>Security Features:</strong><br>
244
- βœ… One-Time Display (keys shown once)<br>
245
- βœ… Hashed Storage (SHA-256, never plaintext)<br>
246
- βœ… Database-Level Isolation (all tables have user_id)<br>
247
- βœ… Deterministic User IDs (same email β†’ same user_id)<br>
248
- βœ… Production Safeguards (ENV-based SKIP_AUTH protection)
249
- </div>
250
-
251
- <hr style="margin: 30px 0; border: none; border-top: 1px solid #e5e7eb;">
252
-
253
- <h2>πŸ“š Resources</h2>
254
- <ul>
255
- <li><strong>GitHub:</strong> <a href="https://github.com/mashrur-rahman-fahim/fleetmind-mcp" target="_blank">mashrur-rahman-fahim/fleetmind-mcp</a></li>
256
- <li><strong>HuggingFace Space:</strong> <a href="https://huggingface.co/spaces/MCP-1st-Birthday/fleetmind-dispatch-ai" target="_blank">MCP-1st-Birthday/fleetmind-dispatch-ai</a></li>
257
- <li><strong>MCP Protocol Docs:</strong> <a href="https://modelcontextprotocol.io" target="_blank">modelcontextprotocol.io</a></li>
258
- </ul>
259
-
260
- <hr style="margin: 30px 0; border: none; border-top: 1px solid #e5e7eb;">
261
-
262
- <p style="text-align: center; color: #6b7280; font-size: 14px;">
263
- FleetMind v1.0 - Built for MCP 1st Birthday Hackathon<br>
264
- 29 AI Tools | 2 Real-Time Resources | Enterprise-Ready
265
- </p>
266
- </div>
267
- </body>
268
- </html>
269
- """)
270
-
271
- @mcp.custom_route("/generate-key", methods=["GET", "POST"])
272
- async def generate_key_page(request):
273
- """API Key generation page"""
274
- if request.method == "GET":
275
- return HTMLResponse("""
276
- <!DOCTYPE html>
277
- <html>
278
- <head>
279
- <title>Generate FleetMind API Key</title>
280
- <style>
281
- body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif; max-width: 600px; margin: 50px auto; padding: 20px; background: #0f172a; color: #e2e8f0; }
282
- .container { background: #1e293b; padding: 40px; border-radius: 12px; box-shadow: 0 8px 16px rgba(0,0,0,0.4); }
283
- h1 { color: #f1f5f9; }
284
- input { width: 100%; padding: 12px; margin: 10px 0; border-radius: 6px; border: 1px solid #334155; background: #0f172a; color: #e2e8f0; font-size: 16px; }
285
- button { background: #3b82f6; color: white; border: none; padding: 12px 24px; border-radius: 6px; font-size: 16px; cursor: pointer; width: 100%; }
286
- button:hover { background: #2563eb; }
287
- button:disabled { background: #64748b; cursor: not-allowed; opacity: 0.6; }
288
- .info { background: #1e3a5f; padding: 15px; border-radius: 6px; margin: 15px 0; border-left: 4px solid #3b82f6; }
289
- </style>
290
- <script>
291
- function handleGenerateKey(event) {
292
- const button = event.target.querySelector('button');
293
- button.textContent = 'Generating...';
294
- button.disabled = true;
295
- }
296
- </script>
297
- </head>
298
- <body>
299
- <div class="container">
300
- <h1>πŸ”‘ Generate API Key</h1>
301
- <p>Create your FleetMind MCP Server API key</p>
302
-
303
- <div class="info">
304
- <strong>πŸ“‹ What you'll need:</strong><br>
305
- β€’ Your email address (used to generate your unique user_id)<br>
306
- β€’ Your name (optional)<br>
307
- <br>
308
- <strong>πŸ” What you'll get:</strong><br>
309
- β€’ API Key: <code>fm_xxxxx...</code> (show once, copy immediately!)<br>
310
- β€’ User ID: <code>user_xxxxx</code> (deterministic from your email)<br>
311
- β€’ All your data (orders/drivers/assignments) will be isolated by this user_id
312
- </div>
313
-
314
- <form method="POST" onsubmit="handleGenerateKey(event)">
315
- <input type="email" name="email" placeholder="Your email address" required>
316
- <input type="text" name="name" placeholder="Your name (optional)">
317
- <button type="submit">Generate API Key</button>
318
- </form>
319
-
320
- <p style="text-align: center; margin-top: 20px;">
321
- <a href="/" style="color: #60a5fa; text-decoration: none;">← Back to Home</a>
322
- </p>
323
- </div>
324
- </body>
325
- </html>
326
- """)
327
-
328
- # POST - Generate API key
329
- else:
330
- try:
331
- form_data = await request.form()
332
- email = form_data.get("email")
333
- name = form_data.get("name") or None
334
-
335
- if not email:
336
- return HTMLResponse("<h1>Error: Email is required</h1>", status_code=400)
337
-
338
- result = db_generate_api_key(email, name)
339
-
340
- if not result['success']:
341
- return HTMLResponse(f"<h1>Error: {result['error']}</h1>", status_code=400)
342
-
343
- # Success - show the API key (one time only!)
344
- return HTMLResponse(f"""
345
- <!DOCTYPE html>
346
- <html>
347
- <head>
348
- <title>Your FleetMind API Key</title>
349
- <style>
350
- body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif; max-width: 800px; margin: 50px auto; padding: 20px; background: #0f172a; color: #e2e8f0; }}
351
- .container {{ background: #1e293b; padding: 40px; border-radius: 12px; box-shadow: 0 8px 16px rgba(0,0,0,0.4); }}
352
- h1 {{ color: #f1f5f9; }}
353
- .success {{ background: #10b981; padding: 15px; border-radius: 6px; margin: 15px 0; }}
354
- .warning {{ background: #ef4444; padding: 15px; border-radius: 6px; margin: 15px 0; }}
355
- code {{ background: #334155; color: #60a5fa; padding: 3px 8px; border-radius: 4px; font-family: 'Courier New', monospace; display: block; margin: 10px 0; word-wrap: break-word; font-size: 14px; }}
356
- pre {{ background: #0f172a; color: #f1f5f9; padding: 20px; border-radius: 8px; overflow-x: auto; border: 1px solid #334155; }}
357
- button {{ background: #3b82f6; color: white; border: none; padding: 12px 24px; border-radius: 6px; font-size: 16px; cursor: pointer; margin: 10px 5px; }}
358
- button:hover {{ background: #2563eb; }}
359
- </style>
360
- <script>
361
- function copyKey() {{
362
- navigator.clipboard.writeText('{result["api_key"]}');
363
- alert('API key copied to clipboard!');
364
- }}
365
- </script>
366
- </head>
367
- <body>
368
- <div class="container">
369
- <h1>βœ… API Key Generated!</h1>
370
-
371
- <div class="success">
372
- <strong>Your API key has been created successfully</strong>
373
- </div>
374
-
375
- <p><strong>πŸ“§ Email:</strong> {result["email"]}</p>
376
- <p><strong>πŸ‘€ Name:</strong> {result["name"]}</p>
377
- <p><strong>πŸ†” User ID:</strong> <code>{result["user_id"]}</code></p>
378
-
379
- <div class="warning">
380
- <strong>⚠️ SAVE THIS KEY NOW - IT WON'T BE SHOWN AGAIN!</strong>
381
- </div>
382
-
383
- <h2>πŸ”‘ Your API Key:</h2>
384
- <code>{result["api_key"]}</code>
385
- <button onclick="copyKey()">πŸ“‹ Copy Key</button>
386
-
387
- <h2>πŸ‘₯ Multi-Tenant Isolation</h2>
388
- <p style="background: #1e3a5f; padding: 15px; border-radius: 6px; margin: 10px 0; border-left: 4px solid #3b82f6;">
389
- <strong>Your user_id (<code>{result["user_id"]}</code>) ensures complete data isolation:</strong><br>
390
- βœ… You will only see your own orders, drivers, and assignments<br>
391
- βœ… All database operations automatically filter by your user_id<br>
392
- βœ… Your user_id is deterministic - same email always gets same ID<br>
393
- βœ… Even if you regenerate your API key, your user_id stays the same
394
- </p>
395
-
396
- <h2>πŸ“‹ Claude Desktop Setup:</h2>
397
- <p>Add this to your <code>claude_desktop_config.json</code>:</p>
398
- <pre>{{
399
- "mcpServers": {{
400
- "fleetmind": {{
401
- "command": "npx",
402
- "args": [
403
- "mcp-remote",
404
- "https://mcp-1st-birthday-fleetmind-dispatch-ai.hf.space/sse?api_key={result["api_key"]}"
405
- ]
406
- }}
407
- }}
408
- }}</pre>
409
- <p style="background: #1e3a5f; padding: 10px; border-radius: 6px; margin: 10px 0; border-left: 4px solid #3b82f6;">
410
- πŸ’‘ <strong>Important:</strong> The API key is included in the URL as a query parameter (<code>?api_key=...</code>)
411
- </p>
412
-
413
- <h2>πŸš€ Next Steps:</h2>
414
- <ol>
415
- <li>Copy your API key (click the button above)</li>
416
- <li>Add it to your Claude Desktop config</li>
417
- <li>Restart Claude Desktop</li>
418
- <li>Start using FleetMind tools!</li>
419
- </ol>
420
-
421
- <h2>πŸ” How Authentication Works:</h2>
422
- <div style="background: #134e4a; padding: 15px; border-radius: 6px; margin: 10px 0; border-left: 4px solid #10b981;">
423
- <strong>Authentication Flow:</strong><br><br>
424
- 1️⃣ <strong>Connection:</strong> Claude Desktop connects to SSE endpoint with your API key in URL<br>
425
- 2️⃣ <strong>Validation:</strong> Server hashes your key (SHA-256) and looks it up in database<br>
426
- 3️⃣ <strong>User Extraction:</strong> Server retrieves your user_id: <code>{result["user_id"]}</code><br>
427
- 4️⃣ <strong>Data Isolation:</strong> All tool calls automatically filter by your user_id<br>
428
- 5️⃣ <strong>Security:</strong> You can only access data associated with your user_id<br>
429
- </div>
430
-
431
- <p style="text-align: center; margin-top: 30px;">
432
- <a href="/" style="color: #60a5fa; text-decoration: none;">← Back to Home</a>
433
- </p>
434
- </div>
435
- </body>
436
- </html>
437
- """)
438
-
439
- except Exception as e:
440
- return HTMLResponse(f"<h1>Error: {str(e)}</h1>", status_code=500)
441
-
442
- logger.info("[OK] Landing page added at / route")
443
- logger.info("[OK] API key generation page added at /generate-key")
444
- logger.info("[OK] MCP SSE endpoint available at /sse")
445
- logger.info("[OK] Authentication handled by proxy.py (port 7860)")
446
-
447
- # Run MCP server with SSE transport
448
- # No middleware needed - proxy.py handles API key capture and injection
449
- mcp.run(
450
- transport="sse",
451
- host=HF_SPACE_HOST,
452
- port=HF_SPACE_PORT
453
- )
454
-
455
- except Exception as e:
456
- logger.error(f"Failed to start server: {e}")
457
- logger.error("Check that:")
458
- logger.error(" 1. Database connection is configured (DB_HOST, DB_USER, etc.)")
459
- logger.error(" 2. Google Maps API key is set (GOOGLE_MAPS_API_KEY)")
460
- logger.error(" 3. Port 7860 is available")
461
- raise
 
1
  """
2
+ FleetMind MCP - Hugging Face Spaces Entry Point
3
+ This is the main entry point for the HF Space deployment
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  """
5
 
 
6
  import sys
 
7
  from pathlib import Path
8
 
9
+ # Add current directory to path
10
  sys.path.insert(0, str(Path(__file__).parent))
11
 
12
+ # Import and launch the Gradio app
13
+ from ui.app import create_interface
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
 
15
  if __name__ == "__main__":
16
+ print("=" * 60)
17
+ print("FleetMind MCP - Starting on Hugging Face Spaces")
18
+ print("=" * 60)
19
+
20
+ # Create and launch the interface
21
+ app = create_interface()
22
+ app.launch(
23
+ server_name="0.0.0.0",
24
+ server_port=7860,
25
+ share=False,
26
+ show_error=True
27
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
chat/__init__.py CHANGED
@@ -1,9 +1,9 @@
1
  """
2
- Chat package for FleetMind MCP Server (Track 1)
3
- Contains geocoding service and tool handlers for MCP server
4
  """
5
 
6
- # Only tools and geocoding are needed for MCP server
7
- # ChatEngine and ConversationManager are archived (were for Gradio UI)
8
 
9
- __all__ = []
 
1
  """
2
+ Chat package for FleetMind MCP
3
+ Handles AI-powered natural language order creation
4
  """
5
 
6
+ from .chat_engine import ChatEngine
7
+ from .conversation import ConversationManager
8
 
9
+ __all__ = ['ChatEngine', 'ConversationManager']
chat/chat_engine.py ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Chat engine for FleetMind
3
+ Main orchestrator for AI-powered conversations with multi-provider support
4
+ """
5
+
6
+ import os
7
+ import logging
8
+ from typing import Tuple, List, Dict
9
+
10
+ from chat.providers import ClaudeProvider, GeminiProvider
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class ChatEngine:
16
+ """Main orchestrator for AI chat conversations with multi-provider support"""
17
+
18
+ def __init__(self):
19
+ # Get provider selection from environment
20
+ provider_name = os.getenv("AI_PROVIDER", "anthropic").lower()
21
+
22
+ logger.info(f"ChatEngine: Selected provider: {provider_name}")
23
+
24
+ # Initialize the selected provider
25
+ if provider_name == "gemini":
26
+ self.provider = GeminiProvider()
27
+ logger.info("ChatEngine: Using Gemini provider")
28
+ elif provider_name == "anthropic":
29
+ self.provider = ClaudeProvider()
30
+ logger.info("ChatEngine: Using Claude provider")
31
+ else:
32
+ # Default to Claude if unknown provider
33
+ logger.warning(f"ChatEngine: Unknown provider '{provider_name}', defaulting to Claude")
34
+ self.provider = ClaudeProvider()
35
+
36
+ # Store provider name for UI
37
+ self.selected_provider = provider_name
38
+
39
+ def is_available(self) -> bool:
40
+ """Check if the chat engine is available"""
41
+ return self.provider.is_available()
42
+
43
+ def get_status(self) -> str:
44
+ """Get status message for UI"""
45
+ provider_status = self.provider.get_status()
46
+ provider_name = self.provider.get_provider_name()
47
+
48
+ return f"**{provider_name}:** {provider_status}"
49
+
50
+ def get_provider_name(self) -> str:
51
+ """Get the active provider name"""
52
+ return self.provider.get_provider_name()
53
+
54
+ def get_model_name(self) -> str:
55
+ """Get the active model name"""
56
+ return self.provider.get_model_name()
57
+
58
+ def process_message(
59
+ self,
60
+ user_message: str,
61
+ conversation
62
+ ) -> Tuple[str, List[Dict]]:
63
+ """
64
+ Process user message and return AI response
65
+
66
+ Args:
67
+ user_message: User's message
68
+ conversation: ConversationManager instance
69
+
70
+ Returns:
71
+ Tuple of (assistant_response, tool_calls_made)
72
+ """
73
+ return self.provider.process_message(user_message, conversation)
74
+
75
+ def get_welcome_message(self) -> str:
76
+ """Get welcome message for new conversations"""
77
+ return self.provider.get_welcome_message()
78
+
79
+ def get_full_status(self) -> Dict[str, str]:
80
+ """
81
+ Get detailed status for all providers
82
+
83
+ Returns:
84
+ Dict with status for each provider
85
+ """
86
+ # Get status without creating new instances (avoid API calls)
87
+ claude_key = os.getenv("ANTHROPIC_API_KEY", "")
88
+ gemini_key = os.getenv("GOOGLE_API_KEY", "")
89
+
90
+ claude_available = bool(claude_key and not claude_key.startswith("your_"))
91
+ gemini_available = bool(gemini_key and not gemini_key.startswith("your_"))
92
+
93
+ claude_status = "βœ… Connected - Model: claude-3-5-sonnet-20241022" if claude_available else "⚠️ Not configured (add ANTHROPIC_API_KEY)"
94
+ gemini_status = f"βœ… Connected - Model: {self.provider.get_model_name()}" if (self.selected_provider == "gemini" and gemini_available) else "⚠️ Not configured (add GOOGLE_API_KEY)" if not gemini_available else "βœ… Configured"
95
+
96
+ return {
97
+ "selected": self.selected_provider,
98
+ "claude": {
99
+ "name": "Claude (Anthropic)",
100
+ "status": claude_status,
101
+ "available": claude_available
102
+ },
103
+ "gemini": {
104
+ "name": "Gemini (Google)",
105
+ "status": gemini_status,
106
+ "available": gemini_available
107
+ }
108
+ }
chat/conversation.py ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Conversation manager for FleetMind chat
3
+ Handles conversation state and history
4
+ """
5
+
6
+ import logging
7
+ from typing import List, Dict
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ class ConversationManager:
13
+ """Manage conversation state and history"""
14
+
15
+ def __init__(self):
16
+ self.history = []
17
+ self.tool_calls = [] # Track all tool calls for transparency
18
+ self.order_context = {} # Accumulated order details
19
+
20
+ def add_message(self, role: str, content: str):
21
+ """
22
+ Add message to conversation history
23
+
24
+ Args:
25
+ role: "user" or "assistant"
26
+ content: Message content
27
+ """
28
+ self.history.append({
29
+ "role": role,
30
+ "content": content
31
+ })
32
+
33
+ def add_tool_result(self, tool_name: str, tool_input: dict, tool_result: dict):
34
+ """
35
+ Track tool usage for transparency
36
+
37
+ Args:
38
+ tool_name: Name of the tool called
39
+ tool_input: Input parameters
40
+ tool_result: Result from tool execution
41
+ """
42
+ self.tool_calls.append({
43
+ "tool": tool_name,
44
+ "input": tool_input,
45
+ "result": tool_result
46
+ })
47
+
48
+ def get_history(self) -> List[Dict]:
49
+ """Get full conversation history"""
50
+ return self.history
51
+
52
+ def get_tool_calls(self) -> List[Dict]:
53
+ """Get all tool calls made in this conversation"""
54
+ return self.tool_calls
55
+
56
+ def get_last_tool_call(self) -> Dict:
57
+ """Get the most recent tool call"""
58
+ if self.tool_calls:
59
+ return self.tool_calls[-1]
60
+ return {}
61
+
62
+ def clear_tool_calls(self):
63
+ """Clear tool call history"""
64
+ self.tool_calls = []
65
+
66
+ def reset(self):
67
+ """Start a new conversation"""
68
+ self.history = []
69
+ self.tool_calls = []
70
+ self.order_context = {}
71
+ logger.info("Conversation reset")
72
+
73
+ def get_message_count(self) -> int:
74
+ """Get number of messages in conversation"""
75
+ return len(self.history)
76
+
77
+ def get_formatted_history(self) -> List[Dict]:
78
+ """
79
+ Get history formatted for Gradio chatbot (messages format)
80
+
81
+ Returns:
82
+ List of message dictionaries with 'role' and 'content' keys
83
+ """
84
+ # For Gradio type="messages", return list of dicts with role/content
85
+ return self.history
chat/geocoding.py CHANGED
@@ -1,23 +1,15 @@
1
  """
2
  Geocoding service for FleetMind
3
- Handles address validation with Google Maps API and smart mock fallback
4
  """
5
 
6
  import os
7
- import googlemaps
8
  import logging
9
- import asyncio
10
- from concurrent.futures import ThreadPoolExecutor
11
  from typing import Dict, Optional
12
 
13
  logger = logging.getLogger(__name__)
14
 
15
- # Thread pool for running blocking geocoding calls
16
- _geocoding_executor = ThreadPoolExecutor(max_workers=3, thread_name_prefix="geocoding")
17
-
18
- # Timeout for geocoding operations (seconds)
19
- GEOCODING_TIMEOUT = 10
20
-
21
  # Common city coordinates for mock geocoding
22
  CITY_COORDINATES = {
23
  "san francisco": (37.7749, -122.4194),
@@ -44,33 +36,20 @@ CITY_COORDINATES = {
44
 
45
 
46
  class GeocodingService:
47
- """Handle address geocoding with Google Maps API and smart mock fallback"""
48
 
49
  def __init__(self):
50
- self.google_maps_key = os.getenv("GOOGLE_MAPS_API_KEY", "")
51
- self.use_mock = not self.google_maps_key or self.google_maps_key.startswith("your_")
52
 
53
  if self.use_mock:
54
- logger.info("Geocoding: Using mock (GOOGLE_MAPS_API_KEY not configured)")
55
- self.gmaps_client = None
56
  else:
57
- try:
58
- # Configure with timeout to prevent hanging
59
- self.gmaps_client = googlemaps.Client(
60
- key=self.google_maps_key,
61
- timeout=GEOCODING_TIMEOUT, # 10 second timeout
62
- retry_timeout=GEOCODING_TIMEOUT # Total retry timeout
63
- )
64
- logger.info("Geocoding: Using Google Maps API (timeout: 10s)")
65
- except Exception as e:
66
- logger.error(f"Failed to initialize Google Maps client: {e}")
67
- self.use_mock = True
68
- self.gmaps_client = None
69
 
70
  def geocode(self, address: str) -> Dict:
71
  """
72
- Geocode address, using mock if API unavailable.
73
- This is a synchronous method with built-in timeout.
74
 
75
  Args:
76
  address: Street address to geocode
@@ -82,65 +61,40 @@ class GeocodingService:
82
  return self._geocode_mock(address)
83
  else:
84
  try:
85
- return self._geocode_google(address)
86
  except Exception as e:
87
- logger.error(f"Google Maps API failed: {e}, falling back to mock")
88
  return self._geocode_mock(address)
89
 
90
- async def geocode_async(self, address: str) -> Dict:
91
- """
92
- Async-safe geocoding that runs blocking calls in a thread pool.
93
- Use this in async contexts to prevent blocking the event loop.
94
-
95
- Args:
96
- address: Street address to geocode
97
-
98
- Returns:
99
- Dict with keys: lat, lng, formatted_address, confidence
100
- """
101
- if self.use_mock:
102
- return self._geocode_mock(address)
103
 
104
- loop = asyncio.get_event_loop()
105
- try:
106
- # Run blocking geocode in thread pool with timeout
107
- result = await asyncio.wait_for(
108
- loop.run_in_executor(_geocoding_executor, self._geocode_google, address),
109
- timeout=GEOCODING_TIMEOUT + 2 # Extra buffer beyond client timeout
110
- )
111
- return result
112
- except asyncio.TimeoutError:
113
- logger.error(f"Geocoding timed out for address: {address}, falling back to mock")
114
- return self._geocode_mock(address)
115
- except Exception as e:
116
- logger.error(f"Async geocoding failed: {e}, falling back to mock")
117
- return self._geocode_mock(address)
118
 
119
- def _geocode_google(self, address: str) -> Dict:
120
- """Real Google Maps API geocoding"""
121
- try:
122
- # Call Google Maps Geocoding API
123
- result = self.gmaps_client.geocode(address)
124
 
125
- if not result:
126
- # No results found, fall back to mock
127
- logger.warning(f"Google Maps API found no results for: {address}")
128
- return self._geocode_mock(address)
129
 
130
- # Get first result
131
- first_result = result[0]
132
- location = first_result['geometry']['location']
 
133
 
134
- return {
135
- "lat": location['lat'],
136
- "lng": location['lng'],
137
- "formatted_address": first_result.get('formatted_address', address),
138
- "confidence": "high (Google Maps API)"
139
- }
140
 
141
- except Exception as e:
142
- logger.error(f"Google Maps geocoding error: {e}")
143
- raise
 
 
 
144
 
145
  def _geocode_mock(self, address: str) -> Dict:
146
  """
@@ -169,144 +123,9 @@ class GeocodingService:
169
  "confidence": "low (mock - default)"
170
  }
171
 
172
- def reverse_geocode(self, lat: float, lng: float) -> Dict:
173
- """
174
- Reverse geocode coordinates to address.
175
- This is a synchronous method with built-in timeout.
176
-
177
- Args:
178
- lat: Latitude
179
- lng: Longitude
180
-
181
- Returns:
182
- Dict with keys: address, city, state, country, formatted_address
183
- """
184
- if self.use_mock:
185
- return self._reverse_geocode_mock(lat, lng)
186
- else:
187
- try:
188
- return self._reverse_geocode_google(lat, lng)
189
- except Exception as e:
190
- logger.error(f"Google Maps reverse geocoding failed: {e}, falling back to mock")
191
- return self._reverse_geocode_mock(lat, lng)
192
-
193
- async def reverse_geocode_async(self, lat: float, lng: float) -> Dict:
194
- """
195
- Async-safe reverse geocoding that runs blocking calls in a thread pool.
196
- Use this in async contexts to prevent blocking the event loop.
197
-
198
- Args:
199
- lat: Latitude
200
- lng: Longitude
201
-
202
- Returns:
203
- Dict with keys: address, city, state, country, formatted_address
204
- """
205
- if self.use_mock:
206
- return self._reverse_geocode_mock(lat, lng)
207
-
208
- loop = asyncio.get_event_loop()
209
- try:
210
- # Run blocking reverse_geocode in thread pool with timeout
211
- result = await asyncio.wait_for(
212
- loop.run_in_executor(
213
- _geocoding_executor,
214
- self._reverse_geocode_google,
215
- lat,
216
- lng
217
- ),
218
- timeout=GEOCODING_TIMEOUT + 2
219
- )
220
- return result
221
- except asyncio.TimeoutError:
222
- logger.error(f"Reverse geocoding timed out for ({lat}, {lng}), falling back to mock")
223
- return self._reverse_geocode_mock(lat, lng)
224
- except Exception as e:
225
- logger.error(f"Async reverse geocoding failed: {e}, falling back to mock")
226
- return self._reverse_geocode_mock(lat, lng)
227
-
228
- def _reverse_geocode_google(self, lat: float, lng: float) -> Dict:
229
- """Real Google Maps API reverse geocoding"""
230
- try:
231
- # Call Google Maps Reverse Geocoding API
232
- result = self.gmaps_client.reverse_geocode((lat, lng))
233
-
234
- if not result:
235
- logger.warning(f"Google Maps API found no results for: ({lat}, {lng})")
236
- return self._reverse_geocode_mock(lat, lng)
237
-
238
- # Get first result
239
- first_result = result[0]
240
-
241
- # Extract address components
242
- address_components = first_result.get('address_components', [])
243
- city = ""
244
- state = ""
245
- country = ""
246
-
247
- for component in address_components:
248
- types = component.get('types', [])
249
- if 'locality' in types:
250
- city = component.get('long_name', '')
251
- elif 'administrative_area_level_1' in types:
252
- state = component.get('short_name', '')
253
- elif 'country' in types:
254
- country = component.get('long_name', '')
255
-
256
- return {
257
- "address": first_result.get('formatted_address', f"{lat}, {lng}"),
258
- "city": city,
259
- "state": state,
260
- "country": country,
261
- "formatted_address": first_result.get('formatted_address', f"{lat}, {lng}"),
262
- "confidence": "high (Google Maps API)"
263
- }
264
-
265
- except Exception as e:
266
- logger.error(f"Google Maps reverse geocoding error: {e}")
267
- raise
268
-
269
- def _reverse_geocode_mock(self, lat: float, lng: float) -> Dict:
270
- """
271
- Mock reverse geocoding
272
- Tries to match coordinates to known cities
273
- """
274
- # Find closest city
275
- min_distance = float('inf')
276
- closest_city = "Unknown Location"
277
-
278
- for city, coords in CITY_COORDINATES.items():
279
- # Simple distance calculation
280
- distance = ((lat - coords[0]) ** 2 + (lng - coords[1]) ** 2) ** 0.5
281
- if distance < min_distance:
282
- min_distance = distance
283
- closest_city = city
284
-
285
- # If very close to a known city (within ~0.1 degrees, roughly 11km)
286
- if min_distance < 0.1:
287
- logger.info(f"Mock reverse geocoding: Matched to {closest_city}")
288
- return {
289
- "address": f"Near {closest_city.title()}",
290
- "city": closest_city.title(),
291
- "state": "CA" if "san francisco" in closest_city or "la" in closest_city else "",
292
- "country": "USA",
293
- "formatted_address": f"Near {closest_city.title()}, USA",
294
- "confidence": f"medium (mock - near {closest_city})"
295
- }
296
- else:
297
- logger.info(f"Mock reverse geocoding: Unknown location at ({lat}, {lng})")
298
- return {
299
- "address": f"{lat}, {lng}",
300
- "city": "",
301
- "state": "",
302
- "country": "",
303
- "formatted_address": f"Coordinates: {lat}, {lng}",
304
- "confidence": "low (mock - no match)"
305
- }
306
-
307
  def get_status(self) -> str:
308
  """Get geocoding service status"""
309
  if self.use_mock:
310
- return "⚠️ Using mock geocoding (add GOOGLE_MAPS_API_KEY for real)"
311
  else:
312
- return "βœ… Google Maps API connected"
 
1
  """
2
  Geocoding service for FleetMind
3
+ Handles address validation with HERE API and smart mock fallback
4
  """
5
 
6
  import os
7
+ import requests
8
  import logging
 
 
9
  from typing import Dict, Optional
10
 
11
  logger = logging.getLogger(__name__)
12
 
 
 
 
 
 
 
13
  # Common city coordinates for mock geocoding
14
  CITY_COORDINATES = {
15
  "san francisco": (37.7749, -122.4194),
 
36
 
37
 
38
  class GeocodingService:
39
+ """Handle address geocoding with HERE API and smart mock fallback"""
40
 
41
  def __init__(self):
42
+ self.here_api_key = os.getenv("HERE_API_KEY", "")
43
+ self.use_mock = not self.here_api_key or self.here_api_key.startswith("your_")
44
 
45
  if self.use_mock:
46
+ logger.info("Geocoding: Using mock (HERE_API_KEY not configured)")
 
47
  else:
48
+ logger.info("Geocoding: Using HERE Maps API")
 
 
 
 
 
 
 
 
 
 
 
49
 
50
  def geocode(self, address: str) -> Dict:
51
  """
52
+ Geocode address, using mock if API unavailable
 
53
 
54
  Args:
55
  address: Street address to geocode
 
61
  return self._geocode_mock(address)
62
  else:
63
  try:
64
+ return self._geocode_here(address)
65
  except Exception as e:
66
+ logger.error(f"HERE API failed: {e}, falling back to mock")
67
  return self._geocode_mock(address)
68
 
69
+ def _geocode_here(self, address: str) -> Dict:
70
+ """Real HERE API geocoding"""
71
+ url = "https://geocode.search.hereapi.com/v1/geocode"
 
 
 
 
 
 
 
 
 
 
72
 
73
+ params = {
74
+ "q": address,
75
+ "apiKey": self.here_api_key
76
+ }
 
 
 
 
 
 
 
 
 
 
77
 
78
+ response = requests.get(url, params=params, timeout=10)
79
+ response.raise_for_status()
 
 
 
80
 
81
+ data = response.json()
 
 
 
82
 
83
+ if not data.get("items"):
84
+ # No results found, fall back to mock
85
+ logger.warning(f"HERE API found no results for: {address}")
86
+ return self._geocode_mock(address)
87
 
88
+ # Get first result
89
+ item = data["items"][0]
90
+ position = item["position"]
 
 
 
91
 
92
+ return {
93
+ "lat": position["lat"],
94
+ "lng": position["lng"],
95
+ "formatted_address": item.get("address", {}).get("label", address),
96
+ "confidence": "high (HERE API)"
97
+ }
98
 
99
  def _geocode_mock(self, address: str) -> Dict:
100
  """
 
123
  "confidence": "low (mock - default)"
124
  }
125
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
  def get_status(self) -> str:
127
  """Get geocoding service status"""
128
  if self.use_mock:
129
+ return "⚠️ Using mock geocoding (add HERE_API_KEY for real)"
130
  else:
131
+ return "βœ… HERE Maps API connected"
chat/providers/__init__.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ AI Provider implementations for FleetMind chat
3
+ Supports multiple AI providers (Anthropic Claude, Google Gemini)
4
+ """
5
+
6
+ from .base_provider import AIProvider
7
+ from .claude_provider import ClaudeProvider
8
+ from .gemini_provider import GeminiProvider
9
+
10
+ __all__ = ['AIProvider', 'ClaudeProvider', 'GeminiProvider']
chat/providers/base_provider.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Base provider interface for AI providers
3
+ """
4
+
5
+ from abc import ABC, abstractmethod
6
+ from typing import Tuple, List, Dict
7
+
8
+
9
+ class AIProvider(ABC):
10
+ """Abstract base class for AI providers"""
11
+
12
+ @abstractmethod
13
+ def is_available(self) -> bool:
14
+ """Check if the provider is available (API key configured)"""
15
+ pass
16
+
17
+ @abstractmethod
18
+ def get_status(self) -> str:
19
+ """Get status message for UI"""
20
+ pass
21
+
22
+ @abstractmethod
23
+ def get_provider_name(self) -> str:
24
+ """Get provider name (e.g., 'Claude', 'Gemini')"""
25
+ pass
26
+
27
+ @abstractmethod
28
+ def get_model_name(self) -> str:
29
+ """Get model name (e.g., 'claude-3-5-sonnet-20241022')"""
30
+ pass
31
+
32
+ @abstractmethod
33
+ def process_message(
34
+ self,
35
+ user_message: str,
36
+ conversation
37
+ ) -> Tuple[str, List[Dict]]:
38
+ """
39
+ Process user message and return AI response
40
+
41
+ Args:
42
+ user_message: User's message
43
+ conversation: ConversationManager instance
44
+
45
+ Returns:
46
+ Tuple of (assistant_response, tool_calls_made)
47
+ """
48
+ pass
49
+
50
+ @abstractmethod
51
+ def get_welcome_message(self) -> str:
52
+ """Get welcome message for new conversations"""
53
+ pass
chat/providers/claude_provider.py ADDED
@@ -0,0 +1,271 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Anthropic Claude provider for FleetMind chat
3
+ """
4
+
5
+ import os
6
+ import logging
7
+ from typing import Tuple, List, Dict
8
+ from anthropic import Anthropic, APIError, APIConnectionError, AuthenticationError
9
+
10
+ from chat.providers.base_provider import AIProvider
11
+ from chat.tools import TOOLS_SCHEMA, execute_tool
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class ClaudeProvider(AIProvider):
17
+ """Anthropic Claude AI provider"""
18
+
19
+ def __init__(self):
20
+ self.api_key = os.getenv("ANTHROPIC_API_KEY", "")
21
+ self.api_available = bool(self.api_key and not self.api_key.startswith("your_"))
22
+
23
+ if self.api_available:
24
+ try:
25
+ self.client = Anthropic(api_key=self.api_key)
26
+ logger.info("ClaudeProvider: Initialized successfully")
27
+ except Exception as e:
28
+ logger.error(f"ClaudeProvider: Failed to initialize: {e}")
29
+ self.api_available = False
30
+ else:
31
+ self.client = None
32
+ logger.warning("ClaudeProvider: ANTHROPIC_API_KEY not configured")
33
+
34
+ self.model = "claude-3-5-sonnet-20241022"
35
+ self.system_prompt = self._get_system_prompt()
36
+
37
+ def _get_system_prompt(self) -> str:
38
+ """Get the system prompt for Claude"""
39
+ return """You are an AI assistant for FleetMind, a delivery dispatch system. Your job is to help coordinators create delivery orders efficiently.
40
+
41
+ **IMPORTANT: When a user wants to create an order, FIRST show them this order form:**
42
+
43
+ πŸ“‹ **Order Information Form**
44
+ Please provide the following details:
45
+
46
+ **Required Fields:**
47
+ β€’ Customer Name: [Full name]
48
+ β€’ Delivery Address: [Street address, city, state, zip]
49
+ β€’ Contact: [Phone number OR email address]
50
+
51
+ **Optional Fields:**
52
+ β€’ Delivery Deadline: [Date/time, or "ASAP" - default: 6 hours from now]
53
+ β€’ Priority: [standard/express/urgent - default: standard]
54
+ β€’ Special Instructions: [Any special notes]
55
+ β€’ Package Weight: [In kg - default: 5.0 kg]
56
+
57
+ **Example:**
58
+ "Customer: John Doe, Address: 123 Main St, San Francisco, CA 94103, Phone: 555-1234, Deliver by 5 PM today"
59
+
60
+ ---
61
+
62
+ **Your Workflow:**
63
+ 1. **If user says "create order" or similar:** Show the form above and ask them to provide the information
64
+ 2. **If they provide all/most info:** Proceed immediately with geocoding and order creation
65
+ 3. **If information is missing:** Show what's missing from the form and ask for those specific fields
66
+ 4. **After collecting required fields:**
67
+ - Use `geocode_address` tool to validate the address
68
+ - Use `create_order` tool to save the order
69
+ - Provide a clear confirmation with order ID
70
+
71
+ **Important Rules:**
72
+ - ALWAYS geocode the address BEFORE creating an order
73
+ - Be efficient - don't ask questions one at a time
74
+ - Accept information in any format (natural language, bullet points, etc.)
75
+ - Keep responses concise and professional
76
+ - Show enthusiasm when orders are successfully created
77
+
78
+ Remember: Dispatch coordinators are busy - help them create orders FAST!"""
79
+
80
+ def is_available(self) -> bool:
81
+ return self.api_available
82
+
83
+ def get_status(self) -> str:
84
+ if self.api_available:
85
+ return f"βœ… Connected - Model: {self.model}"
86
+ return "⚠️ Not configured (add ANTHROPIC_API_KEY)"
87
+
88
+ def get_provider_name(self) -> str:
89
+ return "Claude (Anthropic)"
90
+
91
+ def get_model_name(self) -> str:
92
+ return self.model
93
+
94
+ def process_message(
95
+ self,
96
+ user_message: str,
97
+ conversation
98
+ ) -> Tuple[str, List[Dict]]:
99
+ """Process user message with Claude"""
100
+ if not self.api_available:
101
+ return self._handle_no_api(), []
102
+
103
+ # Add user message to history
104
+ conversation.add_message("user", user_message)
105
+
106
+ try:
107
+ # Make API call to Claude
108
+ response = self.client.messages.create(
109
+ model=self.model,
110
+ max_tokens=4096,
111
+ system=self.system_prompt,
112
+ tools=TOOLS_SCHEMA,
113
+ messages=conversation.get_history()
114
+ )
115
+
116
+ # Process response and handle tool calls
117
+ return self._process_response(response, conversation)
118
+
119
+ except AuthenticationError:
120
+ error_msg = "⚠️ Invalid API key. Please check your ANTHROPIC_API_KEY in .env file."
121
+ logger.error("Authentication error with Anthropic API")
122
+ return error_msg, []
123
+
124
+ except APIConnectionError:
125
+ error_msg = "⚠️ Cannot connect to Anthropic API. Please check your internet connection."
126
+ logger.error("Connection error with Anthropic API")
127
+ return error_msg, []
128
+
129
+ except APIError as e:
130
+ error_msg = f"⚠️ API error: {str(e)}"
131
+ logger.error(f"Anthropic API error: {e}")
132
+ return error_msg, []
133
+
134
+ except Exception as e:
135
+ error_msg = f"⚠️ Unexpected error: {str(e)}"
136
+ logger.error(f"Claude provider error: {e}")
137
+ return error_msg, []
138
+
139
+ def _process_response(
140
+ self,
141
+ response,
142
+ conversation
143
+ ) -> Tuple[str, List[Dict]]:
144
+ """Process Claude's response and handle tool calls"""
145
+ tool_calls_made = []
146
+
147
+ # Check if Claude wants to use tools
148
+ if response.stop_reason == "tool_use":
149
+ # Execute tools
150
+ tool_results = []
151
+
152
+ for content_block in response.content:
153
+ if content_block.type == "tool_use":
154
+ tool_name = content_block.name
155
+ tool_input = content_block.input
156
+
157
+ logger.info(f"Claude executing tool: {tool_name}")
158
+
159
+ # Execute the tool
160
+ tool_result = execute_tool(tool_name, tool_input)
161
+
162
+ # Track for transparency
163
+ tool_calls_made.append({
164
+ "tool": tool_name,
165
+ "input": tool_input,
166
+ "result": tool_result
167
+ })
168
+
169
+ conversation.add_tool_result(tool_name, tool_input, tool_result)
170
+
171
+ # Prepare result for Claude
172
+ tool_results.append({
173
+ "type": "tool_result",
174
+ "tool_use_id": content_block.id,
175
+ "content": str(tool_result)
176
+ })
177
+
178
+ # Add assistant's tool use to history
179
+ conversation.add_message("assistant", response.content)
180
+
181
+ # Add tool results to history
182
+ conversation.add_message("user", tool_results)
183
+
184
+ # Continue conversation with tool results
185
+ followup_response = self.client.messages.create(
186
+ model=self.model,
187
+ max_tokens=4096,
188
+ system=self.system_prompt,
189
+ tools=TOOLS_SCHEMA,
190
+ messages=conversation.get_history()
191
+ )
192
+
193
+ # Extract final text response
194
+ final_text = self._extract_text_response(followup_response)
195
+ conversation.add_message("assistant", final_text)
196
+
197
+ return final_text, tool_calls_made
198
+
199
+ else:
200
+ # No tool use, just text response
201
+ text_response = self._extract_text_response(response)
202
+ conversation.add_message("assistant", text_response)
203
+ return text_response, tool_calls_made
204
+
205
+ def _extract_text_response(self, response) -> str:
206
+ """Extract text content from Claude's response"""
207
+ text_parts = []
208
+ for block in response.content:
209
+ if hasattr(block, 'text'):
210
+ text_parts.append(block.text)
211
+ elif block.type == "text":
212
+ text_parts.append(block.text if hasattr(block, 'text') else str(block))
213
+
214
+ return "\n".join(text_parts) if text_parts else "I apologize, but I couldn't generate a response."
215
+
216
+ def _handle_no_api(self) -> str:
217
+ """Return error message when API is not available"""
218
+ return """⚠️ **Claude API requires Anthropic API key**
219
+
220
+ To use Claude:
221
+
222
+ 1. Get an API key from: https://console.anthropic.com/
223
+ - Sign up for free ($5 credit available)
224
+ - Or use hackathon credits
225
+
226
+ 2. Add to your `.env` file:
227
+ ```
228
+ ANTHROPIC_API_KEY=sk-ant-your-key-here
229
+ ```
230
+
231
+ 3. Restart the application
232
+
233
+ **Alternative:** Switch to Gemini by setting `AI_PROVIDER=gemini` in .env
234
+ """
235
+
236
+ def get_welcome_message(self) -> str:
237
+ if not self.api_available:
238
+ return self._handle_no_api()
239
+
240
+ return """πŸ‘‹ Hello! I'm your AI dispatch assistant powered by **Claude Sonnet 3.5**.
241
+
242
+ I can help you create delivery orders quickly and efficiently!
243
+
244
+ ---
245
+
246
+ πŸ“‹ **To Create an Order, Provide:**
247
+
248
+ **Required:**
249
+ β€’ Customer Name
250
+ β€’ Delivery Address
251
+ β€’ Contact (Phone OR Email)
252
+
253
+ **Optional:**
254
+ β€’ Delivery Deadline (default: 6 hours)
255
+ β€’ Priority: standard/express/urgent (default: standard)
256
+ β€’ Special Instructions
257
+ β€’ Package Weight in kg (default: 5.0)
258
+
259
+ ---
260
+
261
+ **Quick Start Examples:**
262
+
263
+ βœ… *Complete:* "Create order for John Doe, 123 Main St San Francisco CA, phone 555-1234, deliver by 5 PM"
264
+
265
+ βœ… *Partial:* "I need a delivery for Sarah" *(I'll ask for missing details)*
266
+
267
+ βœ… *Natural:* "Urgent package to john@email.com at 456 Market Street"
268
+
269
+ ---
270
+
271
+ What would you like to do?"""
chat/providers/gemini_provider.py ADDED
@@ -0,0 +1,551 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Google Gemini provider for FleetMind chat
3
+ """
4
+
5
+ import os
6
+ import logging
7
+ from typing import Tuple, List, Dict
8
+ import google.generativeai as genai
9
+ from google.generativeai.types import HarmCategory, HarmBlockThreshold
10
+
11
+ from chat.providers.base_provider import AIProvider
12
+ from chat.tools import execute_tool
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class GeminiProvider(AIProvider):
18
+ """Google Gemini AI provider"""
19
+
20
+ def __init__(self):
21
+ self.api_key = os.getenv("GOOGLE_API_KEY", "")
22
+ self.api_available = bool(self.api_key and not self.api_key.startswith("your_"))
23
+ self.model_name = "gemini-2.0-flash"
24
+ self.model = None
25
+ self._initialized = False
26
+
27
+ if not self.api_available:
28
+ logger.warning("GeminiProvider: GOOGLE_API_KEY not configured")
29
+ else:
30
+ logger.info("GeminiProvider: Ready (will initialize on first use)")
31
+
32
+ def _get_system_prompt(self) -> str:
33
+ """Get the system prompt for Gemini"""
34
+ return """You are an AI assistant for FleetMind, a delivery dispatch system.
35
+
36
+ **🚨 CRITICAL RULES - READ CAREFULLY:**
37
+
38
+ 1. **NEVER return text in the middle of tool calls**
39
+ - If you need to call multiple tools, call them ALL in sequence
40
+ - Only return text AFTER all tools are complete
41
+
42
+ 2. **Order Creation MUST be a single automated flow:**
43
+ - Step 1: Call geocode_address (get coordinates)
44
+ - Step 2: IMMEDIATELY call create_order (save to database)
45
+ - Step 3: ONLY THEN return success message
46
+ - DO NOT stop between Step 1 and Step 2
47
+ - DO NOT say "Now creating order..." - just DO it!
48
+
49
+ 3. **Driver Creation is a SINGLE tool call:**
50
+ - When user wants to create a driver, call create_driver immediately
51
+ - NO geocoding needed for drivers
52
+ - Just call create_driver β†’ confirm
53
+
54
+ 4. **If user provides required info, START IMMEDIATELY:**
55
+ - For Orders: Customer name, address, contact (phone OR email)
56
+ - For Drivers: Driver name (phone/email optional)
57
+ - If all present β†’ execute β†’ confirm
58
+ - If missing β†’ ask ONCE for all missing fields
59
+
60
+ **Example of CORRECT behavior:**
61
+
62
+ ORDER:
63
+ User: "Create order for John Doe, 123 Main St SF, phone 555-1234"
64
+ You: [geocode_address] β†’ [create_order] β†’ "βœ… Order ORD-123 created!"
65
+ (ALL in one response, no intermediate text)
66
+
67
+ DRIVER:
68
+ User: "Add new driver Mike Johnson, phone 555-0101, drives a van"
69
+ You: [create_driver] β†’ "βœ… Driver DRV-123 (Mike Johnson) added to fleet!"
70
+ (Single tool call, immediate response)
71
+
72
+ **Example of WRONG behavior (DO NOT DO THIS):**
73
+ User: "Create order for John Doe..."
74
+ You: [geocode_address] β†’ "OK geocoded, now creating..." ❌ WRONG!
75
+
76
+ **Available Tools:**
77
+ - geocode_address: Convert address to GPS coordinates
78
+ - create_order: Create customer delivery order (REQUIRES geocoded address)
79
+ - create_driver: Add new driver/delivery man to fleet
80
+
81
+ **Order Fields:**
82
+ Required: customer_name, delivery_address, contact
83
+ Optional: time_window_end, priority (standard/express/urgent), special_instructions, weight_kg
84
+
85
+ **Driver Fields:**
86
+ Required: name
87
+ Optional: phone, email, vehicle_type (van/truck/car/motorcycle), vehicle_plate, capacity_kg, capacity_m3, skills (list), status (active/busy/offline)
88
+
89
+ **Your goal:** Execute tasks in ONE smooth automated flow. No stopping, no intermediate messages!"""
90
+
91
+ def _get_gemini_tools(self) -> list:
92
+ """Convert tool schemas to Gemini function calling format"""
93
+ # Gemini expects tools wrapped in function_declarations
94
+ return [
95
+ genai.protos.Tool(
96
+ function_declarations=[
97
+ genai.protos.FunctionDeclaration(
98
+ name="geocode_address",
99
+ description="Convert a delivery address to GPS coordinates and validate the address format. Use this before creating an order to ensure the address is valid.",
100
+ parameters=genai.protos.Schema(
101
+ type=genai.protos.Type.OBJECT,
102
+ properties={
103
+ "address": genai.protos.Schema(
104
+ type=genai.protos.Type.STRING,
105
+ description="The full delivery address to geocode (e.g., '123 Main St, San Francisco, CA')"
106
+ )
107
+ },
108
+ required=["address"]
109
+ )
110
+ ),
111
+ genai.protos.FunctionDeclaration(
112
+ name="create_order",
113
+ description="Create a new delivery order in the database. Only call this after geocoding the address successfully.",
114
+ parameters=genai.protos.Schema(
115
+ type=genai.protos.Type.OBJECT,
116
+ properties={
117
+ "customer_name": genai.protos.Schema(
118
+ type=genai.protos.Type.STRING,
119
+ description="Full name of the customer"
120
+ ),
121
+ "customer_phone": genai.protos.Schema(
122
+ type=genai.protos.Type.STRING,
123
+ description="Customer phone number (optional)"
124
+ ),
125
+ "customer_email": genai.protos.Schema(
126
+ type=genai.protos.Type.STRING,
127
+ description="Customer email address (optional)"
128
+ ),
129
+ "delivery_address": genai.protos.Schema(
130
+ type=genai.protos.Type.STRING,
131
+ description="Full delivery address"
132
+ ),
133
+ "delivery_lat": genai.protos.Schema(
134
+ type=genai.protos.Type.NUMBER,
135
+ description="Latitude from geocoding"
136
+ ),
137
+ "delivery_lng": genai.protos.Schema(
138
+ type=genai.protos.Type.NUMBER,
139
+ description="Longitude from geocoding"
140
+ ),
141
+ "time_window_end": genai.protos.Schema(
142
+ type=genai.protos.Type.STRING,
143
+ description="Delivery deadline in ISO format (e.g., '2025-11-13T17:00:00'). If not specified by user, default to 6 hours from now."
144
+ ),
145
+ "priority": genai.protos.Schema(
146
+ type=genai.protos.Type.STRING,
147
+ description="Delivery priority. Default to 'standard' unless user specifies urgent/express."
148
+ ),
149
+ "special_instructions": genai.protos.Schema(
150
+ type=genai.protos.Type.STRING,
151
+ description="Any special delivery instructions (optional)"
152
+ ),
153
+ "weight_kg": genai.protos.Schema(
154
+ type=genai.protos.Type.NUMBER,
155
+ description="Package weight in kilograms (optional, default to 5.0)"
156
+ )
157
+ },
158
+ required=["customer_name", "delivery_address", "delivery_lat", "delivery_lng"]
159
+ )
160
+ ),
161
+ genai.protos.FunctionDeclaration(
162
+ name="create_driver",
163
+ description="Create a new delivery driver/delivery man in the database. Use this to onboard new drivers to the fleet.",
164
+ parameters=genai.protos.Schema(
165
+ type=genai.protos.Type.OBJECT,
166
+ properties={
167
+ "name": genai.protos.Schema(
168
+ type=genai.protos.Type.STRING,
169
+ description="Full name of the driver"
170
+ ),
171
+ "phone": genai.protos.Schema(
172
+ type=genai.protos.Type.STRING,
173
+ description="Driver phone number"
174
+ ),
175
+ "email": genai.protos.Schema(
176
+ type=genai.protos.Type.STRING,
177
+ description="Driver email address (optional)"
178
+ ),
179
+ "vehicle_type": genai.protos.Schema(
180
+ type=genai.protos.Type.STRING,
181
+ description="Type of vehicle: van, truck, car, motorcycle (default: van)"
182
+ ),
183
+ "vehicle_plate": genai.protos.Schema(
184
+ type=genai.protos.Type.STRING,
185
+ description="Vehicle license plate number"
186
+ ),
187
+ "capacity_kg": genai.protos.Schema(
188
+ type=genai.protos.Type.NUMBER,
189
+ description="Vehicle cargo capacity in kilograms (default: 1000.0)"
190
+ ),
191
+ "capacity_m3": genai.protos.Schema(
192
+ type=genai.protos.Type.NUMBER,
193
+ description="Vehicle cargo volume in cubic meters (default: 12.0)"
194
+ ),
195
+ "skills": genai.protos.Schema(
196
+ type=genai.protos.Type.ARRAY,
197
+ description="List of driver skills/certifications: refrigerated, medical_certified, fragile_handler, overnight, express_delivery",
198
+ items=genai.protos.Schema(type=genai.protos.Type.STRING)
199
+ ),
200
+ "status": genai.protos.Schema(
201
+ type=genai.protos.Type.STRING,
202
+ description="Driver status: active, busy, offline, unavailable (default: active)"
203
+ )
204
+ },
205
+ required=["name"]
206
+ )
207
+ )
208
+ ]
209
+ )
210
+ ]
211
+
212
+ def _ensure_initialized(self):
213
+ """Lazy initialization - only create model when first needed"""
214
+ if self._initialized or not self.api_available:
215
+ return
216
+
217
+ try:
218
+ genai.configure(api_key=self.api_key)
219
+ self.model = genai.GenerativeModel(
220
+ model_name=self.model_name,
221
+ tools=self._get_gemini_tools(),
222
+ system_instruction=self._get_system_prompt()
223
+ )
224
+ self._initialized = True
225
+ logger.info(f"GeminiProvider: Model initialized ({self.model_name})")
226
+ except Exception as e:
227
+ logger.error(f"GeminiProvider: Failed to initialize: {e}")
228
+ self.api_available = False
229
+ self.model = None
230
+
231
+ def is_available(self) -> bool:
232
+ return self.api_available
233
+
234
+ def get_status(self) -> str:
235
+ if self.api_available:
236
+ return f"βœ… Connected - Model: {self.model_name}"
237
+ return "⚠️ Not configured (add GOOGLE_API_KEY)"
238
+
239
+ def get_provider_name(self) -> str:
240
+ return "Gemini (Google)"
241
+
242
+ def get_model_name(self) -> str:
243
+ return self.model_name if self.api_available else "gemini-2.0-flash"
244
+
245
+ def process_message(
246
+ self,
247
+ user_message: str,
248
+ conversation
249
+ ) -> Tuple[str, List[Dict]]:
250
+ """Process user message with Gemini"""
251
+ if not self.api_available:
252
+ return self._handle_no_api(), []
253
+
254
+ # Lazy initialization on first use
255
+ self._ensure_initialized()
256
+
257
+ if not self._initialized:
258
+ return "⚠️ Failed to initialize Gemini model. Please check your API key and try again.", []
259
+
260
+ try:
261
+ # Build conversation history for Gemini
262
+ chat = self.model.start_chat(history=self._convert_history(conversation))
263
+
264
+ # Send message and get response
265
+ response = chat.send_message(
266
+ user_message,
267
+ safety_settings={
268
+ HarmCategory.HARM_CATEGORY_HATE_SPEECH: HarmBlockThreshold.BLOCK_NONE,
269
+ HarmCategory.HARM_CATEGORY_HARASSMENT: HarmBlockThreshold.BLOCK_NONE,
270
+ HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT: HarmBlockThreshold.BLOCK_NONE,
271
+ HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT: HarmBlockThreshold.BLOCK_NONE,
272
+ }
273
+ )
274
+
275
+ # Add user message to conversation
276
+ conversation.add_message("user", user_message)
277
+
278
+ # Process response and handle function calls
279
+ return self._process_response(response, conversation, chat)
280
+
281
+ except Exception as e:
282
+ error_msg = f"⚠️ Gemini API error: {str(e)}"
283
+ logger.error(f"Gemini provider error: {e}")
284
+ return error_msg, []
285
+
286
+ def _convert_history(self, conversation) -> list:
287
+ """Convert conversation history to Gemini format"""
288
+ history = []
289
+ # Get all messages from conversation (history is built before adding current message)
290
+ for msg in conversation.get_history():
291
+ role = "user" if msg["role"] == "user" else "model"
292
+ history.append({
293
+ "role": role,
294
+ "parts": [{"text": str(msg["content"])}]
295
+ })
296
+ return history
297
+
298
+ def _process_response(
299
+ self,
300
+ response,
301
+ conversation,
302
+ chat
303
+ ) -> Tuple[str, List[Dict]]:
304
+ """Process Gemini's response and handle function calls"""
305
+ tool_calls_made = []
306
+
307
+ # Check if Gemini wants to call functions
308
+ try:
309
+ # Check ALL parts for function calls (not just first)
310
+ has_function_call = False
311
+ parts = response.candidates[0].content.parts
312
+ logger.info(f"Processing response with {len(parts)} part(s)")
313
+
314
+ for part in parts:
315
+ if hasattr(part, 'function_call'):
316
+ fc = part.function_call
317
+ # More robust check
318
+ if fc is not None:
319
+ try:
320
+ if hasattr(fc, 'name') and fc.name:
321
+ has_function_call = True
322
+ logger.info(f"Detected function call: {fc.name}")
323
+ break
324
+ except Exception as e:
325
+ logger.warning(f"Error checking function call: {e}")
326
+
327
+ if has_function_call:
328
+ # Handle function calls (potentially multiple in sequence)
329
+ current_response = response
330
+ max_iterations = 10 # Allow more iterations for complex tasks
331
+
332
+ for iteration in range(max_iterations):
333
+ # Check if current response has a function call
334
+ try:
335
+ parts = current_response.candidates[0].content.parts
336
+ logger.info(f"Iteration {iteration + 1}: Response has {len(parts)} part(s)")
337
+ except (IndexError, AttributeError) as e:
338
+ logger.error(f"Cannot access response parts: {e}")
339
+ break
340
+
341
+ # Check ALL parts for function calls (some responses have text + function_call)
342
+ has_fc = False
343
+ fc_part = None
344
+
345
+ for idx, part in enumerate(parts):
346
+ if hasattr(part, 'function_call'):
347
+ fc = part.function_call
348
+ if fc and hasattr(fc, 'name') and fc.name:
349
+ has_fc = True
350
+ fc_part = part
351
+ logger.info(f"Iteration {iteration + 1}: Found function_call in part {idx}: {fc.name}")
352
+ break
353
+
354
+ # Also check if there's text (indicates Gemini wants to respond instead of continuing)
355
+ if hasattr(part, 'text') and part.text:
356
+ logger.warning(f"Iteration {iteration + 1}: Part {idx} has text: {part.text[:100]}")
357
+
358
+ if not has_fc:
359
+ # No more function calls, break and extract text
360
+ logger.info(f"No more function calls after iteration {iteration + 1}")
361
+ break
362
+
363
+ # Use the part with function_call
364
+ first_part = fc_part
365
+
366
+ # Extract function call details
367
+ function_call = first_part.function_call
368
+ function_name = function_call.name
369
+ function_args = dict(function_call.args) if function_call.args else {}
370
+
371
+ logger.info(f"Gemini executing function: {function_name} (iteration {iteration + 1})")
372
+
373
+ # Execute the tool
374
+ tool_result = execute_tool(function_name, function_args)
375
+
376
+ # Track for transparency
377
+ tool_calls_made.append({
378
+ "tool": function_name,
379
+ "input": function_args,
380
+ "result": tool_result
381
+ })
382
+
383
+ conversation.add_tool_result(function_name, function_args, tool_result)
384
+
385
+ # Send function result back to Gemini
386
+ try:
387
+ current_response = chat.send_message(
388
+ genai.protos.Content(
389
+ parts=[genai.protos.Part(
390
+ function_response=genai.protos.FunctionResponse(
391
+ name=function_name,
392
+ response={"result": tool_result}
393
+ )
394
+ )]
395
+ )
396
+ )
397
+ except Exception as e:
398
+ logger.error(f"Error sending function response: {e}")
399
+ break
400
+
401
+ # Now extract text from the final response
402
+ # NEVER use .text property directly - always check parts
403
+ final_text = ""
404
+ try:
405
+ parts = current_response.candidates[0].content.parts
406
+ logger.info(f"Extracting text from {len(parts)} parts")
407
+
408
+ for idx, part in enumerate(parts):
409
+ # Check if this part has a function call
410
+ if hasattr(part, 'function_call') and part.function_call:
411
+ fc = part.function_call
412
+ if hasattr(fc, 'name') and fc.name:
413
+ logger.warning(f"Part {idx} still has function call: {fc.name}. Skipping.")
414
+ continue
415
+
416
+ # Extract text from this part
417
+ if hasattr(part, 'text') and part.text:
418
+ logger.info(f"Part {idx} has text: {part.text[:50]}...")
419
+ final_text += part.text
420
+
421
+ except (AttributeError, IndexError) as e:
422
+ logger.error(f"Error extracting text from parts: {e}")
423
+
424
+ # Generate fallback message if still no text
425
+ if not final_text:
426
+ logger.warning("No text extracted from response, generating fallback")
427
+ if tool_calls_made:
428
+ # Create a summary of what was done
429
+ tool_names = [t["tool"] for t in tool_calls_made]
430
+ if "create_order" in tool_names:
431
+ # Check if order was created successfully
432
+ create_result = next((t["result"] for t in tool_calls_made if t["tool"] == "create_order"), {})
433
+ if create_result.get("success"):
434
+ order_id = create_result.get("order_id", "")
435
+ final_text = f"βœ… Order {order_id} created successfully!"
436
+ else:
437
+ final_text = "⚠️ There was an issue creating the order."
438
+ else:
439
+ final_text = f"βœ… Executed {len(tool_calls_made)} tool(s) successfully!"
440
+ else:
441
+ final_text = "βœ… Task completed!"
442
+
443
+ logger.info(f"Returning response: {final_text[:100]}")
444
+ conversation.add_message("assistant", final_text)
445
+ return final_text, tool_calls_made
446
+
447
+ else:
448
+ # No function call detected, extract text from parts
449
+ text_response = ""
450
+ try:
451
+ parts = response.candidates[0].content.parts
452
+ logger.info(f"Extracting text from {len(parts)} parts (no function call)")
453
+
454
+ for idx, part in enumerate(parts):
455
+ # Double-check no function call in this part
456
+ if hasattr(part, 'function_call') and part.function_call:
457
+ fc = part.function_call
458
+ if hasattr(fc, 'name') and fc.name:
459
+ logger.error(f"Part {idx} has function call {fc.name} but was not detected earlier!")
460
+ # We missed a function call - handle it now
461
+ logger.info("Re-processing response with function call handling")
462
+ return self._process_response(response, conversation, chat)
463
+
464
+ # Extract text
465
+ if hasattr(part, 'text') and part.text:
466
+ logger.info(f"Part {idx} has text: {part.text[:50]}...")
467
+ text_response += part.text
468
+
469
+ except (ValueError, AttributeError, IndexError) as e:
470
+ logger.error(f"Error extracting text from response: {e}")
471
+
472
+ # Fallback if no text extracted
473
+ if not text_response:
474
+ logger.warning("No text in response, using fallback")
475
+ text_response = "I'm ready to help! What would you like me to do?"
476
+
477
+ conversation.add_message("assistant", text_response)
478
+ return text_response, tool_calls_made
479
+
480
+ except Exception as e:
481
+ logger.error(f"Error processing Gemini response: {e}")
482
+ error_msg = f"⚠️ Error processing response: {str(e)}"
483
+ conversation.add_message("assistant", error_msg)
484
+ return error_msg, tool_calls_made
485
+
486
+ def _handle_no_api(self) -> str:
487
+ """Return error message when API is not available"""
488
+ return """⚠️ **Gemini API requires Google API key**
489
+
490
+ To use Gemini:
491
+
492
+ 1. Get an API key from: https://aistudio.google.com/app/apikey
493
+ - Free tier: 15 requests/min, 1500/day
494
+ - Or use hackathon credits
495
+
496
+ 2. Add to your `.env` file:
497
+ ```
498
+ GOOGLE_API_KEY=your-gemini-key-here
499
+ ```
500
+
501
+ 3. Restart the application
502
+
503
+ **Alternative:** Switch to Claude by setting `AI_PROVIDER=anthropic` in .env
504
+ """
505
+
506
+ def get_welcome_message(self) -> str:
507
+ if not self.api_available:
508
+ return self._handle_no_api()
509
+
510
+ # Initialize on first use (welcome message)
511
+ self._ensure_initialized()
512
+
513
+ return """πŸ‘‹ Hello! I'm your AI dispatch assistant powered by **Google Gemini 2.0 Flash**.
514
+
515
+ I can help you manage your delivery fleet!
516
+
517
+ ---
518
+
519
+ πŸ“‹ **What I Can Do:**
520
+
521
+ **1. Create Delivery Orders:**
522
+ β€’ Customer Name
523
+ β€’ Delivery Address
524
+ β€’ Contact (Phone OR Email)
525
+ β€’ Optional: Deadline, Priority, Special Instructions
526
+
527
+ **2. Add New Drivers:**
528
+ β€’ Driver Name (required)
529
+ β€’ Optional: Phone, Email, Vehicle Type, License Plate, Skills
530
+
531
+ ---
532
+
533
+ **Examples - Just Type Naturally:**
534
+
535
+ πŸ“¦ **Orders:**
536
+ πŸ’¬ "Create order for John Doe, 123 Main St San Francisco CA, phone 555-1234, deliver by 5 PM"
537
+ πŸ’¬ "New urgent delivery to Sarah at 456 Oak Ave NYC, email sarah@email.com"
538
+
539
+ 🚚 **Drivers:**
540
+ πŸ’¬ "Add new driver Tom Wilson, phone 555-0101, drives a van, plate ABC-123"
541
+ πŸ’¬ "Create driver Sarah Martinez with refrigerated truck, phone 555-0202"
542
+ πŸ’¬ "New driver: Mike Chen, email mike@fleet.com, motorcycle delivery"
543
+
544
+ ---
545
+
546
+ πŸš€ **I'll automatically:**
547
+ β€’ Geocode addresses for orders
548
+ β€’ Generate unique IDs
549
+ β€’ Save everything to the database
550
+
551
+ What would you like to do?"""
chat/route_optimizer.py DELETED
@@ -1,196 +0,0 @@
1
- """
2
- Intelligent Route Optimizer for FleetMind
3
- Combines traffic, weather, and vehicle type for optimal routing decisions
4
- """
5
-
6
- import logging
7
- from typing import Dict, List, Optional
8
- from chat.tools import handle_calculate_route, geocoding_service
9
- from chat.weather import weather_service
10
-
11
- logger = logging.getLogger(__name__)
12
-
13
-
14
- def calculate_intelligent_route(
15
- origin: str,
16
- destination: str,
17
- vehicle_type: str = "car",
18
- consider_weather: bool = True,
19
- consider_traffic: bool = True
20
- ) -> Dict:
21
- """
22
- Calculate optimal route considering traffic, weather, and vehicle type
23
-
24
- Args:
25
- origin: Starting location (address or coordinates)
26
- destination: Ending location (address or coordinates)
27
- vehicle_type: Type of vehicle (motorcycle, car, van, truck)
28
- consider_weather: Whether to factor in weather conditions
29
- consider_traffic: Whether to factor in traffic conditions
30
-
31
- Returns:
32
- Comprehensive routing result with recommendations and warnings
33
- """
34
- logger.info(f"Intelligent routing: {origin} β†’ {destination} (vehicle: {vehicle_type})")
35
-
36
- # Step 1: Calculate base route with traffic data
37
- route_result = handle_calculate_route({
38
- "origin": origin,
39
- "destination": destination,
40
- "vehicle_type": vehicle_type,
41
- "alternatives": True # Get alternative routes
42
- })
43
-
44
- if not route_result.get("success"):
45
- return route_result # Return error
46
-
47
- # Step 2: Get weather data for the destination area
48
- weather_data = None
49
- weather_impact = None
50
-
51
- if consider_weather:
52
- try:
53
- # Geocode destination to get coordinates
54
- dest_geocoded = geocoding_service.geocode(destination)
55
- dest_lat = dest_geocoded["lat"]
56
- dest_lng = dest_geocoded["lng"]
57
-
58
- # Get current weather
59
- weather_data = weather_service.get_current_weather(dest_lat, dest_lng)
60
-
61
- # Assess weather impact for this vehicle type
62
- weather_impact = weather_service.assess_weather_impact(weather_data, vehicle_type)
63
-
64
- logger.info(f"Weather impact: {weather_impact['severity']} (multiplier: {weather_impact['speed_multiplier']}x)")
65
- except Exception as e:
66
- logger.warning(f"Weather data unavailable: {e}")
67
- consider_weather = False
68
-
69
- # Step 3: Calculate adjusted duration
70
- base_duration = route_result["duration"]["seconds"]
71
- traffic_duration = route_result["duration_in_traffic"]["seconds"]
72
-
73
- # Start with traffic-aware duration
74
- adjusted_duration = traffic_duration
75
-
76
- # Apply weather adjustments if available
77
- if consider_weather and weather_impact:
78
- adjusted_duration = int(adjusted_duration * weather_impact["speed_multiplier"])
79
-
80
- # Calculate delay percentages
81
- traffic_delay_percent = 0
82
- weather_delay_percent = 0
83
-
84
- if consider_traffic and traffic_duration > base_duration:
85
- traffic_delay_percent = int(((traffic_duration - base_duration) / base_duration) * 100)
86
-
87
- if consider_weather and weather_impact and weather_impact["speed_multiplier"] > 1.0:
88
- weather_delay_percent = int(((weather_impact["speed_multiplier"] - 1.0) * 100))
89
-
90
- total_delay_percent = int(((adjusted_duration - base_duration) / base_duration) * 100) if base_duration > 0 else 0
91
-
92
- # Step 4: Generate traffic status
93
- traffic_status = "unknown"
94
- if consider_traffic:
95
- if traffic_delay_percent == 0:
96
- traffic_status = "clear"
97
- elif traffic_delay_percent < 15:
98
- traffic_status = "light"
99
- elif traffic_delay_percent < 30:
100
- traffic_status = "moderate"
101
- elif traffic_delay_percent < 50:
102
- traffic_status = "heavy"
103
- else:
104
- traffic_status = "severe"
105
-
106
- # Step 5: Generate recommendations and warnings
107
- recommendations = []
108
- warnings = []
109
-
110
- # Traffic recommendations
111
- if consider_traffic:
112
- if traffic_delay_percent > 30:
113
- recommendations.append(f"🚦 Heavy traffic: {traffic_delay_percent}% delay - consider alternate route or timing")
114
- elif traffic_delay_percent > 15:
115
- recommendations.append(f"🚦 Moderate traffic: {traffic_delay_percent}% delay expected")
116
-
117
- # Weather recommendations
118
- if consider_weather and weather_impact:
119
- if weather_impact["warnings"]:
120
- warnings.extend(weather_impact["warnings"])
121
-
122
- if weather_impact["recommend_delay"]:
123
- recommendations.append("⚠️ SEVERE WEATHER: Consider delaying trip until conditions improve")
124
-
125
- if vehicle_type == "motorcycle" and not weather_impact["safe_for_motorcycle"]:
126
- warnings.append("🏍️ WARNING: Current weather not safe for motorcycle - consider alternative vehicle")
127
-
128
- # Vehicle-specific recommendations
129
- if vehicle_type == "motorcycle":
130
- if traffic_delay_percent > 40:
131
- recommendations.append("🏍️ TIP: Motorcycles can navigate heavy traffic more efficiently")
132
-
133
- # Format durations
134
- def format_duration(seconds):
135
- hours = seconds // 3600
136
- minutes = (seconds % 3600) // 60
137
- if hours > 0:
138
- return f"{hours}h {minutes}m"
139
- return f"{minutes}m"
140
-
141
- # Step 6: Build comprehensive response
142
- response = {
143
- "success": True,
144
- "route": {
145
- "origin": route_result["origin"],
146
- "destination": route_result["destination"],
147
- "distance": route_result["distance"],
148
- "vehicle_type": vehicle_type,
149
- "route_summary": route_result["route_summary"],
150
- "confidence": route_result["confidence"]
151
- },
152
- "timing": {
153
- "base_duration": {
154
- "seconds": base_duration,
155
- "text": format_duration(base_duration)
156
- },
157
- "with_traffic": {
158
- "seconds": traffic_duration,
159
- "text": format_duration(traffic_duration)
160
- },
161
- "adjusted_duration": {
162
- "seconds": adjusted_duration,
163
- "text": format_duration(adjusted_duration)
164
- },
165
- "traffic_delay_percent": traffic_delay_percent,
166
- "weather_delay_percent": weather_delay_percent,
167
- "total_delay_percent": total_delay_percent
168
- },
169
- "conditions": {
170
- "traffic_status": traffic_status,
171
- "traffic_considered": consider_traffic,
172
- "weather_considered": consider_weather
173
- },
174
- "recommendations": recommendations,
175
- "warnings": warnings
176
- }
177
-
178
- # Add weather data if available
179
- if weather_data:
180
- response["weather"] = {
181
- "conditions": weather_data["conditions"],
182
- "description": weather_data["description"],
183
- "temperature_c": round(weather_data["temperature_c"], 1),
184
- "precipitation_mm": round(weather_data["precipitation_mm"], 1),
185
- "visibility_m": weather_data["visibility_m"],
186
- "impact_severity": weather_impact["severity"] if weather_impact else "none"
187
- }
188
-
189
- # Add alternative routes if available
190
- if route_result.get("alternatives"):
191
- response["alternatives"] = route_result["alternatives"]
192
- response["alternatives_count"] = len(route_result["alternatives"])
193
-
194
- logger.info(f"Intelligent route calculated: {format_duration(adjusted_duration)} (base: {format_duration(base_duration)})")
195
-
196
- return response
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
chat/tools.py CHANGED
The diff for this file is too large to render. See raw diff
 
chat/weather.py DELETED
@@ -1,248 +0,0 @@
1
- """
2
- Weather service for FleetMind
3
- Provides weather data for intelligent routing decisions
4
- """
5
-
6
- import os
7
- import logging
8
- import requests
9
- from typing import Dict, Optional
10
- from datetime import datetime
11
-
12
- logger = logging.getLogger(__name__)
13
-
14
-
15
- class WeatherService:
16
- """Handle weather data fetching with OpenWeatherMap API and mock fallback"""
17
-
18
- def __init__(self):
19
- self.api_key = os.getenv("OPENWEATHERMAP_API_KEY", "")
20
- self.use_mock = not self.api_key or self.api_key.startswith("your_")
21
- self.base_url = "https://api.openweathermap.org/data/2.5/weather"
22
-
23
- if self.use_mock:
24
- logger.info("Weather Service: Using mock (OPENWEATHERMAP_API_KEY not configured)")
25
- else:
26
- logger.info("Weather Service: Using OpenWeatherMap API")
27
-
28
- def get_current_weather(self, lat: float, lng: float) -> Dict:
29
- """
30
- Get current weather conditions at specified coordinates
31
-
32
- Args:
33
- lat: Latitude
34
- lng: Longitude
35
-
36
- Returns:
37
- Dict with weather data: temp, conditions, precipitation, visibility, wind
38
- """
39
- if self.use_mock:
40
- return self._get_weather_mock(lat, lng)
41
- else:
42
- try:
43
- return self._get_weather_openweathermap(lat, lng)
44
- except Exception as e:
45
- logger.error(f"OpenWeatherMap API failed: {e}, falling back to mock")
46
- return self._get_weather_mock(lat, lng)
47
-
48
- def _get_weather_openweathermap(self, lat: float, lng: float) -> Dict:
49
- """Fetch weather from OpenWeatherMap API"""
50
- try:
51
- params = {
52
- "lat": lat,
53
- "lon": lng,
54
- "appid": self.api_key,
55
- "units": "metric" # Celsius, km/h
56
- }
57
-
58
- response = requests.get(self.base_url, params=params, timeout=5)
59
- response.raise_for_status()
60
- data = response.json()
61
-
62
- # Extract weather information
63
- main = data.get("main", {})
64
- weather = data.get("weather", [{}])[0]
65
- wind = data.get("wind", {})
66
- rain = data.get("rain", {})
67
- snow = data.get("snow", {})
68
- visibility = data.get("visibility", 10000) # Default 10km
69
-
70
- # Calculate precipitation (rain + snow in last hour)
71
- precipitation_mm = rain.get("1h", 0) + snow.get("1h", 0)
72
-
73
- weather_data = {
74
- "temperature_c": main.get("temp", 20),
75
- "feels_like_c": main.get("feels_like", 20),
76
- "humidity_percent": main.get("humidity", 50),
77
- "conditions": weather.get("main", "Clear"),
78
- "description": weather.get("description", "clear sky"),
79
- "precipitation_mm": precipitation_mm,
80
- "visibility_m": visibility,
81
- "wind_speed_mps": wind.get("speed", 0),
82
- "wind_gust_mps": wind.get("gust", 0),
83
- "timestamp": datetime.now().isoformat(),
84
- "source": "OpenWeatherMap API"
85
- }
86
-
87
- logger.info(f"Weather fetched: {weather_data['conditions']}, {weather_data['temperature_c']}Β°C")
88
- return weather_data
89
-
90
- except Exception as e:
91
- logger.error(f"OpenWeatherMap API error: {e}")
92
- raise
93
-
94
- def _get_weather_mock(self, lat: float, lng: float) -> Dict:
95
- """Mock weather data for testing"""
96
- # Generate pseudo-random but realistic weather based on coordinates
97
- import random
98
- random.seed(int(lat * 1000) + int(lng * 1000))
99
-
100
- conditions_options = ["Clear", "Clouds", "Rain", "Drizzle", "Fog"]
101
- weights = [0.5, 0.3, 0.15, 0.03, 0.02] # Mostly clear/cloudy
102
- conditions = random.choices(conditions_options, weights=weights)[0]
103
-
104
- if conditions == "Clear":
105
- precipitation_mm = 0
106
- visibility_m = 10000
107
- description = "clear sky"
108
- elif conditions == "Clouds":
109
- precipitation_mm = 0
110
- visibility_m = 8000
111
- description = "scattered clouds"
112
- elif conditions == "Rain":
113
- precipitation_mm = random.uniform(2, 10)
114
- visibility_m = random.randint(5000, 8000)
115
- description = "moderate rain"
116
- elif conditions == "Drizzle":
117
- precipitation_mm = random.uniform(0.5, 2)
118
- visibility_m = random.randint(6000, 9000)
119
- description = "light rain"
120
- else: # Fog
121
- precipitation_mm = 0
122
- visibility_m = random.randint(500, 2000)
123
- description = "foggy"
124
-
125
- weather_data = {
126
- "temperature_c": random.uniform(10, 25),
127
- "feels_like_c": random.uniform(8, 23),
128
- "humidity_percent": random.randint(40, 80),
129
- "conditions": conditions,
130
- "description": description,
131
- "precipitation_mm": precipitation_mm,
132
- "visibility_m": visibility_m,
133
- "wind_speed_mps": random.uniform(0, 8),
134
- "wind_gust_mps": random.uniform(0, 12),
135
- "timestamp": datetime.now().isoformat(),
136
- "source": "Mock (testing)"
137
- }
138
-
139
- logger.info(f"Mock weather: {conditions}, {weather_data['temperature_c']:.1f}Β°C")
140
- return weather_data
141
-
142
- def assess_weather_impact(self, weather_data: Dict, vehicle_type: str = "car") -> Dict:
143
- """
144
- Assess how weather affects routing for a given vehicle type
145
-
146
- Args:
147
- weather_data: Weather data from get_current_weather()
148
- vehicle_type: Type of vehicle (car, van, truck, motorcycle)
149
-
150
- Returns:
151
- Dict with impact assessment and warnings
152
- """
153
- precipitation = weather_data.get("precipitation_mm", 0)
154
- visibility = weather_data.get("visibility_m", 10000)
155
- wind_speed = weather_data.get("wind_speed_mps", 0)
156
- conditions = weather_data.get("conditions", "Clear")
157
-
158
- impact_score = 0 # 0 = no impact, 1 = severe impact
159
- speed_multiplier = 1.0 # 1.0 = no change, 1.5 = 50% slower
160
- warnings = []
161
- severity = "none"
162
-
163
- # Precipitation impact
164
- if precipitation > 10: # Heavy rain (>10mm/h)
165
- impact_score += 0.6
166
- speed_multiplier *= 1.4
167
- warnings.append("⚠️ Heavy rain - significantly reduced speeds")
168
- severity = "severe"
169
-
170
- if vehicle_type == "motorcycle":
171
- speed_multiplier *= 1.2 # Additional 20% slower for motorcycles
172
- warnings.append("🏍️ DANGER: Motorcycle in heavy rain - consider rescheduling")
173
-
174
- elif precipitation > 5: # Moderate rain (5-10mm/h)
175
- impact_score += 0.3
176
- speed_multiplier *= 1.2
177
- warnings.append("🌧️ Moderate rain - reduced speeds")
178
- severity = "moderate"
179
-
180
- if vehicle_type == "motorcycle":
181
- speed_multiplier *= 1.15
182
- warnings.append("🏍️ Caution: Wet roads for motorcycle")
183
-
184
- elif precipitation > 0: # Light rain
185
- impact_score += 0.1
186
- speed_multiplier *= 1.1
187
- if vehicle_type == "motorcycle":
188
- warnings.append("🏍️ Light rain - exercise caution")
189
- severity = "minor"
190
-
191
- # Visibility impact
192
- if visibility < 1000: # Poor visibility (<1km)
193
- impact_score += 0.5
194
- speed_multiplier *= 1.3
195
- warnings.append("🌫️ Poor visibility - drive carefully")
196
- if severity == "none":
197
- severity = "moderate"
198
-
199
- elif visibility < 5000: # Reduced visibility
200
- impact_score += 0.2
201
- speed_multiplier *= 1.1
202
- if severity == "none":
203
- severity = "minor"
204
-
205
- # Wind impact (mainly for motorcycles and high-profile vehicles)
206
- if wind_speed > 15: # Strong wind (>54 km/h)
207
- if vehicle_type in ["motorcycle", "truck"]:
208
- impact_score += 0.3
209
- speed_multiplier *= 1.15
210
- warnings.append(f"πŸ’¨ Strong winds - affects {vehicle_type}")
211
- if severity == "none":
212
- severity = "moderate"
213
-
214
- # Extreme conditions
215
- if conditions == "Thunderstorm":
216
- impact_score += 0.8
217
- speed_multiplier *= 1.6
218
- warnings.append("β›ˆοΈ SEVERE: Thunderstorm - consider delaying trip")
219
- severity = "severe"
220
-
221
- if conditions == "Snow":
222
- impact_score += 0.7
223
- speed_multiplier *= 1.5
224
- warnings.append("❄️ Snow conditions - significantly reduced speeds")
225
- severity = "severe"
226
-
227
- # Cap impact score at 1.0
228
- impact_score = min(impact_score, 1.0)
229
-
230
- return {
231
- "impact_score": round(impact_score, 2),
232
- "speed_multiplier": round(speed_multiplier, 2),
233
- "severity": severity,
234
- "warnings": warnings,
235
- "safe_for_motorcycle": precipitation < 5 and visibility > 3000 and wind_speed < 12,
236
- "recommend_delay": severity == "severe"
237
- }
238
-
239
- def get_status(self) -> str:
240
- """Get weather service status"""
241
- if self.use_mock:
242
- return "⚠️ Mock weather service (configure OPENWEATHERMAP_API_KEY)"
243
- else:
244
- return "βœ… OpenWeatherMap API connected"
245
-
246
-
247
- # Global weather service instance
248
- weather_service = WeatherService()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
database/api_keys.py DELETED
@@ -1,265 +0,0 @@
1
- """
2
- API Key Authentication System for FleetMind MCP Server
3
-
4
- Simple API key management for multi-tenant authentication without OAuth complexity.
5
- Works with Claude Desktop and mcp-remote today!
6
-
7
- Usage:
8
- 1. User generates API key via web interface or CLI
9
- 2. User adds API key to Claude Desktop config
10
- 3. MCP server validates key and returns user_id
11
- 4. Multi-tenant isolation works automatically
12
- """
13
-
14
- import os
15
- import secrets
16
- import hashlib
17
- from datetime import datetime
18
- from typing import Optional, Dict
19
- from database.connection import get_db_connection
20
-
21
- def create_api_keys_table():
22
- """Create api_keys table if it doesn't exist"""
23
- conn = get_db_connection()
24
- cursor = conn.cursor()
25
-
26
- cursor.execute("""
27
- CREATE TABLE IF NOT EXISTS api_keys (
28
- key_id SERIAL PRIMARY KEY,
29
- user_id VARCHAR(100) NOT NULL,
30
- email VARCHAR(255) NOT NULL,
31
- name VARCHAR(255),
32
- api_key_hash VARCHAR(64) NOT NULL UNIQUE,
33
- api_key_prefix VARCHAR(20) NOT NULL,
34
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
35
- last_used_at TIMESTAMP,
36
- is_active BOOLEAN DEFAULT true,
37
- UNIQUE(user_id)
38
- )
39
- """)
40
-
41
- cursor.execute("""
42
- CREATE INDEX IF NOT EXISTS idx_api_keys_hash
43
- ON api_keys(api_key_hash)
44
- """)
45
-
46
- cursor.execute("""
47
- CREATE INDEX IF NOT EXISTS idx_api_keys_user
48
- ON api_keys(user_id)
49
- """)
50
-
51
- conn.commit()
52
- cursor.close()
53
- conn.close()
54
-
55
- def generate_api_key(email: str, name: str = None) -> Dict[str, str]:
56
- """
57
- Generate a new API key for a user
58
-
59
- Args:
60
- email: User's email address (used as identifier)
61
- name: User's display name (optional)
62
-
63
- Returns:
64
- dict with api_key (show once!), user_id, email, name
65
- """
66
- # Generate secure random API key
67
- api_key = f"fm_{secrets.token_urlsafe(32)}" # fm_ prefix for FleetMind
68
-
69
- # Hash the API key for storage (never store plain text!)
70
- api_key_hash = hashlib.sha256(api_key.encode()).hexdigest()
71
-
72
- # Store prefix for display (first 12 chars)
73
- api_key_prefix = api_key[:12]
74
-
75
- # Generate user_id from email
76
- user_id = f"user_{hashlib.md5(email.encode()).hexdigest()[:12]}"
77
-
78
- conn = get_db_connection()
79
- cursor = conn.cursor()
80
-
81
- try:
82
- # Check if user already has a key
83
- cursor.execute("SELECT user_id FROM api_keys WHERE email = %s", (email,))
84
- existing = cursor.fetchone()
85
-
86
- if existing:
87
- cursor.close()
88
- conn.close()
89
- return {
90
- "success": False,
91
- "error": "User already has an API key. Revoke the old key first."
92
- }
93
-
94
- # Insert new API key
95
- cursor.execute("""
96
- INSERT INTO api_keys (user_id, email, name, api_key_hash, api_key_prefix)
97
- VALUES (%s, %s, %s, %s, %s)
98
- RETURNING user_id, email, name, created_at
99
- """, (user_id, email, name, api_key_hash, api_key_prefix))
100
-
101
- result = cursor.fetchone()
102
- conn.commit()
103
-
104
- if not result:
105
- raise Exception("Failed to insert API key")
106
-
107
- # Unpack the result tuple
108
- ret_user_id, ret_email, ret_name, ret_created_at = result
109
-
110
- return {
111
- "success": True,
112
- "api_key": api_key, # SHOW THIS ONCE! Never displayed again
113
- "user_id": ret_user_id,
114
- "email": ret_email,
115
- "name": ret_name or "FleetMind User",
116
- "created_at": str(ret_created_at) if ret_created_at else "",
117
- "message": "⚠️ IMPORTANT: Save this API key now! It won't be shown again."
118
- }
119
-
120
- except Exception as e:
121
- conn.rollback()
122
- import traceback
123
- error_details = traceback.format_exc()
124
- print(f"API Key Generation Error: {e}")
125
- print(f"Error details: {error_details}")
126
- return {
127
- "success": False,
128
- "error": f"Failed to generate API key: {str(e)}"
129
- }
130
- finally:
131
- cursor.close()
132
- conn.close()
133
-
134
- def verify_api_key(api_key: str) -> Optional[Dict[str, str]]:
135
- """
136
- Verify API key and return user info
137
-
138
- Args:
139
- api_key: The API key to verify
140
-
141
- Returns:
142
- User info dict if valid, None if invalid
143
- """
144
- if not api_key or not api_key.startswith("fm_"):
145
- return None
146
-
147
- # Hash the provided key
148
- api_key_hash = hashlib.sha256(api_key.encode()).hexdigest()
149
-
150
- conn = get_db_connection()
151
- cursor = conn.cursor()
152
-
153
- try:
154
- # Look up the key
155
- cursor.execute("""
156
- SELECT user_id, email, name, is_active
157
- FROM api_keys
158
- WHERE api_key_hash = %s
159
- """, (api_key_hash,))
160
-
161
- result = cursor.fetchone()
162
-
163
- if not result:
164
- return None
165
-
166
- # Access RealDictRow fields by key (not tuple unpacking!)
167
- user_id = result['user_id']
168
- email = result['email']
169
- name = result['name']
170
- is_active = result['is_active']
171
-
172
- if not is_active:
173
- return None
174
-
175
- # Update last_used_at
176
- cursor.execute("""
177
- UPDATE api_keys
178
- SET last_used_at = CURRENT_TIMESTAMP
179
- WHERE api_key_hash = %s
180
- """, (api_key_hash,))
181
- conn.commit()
182
-
183
- return {
184
- 'user_id': user_id,
185
- 'email': email,
186
- 'name': name or 'FleetMind User',
187
- 'scopes': ['orders:read', 'orders:write', 'drivers:read', 'drivers:write', 'assignments:manage']
188
- }
189
-
190
- except Exception as e:
191
- print(f"API key verification error: {e}")
192
- return None
193
- finally:
194
- cursor.close()
195
- conn.close()
196
-
197
- def list_api_keys() -> list:
198
- """List all API keys (without showing actual keys)"""
199
- conn = get_db_connection()
200
- cursor = conn.cursor()
201
-
202
- cursor.execute("""
203
- SELECT user_id, email, name, api_key_prefix, created_at, last_used_at, is_active
204
- FROM api_keys
205
- ORDER BY created_at DESC
206
- """)
207
-
208
- keys = []
209
- for row in cursor.fetchall():
210
- keys.append({
211
- 'user_id': row[0],
212
- 'email': row[1],
213
- 'name': row[2],
214
- 'key_preview': f"{row[3]}...",
215
- 'created_at': row[4].isoformat(),
216
- 'last_used_at': row[5].isoformat() if row[5] else None,
217
- 'is_active': row[6]
218
- })
219
-
220
- cursor.close()
221
- conn.close()
222
- return keys
223
-
224
- def revoke_api_key(email: str) -> Dict[str, any]:
225
- """Revoke (deactivate) an API key"""
226
- conn = get_db_connection()
227
- cursor = conn.cursor()
228
-
229
- try:
230
- cursor.execute("""
231
- UPDATE api_keys
232
- SET is_active = false
233
- WHERE email = %s
234
- RETURNING user_id, email
235
- """, (email,))
236
-
237
- result = cursor.fetchone()
238
- conn.commit()
239
-
240
- if result:
241
- return {
242
- "success": True,
243
- "message": f"API key revoked for {result[1]}"
244
- }
245
- else:
246
- return {
247
- "success": False,
248
- "error": "No API key found for this email"
249
- }
250
-
251
- except Exception as e:
252
- conn.rollback()
253
- return {
254
- "success": False,
255
- "error": f"Failed to revoke key: {str(e)}"
256
- }
257
- finally:
258
- cursor.close()
259
- conn.close()
260
-
261
- # Initialize table on import
262
- try:
263
- create_api_keys_table()
264
- except:
265
- pass # Table might already exist
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
database/connection.py CHANGED
@@ -45,8 +45,7 @@ def get_db_connection() -> psycopg2.extensions.connection:
45
  database=DB_CONFIG['database'],
46
  user=DB_CONFIG['user'],
47
  password=DB_CONFIG['password'],
48
- cursor_factory=psycopg2.extras.RealDictCursor,
49
- sslmode='prefer'
50
  )
51
 
52
  logger.info(f"Database connection established: {DB_CONFIG['database']}@{DB_CONFIG['host']}")
 
45
  database=DB_CONFIG['database'],
46
  user=DB_CONFIG['user'],
47
  password=DB_CONFIG['password'],
48
+ cursor_factory=psycopg2.extras.RealDictCursor
 
49
  )
50
 
51
  logger.info(f"Database connection established: {DB_CONFIG['database']}@{DB_CONFIG['host']}")
database/migrations/002_create_assignments_table.py DELETED
@@ -1,157 +0,0 @@
1
- """
2
- Migration 002: Create Assignments Table
3
- Creates the assignments table for managing order-driver assignments with route data
4
- """
5
-
6
- import sys
7
- import os
8
-
9
- # Add parent directory to path for imports
10
- sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
11
-
12
- from database.connection import get_db_connection
13
-
14
- MIGRATION_SQL = """
15
- -- Create assignments table with enhanced route tracking
16
- CREATE TABLE IF NOT EXISTS assignments (
17
- assignment_id VARCHAR(50) PRIMARY KEY,
18
- order_id VARCHAR(50) NOT NULL,
19
- driver_id VARCHAR(50) NOT NULL,
20
-
21
- -- Assignment metadata
22
- assigned_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
23
- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
24
- sequence_number INTEGER,
25
-
26
- -- Route data from Google Routes API
27
- route_distance_meters INTEGER,
28
- route_duration_seconds INTEGER,
29
- route_duration_in_traffic_seconds INTEGER,
30
- route_summary TEXT,
31
- route_polyline TEXT,
32
-
33
- -- Origin/destination tracking
34
- driver_start_location_lat DECIMAL(10, 8),
35
- driver_start_location_lng DECIMAL(11, 8),
36
- delivery_location_lat DECIMAL(10, 8),
37
- delivery_location_lng DECIMAL(11, 8),
38
- delivery_address TEXT,
39
-
40
- -- Timing data
41
- estimated_arrival TIMESTAMP,
42
- actual_arrival TIMESTAMP,
43
-
44
- -- Actual vs estimated tracking
45
- actual_distance_meters INTEGER,
46
-
47
- -- Vehicle context
48
- vehicle_type VARCHAR(50),
49
-
50
- -- Status management
51
- status VARCHAR(20) CHECK(status IN ('active', 'in_progress', 'completed', 'cancelled', 'failed')) DEFAULT 'active',
52
-
53
- -- Additional metadata
54
- traffic_delay_seconds INTEGER,
55
- weather_conditions JSONB,
56
- route_confidence VARCHAR(100),
57
- notes TEXT,
58
-
59
- -- Foreign keys with proper cascade behavior
60
- FOREIGN KEY (order_id) REFERENCES orders(order_id) ON DELETE CASCADE,
61
- FOREIGN KEY (driver_id) REFERENCES drivers(driver_id) ON DELETE RESTRICT
62
- );
63
-
64
- -- Create indexes for performance
65
- CREATE INDEX IF NOT EXISTS idx_assignments_driver ON assignments(driver_id);
66
- CREATE INDEX IF NOT EXISTS idx_assignments_order ON assignments(order_id);
67
- CREATE INDEX IF NOT EXISTS idx_assignments_status ON assignments(status);
68
- CREATE INDEX IF NOT EXISTS idx_assignments_assigned_at ON assignments(assigned_at);
69
-
70
- -- Create trigger for auto-updating updated_at
71
- CREATE OR REPLACE FUNCTION update_updated_at_column()
72
- RETURNS TRIGGER AS $$
73
- BEGIN
74
- NEW.updated_at = CURRENT_TIMESTAMP;
75
- RETURN NEW;
76
- END;
77
- $$ LANGUAGE plpgsql;
78
-
79
- CREATE TRIGGER update_assignments_timestamp
80
- BEFORE UPDATE ON assignments
81
- FOR EACH ROW
82
- EXECUTE FUNCTION update_updated_at_column();
83
-
84
- -- Add unique constraint to prevent multiple active assignments per order
85
- CREATE UNIQUE INDEX IF NOT EXISTS idx_assignments_unique_active_order
86
- ON assignments(order_id)
87
- WHERE status IN ('active', 'in_progress');
88
- """
89
-
90
- ROLLBACK_SQL = """
91
- DROP INDEX IF EXISTS idx_assignments_unique_active_order;
92
- DROP TRIGGER IF EXISTS update_assignments_timestamp ON assignments;
93
- DROP INDEX IF EXISTS idx_assignments_assigned_at;
94
- DROP INDEX IF EXISTS idx_assignments_status;
95
- DROP INDEX IF EXISTS idx_assignments_order;
96
- DROP INDEX IF EXISTS idx_assignments_driver;
97
- DROP TABLE IF EXISTS assignments CASCADE;
98
- """
99
-
100
-
101
- def up():
102
- """Apply migration - create assignments table"""
103
- print("Running migration 002: Create assignments table...")
104
-
105
- try:
106
- conn = get_db_connection()
107
- cursor = conn.cursor()
108
-
109
- # Execute migration SQL
110
- cursor.execute(MIGRATION_SQL)
111
-
112
- conn.commit()
113
- cursor.close()
114
- conn.close()
115
-
116
- print("SUCCESS: Migration 002 applied successfully")
117
- print(" - Created assignments table")
118
- print(" - Created indexes (driver, order, status, assigned_at)")
119
- print(" - Created update trigger")
120
- print(" - Created unique constraint for active assignments per order")
121
- return True
122
-
123
- except Exception as e:
124
- print(f"ERROR: Migration 002 failed: {e}")
125
- return False
126
-
127
-
128
- def down():
129
- """Rollback migration - drop assignments table"""
130
- print("Rolling back migration 002: Drop assignments table...")
131
-
132
- try:
133
- conn = get_db_connection()
134
- cursor = conn.cursor()
135
-
136
- # Execute rollback SQL
137
- cursor.execute(ROLLBACK_SQL)
138
-
139
- conn.commit()
140
- cursor.close()
141
- conn.close()
142
-
143
- print("SUCCESS: Migration 002 rolled back successfully")
144
- return True
145
-
146
- except Exception as e:
147
- print(f"ERROR: Migration 002 rollback failed: {e}")
148
- return False
149
-
150
-
151
- if __name__ == "__main__":
152
- import sys
153
-
154
- if len(sys.argv) > 1 and sys.argv[1] == "down":
155
- down()
156
- else:
157
- up()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
database/migrations/003_add_order_driver_fk.py DELETED
@@ -1,88 +0,0 @@
1
- """
2
- Migration 003: Add Foreign Key Constraint to orders.assigned_driver_id
3
- Adds FK constraint to ensure referential integrity between orders and drivers
4
- """
5
-
6
- import sys
7
- import os
8
-
9
- # Add parent directory to path for imports
10
- sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
11
-
12
- from database.connection import get_db_connection
13
-
14
- MIGRATION_SQL = """
15
- -- Add foreign key constraint to orders.assigned_driver_id
16
- -- Use ON DELETE SET NULL so that if driver is deleted, order is not lost
17
- -- (The assignment record will be handled separately via RESTRICT constraint)
18
- ALTER TABLE orders
19
- ADD CONSTRAINT fk_orders_assigned_driver
20
- FOREIGN KEY (assigned_driver_id)
21
- REFERENCES drivers(driver_id)
22
- ON DELETE SET NULL;
23
- """
24
-
25
- ROLLBACK_SQL = """
26
- -- Drop foreign key constraint
27
- ALTER TABLE orders
28
- DROP CONSTRAINT IF EXISTS fk_orders_assigned_driver;
29
- """
30
-
31
-
32
- def up():
33
- """Apply migration - add FK constraint"""
34
- print("Running migration 003: Add FK constraint to orders.assigned_driver_id...")
35
-
36
- try:
37
- conn = get_db_connection()
38
- cursor = conn.cursor()
39
-
40
- # Execute migration SQL
41
- cursor.execute(MIGRATION_SQL)
42
-
43
- conn.commit()
44
- cursor.close()
45
- conn.close()
46
-
47
- print("SUCCESS: Migration 003 applied successfully")
48
- print(" - Added FK constraint: orders.assigned_driver_id -> drivers.driver_id")
49
- print(" - ON DELETE SET NULL (preserves order history if driver deleted)")
50
- return True
51
-
52
- except Exception as e:
53
- print(f"ERROR: Migration 003 failed: {e}")
54
- print(" Note: This may fail if there are existing invalid driver references")
55
- print(" Clean up orphaned assigned_driver_id values before running this migration")
56
- return False
57
-
58
-
59
- def down():
60
- """Rollback migration - drop FK constraint"""
61
- print("Rolling back migration 003: Drop FK constraint from orders.assigned_driver_id...")
62
-
63
- try:
64
- conn = get_db_connection()
65
- cursor = conn.cursor()
66
-
67
- # Execute rollback SQL
68
- cursor.execute(ROLLBACK_SQL)
69
-
70
- conn.commit()
71
- cursor.close()
72
- conn.close()
73
-
74
- print("SUCCESS: Migration 003 rolled back successfully")
75
- return True
76
-
77
- except Exception as e:
78
- print(f"ERROR: Migration 003 rollback failed: {e}")
79
- return False
80
-
81
-
82
- if __name__ == "__main__":
83
- import sys
84
-
85
- if len(sys.argv) > 1 and sys.argv[1] == "down":
86
- down()
87
- else:
88
- up()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
database/migrations/004_add_route_directions.py DELETED
@@ -1,84 +0,0 @@
1
- """
2
- Migration 004: Add route_directions column to assignments table
3
- Adds JSONB column to store turn-by-turn navigation instructions from Google Routes API
4
- """
5
-
6
- import sys
7
- import os
8
-
9
- # Add parent directory to path for imports
10
- sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
11
-
12
- from database.connection import get_db_connection
13
-
14
- MIGRATION_SQL = """
15
- -- Add route_directions column to store turn-by-turn navigation steps
16
- ALTER TABLE assignments
17
- ADD COLUMN IF NOT EXISTS route_directions JSONB;
18
-
19
- -- Add comment to explain the column
20
- COMMENT ON COLUMN assignments.route_directions IS 'Turn-by-turn navigation instructions from Google Routes API (array of steps with instructions, distance, duration)';
21
- """
22
-
23
- ROLLBACK_SQL = """
24
- -- Drop route_directions column
25
- ALTER TABLE assignments
26
- DROP COLUMN IF EXISTS route_directions;
27
- """
28
-
29
-
30
- def up():
31
- """Apply migration - add route_directions column"""
32
- print("Running migration 004: Add route_directions column to assignments table...")
33
-
34
- try:
35
- conn = get_db_connection()
36
- cursor = conn.cursor()
37
-
38
- # Execute migration SQL
39
- cursor.execute(MIGRATION_SQL)
40
-
41
- conn.commit()
42
- cursor.close()
43
- conn.close()
44
-
45
- print("SUCCESS: Migration 004 applied successfully")
46
- print(" - Added route_directions JSONB column to assignments table")
47
- print(" - Column will store turn-by-turn navigation instructions")
48
- return True
49
-
50
- except Exception as e:
51
- print(f"ERROR: Migration 004 failed: {e}")
52
- return False
53
-
54
-
55
- def down():
56
- """Rollback migration - drop route_directions column"""
57
- print("Rolling back migration 004: Drop route_directions column...")
58
-
59
- try:
60
- conn = get_db_connection()
61
- cursor = conn.cursor()
62
-
63
- # Execute rollback SQL
64
- cursor.execute(ROLLBACK_SQL)
65
-
66
- conn.commit()
67
- cursor.close()
68
- conn.close()
69
-
70
- print("SUCCESS: Migration 004 rolled back successfully")
71
- return True
72
-
73
- except Exception as e:
74
- print(f"ERROR: Migration 004 rollback failed: {e}")
75
- return False
76
-
77
-
78
- if __name__ == "__main__":
79
- import sys
80
-
81
- if len(sys.argv) > 1 and sys.argv[1] == "down":
82
- down()
83
- else:
84
- up()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
database/migrations/005_add_failure_reason.py DELETED
@@ -1,98 +0,0 @@
1
- """
2
- Migration 005: Add failure_reason column to assignments table
3
- Adds structured failure reason field for failed deliveries
4
- """
5
-
6
- import sys
7
- import os
8
-
9
- # Add parent directory to path for imports
10
- sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
11
-
12
- from database.connection import get_db_connection
13
-
14
- MIGRATION_SQL = """
15
- -- Add failure_reason column with predefined categories
16
- ALTER TABLE assignments
17
- ADD COLUMN IF NOT EXISTS failure_reason VARCHAR(100)
18
- CHECK(failure_reason IN (
19
- 'customer_not_available',
20
- 'wrong_address',
21
- 'refused_delivery',
22
- 'damaged_goods',
23
- 'payment_issue',
24
- 'vehicle_breakdown',
25
- 'access_restricted',
26
- 'weather_conditions',
27
- 'other'
28
- ));
29
-
30
- -- Add comment to explain the column
31
- COMMENT ON COLUMN assignments.failure_reason IS 'Structured reason for delivery failure (required when status is failed)';
32
- """
33
-
34
- ROLLBACK_SQL = """
35
- -- Drop failure_reason column
36
- ALTER TABLE assignments
37
- DROP COLUMN IF EXISTS failure_reason;
38
- """
39
-
40
-
41
- def up():
42
- """Apply migration - add failure_reason column"""
43
- print("Running migration 005: Add failure_reason column to assignments table...")
44
-
45
- try:
46
- conn = get_db_connection()
47
- cursor = conn.cursor()
48
-
49
- # Execute migration SQL
50
- cursor.execute(MIGRATION_SQL)
51
-
52
- conn.commit()
53
- cursor.close()
54
- conn.close()
55
-
56
- print("SUCCESS: Migration 005 applied successfully")
57
- print(" - Added failure_reason VARCHAR(100) column to assignments table")
58
- print(" - Constraint added for predefined failure categories")
59
- print(" - Available reasons: customer_not_available, wrong_address, refused_delivery,")
60
- print(" damaged_goods, payment_issue, vehicle_breakdown, access_restricted,")
61
- print(" weather_conditions, other")
62
- return True
63
-
64
- except Exception as e:
65
- print(f"ERROR: Migration 005 failed: {e}")
66
- return False
67
-
68
-
69
- def down():
70
- """Rollback migration - drop failure_reason column"""
71
- print("Rolling back migration 005: Drop failure_reason column...")
72
-
73
- try:
74
- conn = get_db_connection()
75
- cursor = conn.cursor()
76
-
77
- # Execute rollback SQL
78
- cursor.execute(ROLLBACK_SQL)
79
-
80
- conn.commit()
81
- cursor.close()
82
- conn.close()
83
-
84
- print("SUCCESS: Migration 005 rolled back successfully")
85
- return True
86
-
87
- except Exception as e:
88
- print(f"ERROR: Migration 005 rollback failed: {e}")
89
- return False
90
-
91
-
92
- if __name__ == "__main__":
93
- import sys
94
-
95
- if len(sys.argv) > 1 and sys.argv[1] == "down":
96
- down()
97
- else:
98
- up()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
database/migrations/006_add_delivery_timing.py DELETED
@@ -1,106 +0,0 @@
1
- """
2
- Migration 006: Add delivery timing and SLA tracking fields to orders table
3
- Adds expected_delivery_time (mandatory), delivery_status, and sla_grace_period_minutes
4
- """
5
-
6
- import sys
7
- import os
8
-
9
- # Add parent directory to path for imports
10
- sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
11
-
12
- from database.connection import get_db_connection
13
-
14
- MIGRATION_SQL = """
15
- -- Add expected delivery time (mandatory deadline promised to customer)
16
- ALTER TABLE orders
17
- ADD COLUMN IF NOT EXISTS expected_delivery_time TIMESTAMP;
18
-
19
- -- Add delivery performance status
20
- ALTER TABLE orders
21
- ADD COLUMN IF NOT EXISTS delivery_status VARCHAR(20)
22
- CHECK(delivery_status IN ('on_time', 'late', 'very_late', 'failed_on_time', 'failed_late'));
23
-
24
- -- Add SLA grace period (minutes after expected time that's still acceptable)
25
- ALTER TABLE orders
26
- ADD COLUMN IF NOT EXISTS sla_grace_period_minutes INTEGER DEFAULT 15;
27
-
28
- -- Add comments
29
- COMMENT ON COLUMN orders.expected_delivery_time IS 'Required delivery deadline promised to customer (mandatory when creating order)';
30
- COMMENT ON COLUMN orders.delivery_status IS 'Delivery performance: on_time, late (within grace), very_late (SLA violation), failed_on_time, failed_late';
31
- COMMENT ON COLUMN orders.sla_grace_period_minutes IS 'Grace period in minutes after expected_delivery_time (default: 15 mins)';
32
-
33
- -- Create index for querying by expected delivery time
34
- CREATE INDEX IF NOT EXISTS idx_orders_expected_delivery ON orders(expected_delivery_time);
35
- """
36
-
37
- ROLLBACK_SQL = """
38
- -- Drop indexes
39
- DROP INDEX IF EXISTS idx_orders_expected_delivery;
40
-
41
- -- Drop columns
42
- ALTER TABLE orders
43
- DROP COLUMN IF EXISTS expected_delivery_time,
44
- DROP COLUMN IF EXISTS delivery_status,
45
- DROP COLUMN IF EXISTS sla_grace_period_minutes;
46
- """
47
-
48
-
49
- def up():
50
- """Apply migration - add delivery timing fields"""
51
- print("Running migration 006: Add delivery timing and SLA tracking fields...")
52
-
53
- try:
54
- conn = get_db_connection()
55
- cursor = conn.cursor()
56
-
57
- # Execute migration SQL
58
- cursor.execute(MIGRATION_SQL)
59
-
60
- conn.commit()
61
- cursor.close()
62
- conn.close()
63
-
64
- print("SUCCESS: Migration 006 applied successfully")
65
- print(" - Added expected_delivery_time TIMESTAMP column")
66
- print(" - Added delivery_status VARCHAR(20) column")
67
- print(" - Added sla_grace_period_minutes INTEGER column (default: 15)")
68
- print(" - Created index on expected_delivery_time")
69
- print(" - Valid delivery statuses: on_time, late, very_late, failed_on_time, failed_late")
70
- return True
71
-
72
- except Exception as e:
73
- print(f"ERROR: Migration 006 failed: {e}")
74
- return False
75
-
76
-
77
- def down():
78
- """Rollback migration - drop delivery timing fields"""
79
- print("Rolling back migration 006: Drop delivery timing fields...")
80
-
81
- try:
82
- conn = get_db_connection()
83
- cursor = conn.cursor()
84
-
85
- # Execute rollback SQL
86
- cursor.execute(ROLLBACK_SQL)
87
-
88
- conn.commit()
89
- cursor.close()
90
- conn.close()
91
-
92
- print("SUCCESS: Migration 006 rolled back successfully")
93
- return True
94
-
95
- except Exception as e:
96
- print(f"ERROR: Migration 006 rollback failed: {e}")
97
- return False
98
-
99
-
100
- if __name__ == "__main__":
101
- import sys
102
-
103
- if len(sys.argv) > 1 and sys.argv[1] == "down":
104
- down()
105
- else:
106
- up()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
database/migrations/007_add_user_id.py DELETED
@@ -1,167 +0,0 @@
1
- """
2
- Migration 007: Add user_id columns for multi-tenant support
3
-
4
- This migration adds user_id to all tables to enable user-specific data isolation.
5
- Each user will only see their own orders, drivers, assignments, etc.
6
- """
7
-
8
- import sys
9
- from pathlib import Path
10
-
11
- # Add parent directory to path
12
- sys.path.insert(0, str(Path(__file__).parent.parent.parent))
13
-
14
- from database.connection import execute_write
15
-
16
-
17
- def up():
18
- """Add user_id columns and indexes"""
19
-
20
- migrations = [
21
- # Add user_id column to orders table
22
- """
23
- ALTER TABLE orders
24
- ADD COLUMN IF NOT EXISTS user_id VARCHAR(255);
25
- """,
26
-
27
- # Add user_id column to drivers table
28
- """
29
- ALTER TABLE drivers
30
- ADD COLUMN IF NOT EXISTS user_id VARCHAR(255);
31
- """,
32
-
33
- # Add user_id column to assignments table
34
- """
35
- ALTER TABLE assignments
36
- ADD COLUMN IF NOT EXISTS user_id VARCHAR(255);
37
- """,
38
-
39
- # Add user_id column to exceptions table
40
- """
41
- ALTER TABLE exceptions
42
- ADD COLUMN IF NOT EXISTS user_id VARCHAR(255);
43
- """,
44
-
45
- # Add user_id column to agent_decisions table
46
- """
47
- ALTER TABLE agent_decisions
48
- ADD COLUMN IF NOT EXISTS user_id VARCHAR(255);
49
- """,
50
-
51
- # Add user_id column to metrics table
52
- """
53
- ALTER TABLE metrics
54
- ADD COLUMN IF NOT EXISTS user_id VARCHAR(255);
55
- """,
56
-
57
- # Create indexes for fast user-based filtering
58
- """
59
- CREATE INDEX IF NOT EXISTS idx_orders_user_id ON orders(user_id);
60
- """,
61
-
62
- """
63
- CREATE INDEX IF NOT EXISTS idx_drivers_user_id ON drivers(user_id);
64
- """,
65
-
66
- """
67
- CREATE INDEX IF NOT EXISTS idx_assignments_user_id ON assignments(user_id);
68
- """,
69
-
70
- """
71
- CREATE INDEX IF NOT EXISTS idx_exceptions_user_id ON exceptions(user_id);
72
- """,
73
-
74
- """
75
- CREATE INDEX IF NOT EXISTS idx_agent_decisions_user_id ON agent_decisions(user_id);
76
- """,
77
-
78
- """
79
- CREATE INDEX IF NOT EXISTS idx_metrics_user_id ON metrics(user_id);
80
- """,
81
-
82
- # Create composite indexes for common queries
83
- """
84
- CREATE INDEX IF NOT EXISTS idx_orders_user_status ON orders(user_id, status);
85
- """,
86
-
87
- """
88
- CREATE INDEX IF NOT EXISTS idx_orders_user_created ON orders(user_id, created_at DESC);
89
- """,
90
-
91
- """
92
- CREATE INDEX IF NOT EXISTS idx_drivers_user_status ON drivers(user_id, status);
93
- """,
94
-
95
- """
96
- CREATE INDEX IF NOT EXISTS idx_assignments_user_driver ON assignments(user_id, driver_id);
97
- """,
98
-
99
- """
100
- CREATE INDEX IF NOT EXISTS idx_assignments_user_order ON assignments(user_id, order_id);
101
- """,
102
- ]
103
-
104
- print("Migration 007: Adding user_id columns...")
105
-
106
- for i, sql in enumerate(migrations, 1):
107
- try:
108
- print(f" [{i}/{len(migrations)}] Executing: {sql.strip()[:60]}...")
109
- execute_write(sql)
110
- print(f" Success")
111
- except Exception as e:
112
- print(f" Warning: {e}")
113
- # Continue even if column already exists
114
-
115
- print("\nMigration 007 complete!")
116
- print("\nNext steps:")
117
- print(" 1. Existing data will have NULL user_id (that's OK for now)")
118
- print(" 2. New data will automatically get user_id from authentication")
119
- print(" 3. You can optionally run a data migration to assign existing records to a test user")
120
-
121
-
122
- def down():
123
- """Remove user_id columns and indexes (rollback)"""
124
-
125
- rollback_migrations = [
126
- # Drop indexes first
127
- "DROP INDEX IF EXISTS idx_assignments_user_order;",
128
- "DROP INDEX IF EXISTS idx_assignments_user_driver;",
129
- "DROP INDEX IF EXISTS idx_drivers_user_status;",
130
- "DROP INDEX IF EXISTS idx_orders_user_created;",
131
- "DROP INDEX IF EXISTS idx_orders_user_status;",
132
- "DROP INDEX IF EXISTS idx_metrics_user_id;",
133
- "DROP INDEX IF EXISTS idx_agent_decisions_user_id;",
134
- "DROP INDEX IF EXISTS idx_exceptions_user_id;",
135
- "DROP INDEX IF EXISTS idx_assignments_user_id;",
136
- "DROP INDEX IF EXISTS idx_drivers_user_id;",
137
- "DROP INDEX IF EXISTS idx_orders_user_id;",
138
-
139
- # Drop columns
140
- "ALTER TABLE metrics DROP COLUMN IF EXISTS user_id;",
141
- "ALTER TABLE agent_decisions DROP COLUMN IF EXISTS user_id;",
142
- "ALTER TABLE exceptions DROP COLUMN IF EXISTS user_id;",
143
- "ALTER TABLE assignments DROP COLUMN IF EXISTS user_id;",
144
- "ALTER TABLE drivers DROP COLUMN IF EXISTS user_id;",
145
- "ALTER TABLE orders DROP COLUMN IF EXISTS user_id;",
146
- ]
147
-
148
- print("Rolling back Migration 007...")
149
-
150
- for i, sql in enumerate(rollback_migrations, 1):
151
- try:
152
- print(f" [{i}/{len(rollback_migrations)}] {sql[:60]}...")
153
- execute_write(sql)
154
- print(f" Success")
155
- except Exception as e:
156
- print(f" Warning: {e}")
157
-
158
- print("\nRollback complete!")
159
-
160
-
161
- if __name__ == "__main__":
162
- import sys
163
-
164
- if len(sys.argv) > 1 and sys.argv[1] == "down":
165
- down()
166
- else:
167
- up()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
database/migrations/008_add_driver_address.py DELETED
@@ -1,64 +0,0 @@
1
- """
2
- Migration 008: Add current_address field to drivers table
3
- This stores the address for driver locations (provided by user along with lat/lng)
4
- """
5
-
6
- import sys
7
- from pathlib import Path
8
-
9
- # Add parent directory to path
10
- sys.path.insert(0, str(Path(__file__).parent.parent.parent))
11
-
12
- from database.connection import execute_write
13
-
14
-
15
- def up():
16
- """Add current_address column to drivers table"""
17
-
18
- migrations = [
19
- """
20
- ALTER TABLE drivers
21
- ADD COLUMN IF NOT EXISTS current_address TEXT;
22
- """,
23
- ]
24
-
25
- print("Migration 008: Adding current_address column to drivers...")
26
-
27
- for i, sql in enumerate(migrations, 1):
28
- try:
29
- print(f" [{i}/{len(migrations)}] Executing: {sql.strip()[:60]}...")
30
- execute_write(sql)
31
- print(f" Success")
32
- except Exception as e:
33
- print(f" Warning: {e}")
34
-
35
- print("\nMigration 008 complete!")
36
-
37
-
38
- def down():
39
- """Remove current_address column from drivers table"""
40
-
41
- rollback_migrations = [
42
- "ALTER TABLE drivers DROP COLUMN IF EXISTS current_address;",
43
- ]
44
-
45
- print("Rolling back Migration 008...")
46
-
47
- for i, sql in enumerate(rollback_migrations, 1):
48
- try:
49
- print(f" [{i}/{len(rollback_migrations)}] {sql[:60]}...")
50
- execute_write(sql)
51
- print(f" Success")
52
- except Exception as e:
53
- print(f" Warning: {e}")
54
-
55
- print("\nRollback complete!")
56
-
57
-
58
- if __name__ == "__main__":
59
- import sys
60
-
61
- if len(sys.argv) > 1 and sys.argv[1] == "down":
62
- down()
63
- else:
64
- up()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
database/user_context.py DELETED
@@ -1,81 +0,0 @@
1
- """
2
- User Context and Permission Module
3
- Handles permission checks for API key authentication
4
- """
5
-
6
- from typing import Optional
7
-
8
-
9
- def check_permission(user_scopes: list, required_scope: str) -> bool:
10
- """
11
- Check if user has required permission
12
-
13
- Args:
14
- user_scopes: List of scopes user has
15
- required_scope: Scope needed for this operation
16
-
17
- Returns:
18
- True if user has permission
19
- """
20
- # Admin has all permissions
21
- if 'admin' in user_scopes:
22
- return True
23
-
24
- # Check specific scope
25
- return required_scope in user_scopes
26
-
27
-
28
- # Scope requirements for each tool
29
- SCOPE_REQUIREMENTS = {
30
- # Order operations
31
- 'create_order': 'orders:write',
32
- 'fetch_orders': 'orders:read',
33
- 'update_order': 'orders:write',
34
- 'delete_order': 'orders:write',
35
- 'search_orders': 'orders:read',
36
- 'get_order_details': 'orders:read',
37
- 'count_orders': 'orders:read',
38
- 'get_incomplete_orders': 'orders:read',
39
-
40
- # Driver operations
41
- 'create_driver': 'drivers:write',
42
- 'fetch_drivers': 'drivers:read',
43
- 'update_driver': 'drivers:write',
44
- 'delete_driver': 'drivers:write',
45
- 'search_drivers': 'drivers:read',
46
- 'get_driver_details': 'drivers:read',
47
- 'count_drivers': 'drivers:read',
48
- 'get_available_drivers': 'drivers:read',
49
-
50
- # Assignment operations
51
- 'create_assignment': 'assignments:manage',
52
- 'auto_assign_order': 'assignments:manage',
53
- 'intelligent_assign_order': 'assignments:manage',
54
- 'get_assignment_details': 'assignments:manage',
55
- 'update_assignment': 'assignments:manage',
56
- 'unassign_order': 'assignments:manage',
57
- 'complete_delivery': 'assignments:manage',
58
- 'fail_delivery': 'assignments:manage',
59
-
60
- # Routing (public - no scope required)
61
- 'geocode_address': None,
62
- 'calculate_route': None,
63
- 'calculate_intelligent_route': None,
64
-
65
- # Dangerous operations (admin only)
66
- 'delete_all_orders': 'admin',
67
- 'delete_all_drivers': 'admin',
68
- }
69
-
70
-
71
- def get_required_scope(tool_name: str) -> Optional[str]:
72
- """
73
- Get the scope required for a tool
74
-
75
- Args:
76
- tool_name: Name of the tool
77
-
78
- Returns:
79
- Required scope or None if tool is public
80
- """
81
- return SCOPE_REQUIREMENTS.get(tool_name, 'admin')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
flow.md ADDED
@@ -0,0 +1,828 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ● πŸ”„ Complete Function Flow
2
+
3
+ Input: "Create an order for John Doe at 123 Main St, San
4
+ Francisco"
5
+
6
+ Let me trace every single function call with this exact example!
7
+
8
+ ---
9
+ πŸ“ž FUNCTION CALL SEQUENCE:
10
+
11
+ STEP 1: User Clicks Send Button
12
+
13
+ FILE: ui/app.py
14
+ LINE: 448-452
15
+
16
+ User clicks "Send" button in Gradio UI
17
+ ↓
18
+ FUNCTION CALLED: send_message(message, conv_state)
19
+ message = "Create an order for John Doe at 123 Main St, San
20
+ Francisco"
21
+ conv_state = ConversationManager object
22
+
23
+ Function Code:
24
+ def send_message(message, conv_state):
25
+ """Handle send button click"""
26
+ chat_history, tools, new_state = handle_chat_message(message,
27
+ conv_state)
28
+ # ↑
29
+ # CALLS THIS NEXT
30
+ return chat_history, tools, new_state, ""
31
+
32
+ ---
33
+ STEP 2: handle_chat_message()
34
+
35
+ FILE: ui/app.py
36
+ LINE: 223-241
37
+
38
+ FUNCTION: handle_chat_message(message, conversation_state)
39
+ message = "Create an order for John Doe at 123 Main St, San
40
+ Francisco"
41
+ conversation_state = ConversationManager object
42
+
43
+ Function Code:
44
+ def handle_chat_message(message, conversation_state):
45
+ if not message.strip():
46
+ return ...
47
+
48
+ # Process message through chat engine
49
+ response, tool_calls = chat_engine.process_message(message,
50
+ conversation_state)
51
+ # ↑
52
+ # CALLS THIS NEXT
53
+
54
+ # Return updated UI
55
+ return conversation_state.get_formatted_history(),
56
+ conversation_state.get_tool_calls(), conversation_state
57
+
58
+ ---
59
+ STEP 3: chat_engine.process_message()
60
+
61
+ FILE: chat/chat_engine.py
62
+ LINE: 58-73
63
+
64
+ FUNCTION: ChatEngine.process_message(user_message, conversation)
65
+ user_message = "Create an order for John Doe at 123 Main St,
66
+ San Francisco"
67
+ conversation = ConversationManager object
68
+
69
+ Function Code:
70
+ def process_message(self, user_message, conversation):
71
+ """Process user message and return AI response"""
72
+ return self.provider.process_message(user_message,
73
+ conversation)
74
+ # ↑
75
+ # self.provider = GeminiProvider (from chat_engine.py:26)
76
+ # CALLS GeminiProvider.process_message() NEXT
77
+
78
+ ---
79
+ STEP 4: GeminiProvider.process_message()
80
+
81
+ FILE: chat/providers/gemini_provider.py
82
+ LINE: 173-212
83
+
84
+ FUNCTION: GeminiProvider.process_message(user_message,
85
+ conversation)
86
+ user_message = "Create an order for John Doe at 123 Main St,
87
+ San Francisco"
88
+ conversation = ConversationManager object
89
+
90
+ Function Code:
91
+ def process_message(self, user_message, conversation):
92
+ """Process user message with Gemini"""
93
+ if not self.api_available:
94
+ return self._handle_no_api(), []
95
+
96
+ # Lazy initialization on first use
97
+ self._ensure_initialized() # ← CALLS THIS if not initialized
98
+
99
+ if not self._initialized:
100
+ return "⚠️ Failed to initialize...", []
101
+
102
+ try:
103
+ # Build conversation history for Gemini
104
+ chat =
105
+ self.model.start_chat(history=self._convert_history(conversation))
106
+ # ↑
107
+ # CALLS
108
+ _convert_history()
109
+
110
+ # Send message and get response
111
+ response = chat.send_message(user_message,
112
+ safety_settings={...})
113
+ # ↑
114
+ # 🌐 API CALL TO GOOGLE GEMINI
115
+ # Sends: "Create an order for John Doe at 123
116
+ Main St, San Francisco"
117
+
118
+ # Add user message to conversation
119
+ conversation.add_message("user", user_message)
120
+
121
+ # Process response and handle function calls
122
+ return self._process_response(response, conversation,
123
+ chat)
124
+ # ↑
125
+ # CALLS THIS NEXT
126
+
127
+ ---
128
+ STEP 5: Gemini API Processes Request
129
+
130
+ 🌐 GOOGLE GEMINI API (External)
131
+
132
+ RECEIVES:
133
+ - System Prompt: "You are an AI assistant for FleetMind..."
134
+ - User Message: "Create an order for John Doe at 123 Main St, San
135
+ Francisco"
136
+ - Available Tools: [geocode_address, create_order]
137
+
138
+ AI ANALYZES:
139
+ "User wants to create an order. I have:
140
+ βœ… Customer Name: John Doe
141
+ βœ… Address: 123 Main St, San Francisco
142
+ ❌ GPS Coordinates: Missing!
143
+
144
+ DECISION: Call geocode_address tool first to get coordinates."
145
+
146
+ RETURNS TO CODE:
147
+ response = {
148
+ candidates: [{
149
+ content: {
150
+ parts: [{
151
+ function_call: {
152
+ name: "geocode_address",
153
+ args: {
154
+ "address": "123 Main St, San Francisco"
155
+ }
156
+ }
157
+ }]
158
+ }
159
+ }]
160
+ }
161
+
162
+ ---
163
+ STEP 6: _process_response() - Detects Function Call
164
+
165
+ FILE: chat/providers/gemini_provider.py
166
+ LINE: 226-393
167
+
168
+ FUNCTION: _process_response(response, conversation, chat)
169
+ response = Response from Gemini with function_call
170
+ conversation = ConversationManager object
171
+ chat = Gemini chat session
172
+
173
+ Function Code:
174
+ def _process_response(self, response, conversation, chat):
175
+ """Process Gemini's response and handle function calls"""
176
+ tool_calls_made = []
177
+
178
+ try:
179
+ # Check ALL parts for function calls
180
+ parts = response.candidates[0].content.parts
181
+ logger.info(f"Processing response with {len(parts)}
182
+ part(s)")
183
+ # ↑
184
+ # LOGS: "Processing response with 1 part(s)"
185
+
186
+ for part in parts:
187
+ if hasattr(part, 'function_call'):
188
+ fc = part.function_call
189
+ if fc and hasattr(fc, 'name') and fc.name:
190
+ has_function_call = True
191
+ logger.info(f"Detected function call:
192
+ {fc.name}")
193
+ # ↑
194
+ # LOGS: "Detected function call:
195
+ geocode_address"
196
+ break
197
+
198
+ if has_function_call:
199
+ # Handle function calls (potentially multiple in
200
+ sequence)
201
+ current_response = response
202
+ max_iterations = 10
203
+
204
+ for iteration in range(max_iterations): # ← LOOP
205
+ STARTS
206
+ # Extract function call details
207
+ first_part =
208
+ current_response.candidates[0].content.parts[0]
209
+ function_call = first_part.function_call
210
+ function_name = function_call.name #
211
+ "geocode_address"
212
+ function_args = dict(function_call.args) #
213
+ {"address": "123 Main St, San Francisco"}
214
+
215
+ logger.info(f"Gemini executing function:
216
+ {function_name} (iteration {iteration + 1})")
217
+ # ↑
218
+ # LOGS: "Gemini executing function:
219
+ geocode_address (iteration 1)"
220
+
221
+ # Execute the tool
222
+ tool_result = execute_tool(function_name,
223
+ function_args)
224
+ # ↑
225
+ # CALLS execute_tool() NEXT
226
+
227
+ ---
228
+ STEP 7: execute_tool() - Routes to Handler
229
+
230
+ FILE: chat/tools.py
231
+ LINE: 92-118
232
+
233
+ FUNCTION: execute_tool(tool_name, tool_input)
234
+ tool_name = "geocode_address"
235
+ tool_input = {"address": "123 Main St, San Francisco"}
236
+
237
+ Function Code:
238
+ def execute_tool(tool_name, tool_input):
239
+ """Route tool execution to appropriate handler"""
240
+ try:
241
+ if tool_name == "geocode_address":
242
+ return handle_geocode_address(tool_input)
243
+ # ↑
244
+ # CALLS THIS NEXT
245
+ elif tool_name == "create_order":
246
+ return handle_create_order(tool_input)
247
+ else:
248
+ return {"success": False, "error": f"Unknown tool:
249
+ {tool_name}"}
250
+ except Exception as e:
251
+ logger.error(f"Tool execution error ({tool_name}): {e}")
252
+ return {"success": False, "error": str(e)}
253
+
254
+ ---
255
+ STEP 8: handle_geocode_address()
256
+
257
+ FILE: chat/tools.py
258
+ LINE: 121-150
259
+
260
+ FUNCTION: handle_geocode_address(tool_input)
261
+ tool_input = {"address": "123 Main St, San Francisco"}
262
+
263
+ Function Code:
264
+ def handle_geocode_address(tool_input):
265
+ """Execute geocoding tool"""
266
+ address = tool_input.get("address", "") # "123 Main St, San
267
+ Francisco"
268
+
269
+ if not address:
270
+ return {"success": False, "error": "Address is required"}
271
+
272
+ logger.info(f"Geocoding address: {address}")
273
+ # ↑
274
+ # LOGS: "Geocoding address: 123 Main St, San
275
+ Francisco"
276
+
277
+ result = geocoding_service.geocode(address)
278
+ # ↑
279
+ # CALLS geocoding_service.geocode() NEXT
280
+
281
+ return {
282
+ "success": True,
283
+ "latitude": result["lat"],
284
+ "longitude": result["lng"],
285
+ "formatted_address": result["formatted_address"],
286
+ "confidence": result["confidence"],
287
+ "message": f"Address geocoded successfully
288
+ ({result['confidence']})"
289
+ }
290
+
291
+ ---
292
+ STEP 9: GeocodingService.geocode()
293
+
294
+ FILE: chat/geocoding.py
295
+ LINE: 28-65
296
+
297
+ FUNCTION: GeocodingService.geocode(address)
298
+ address = "123 Main St, San Francisco"
299
+
300
+ Function Code:
301
+ def geocode(self, address):
302
+ """Geocode an address to coordinates"""
303
+ if not address:
304
+ return self._error_response("Address is required")
305
+
306
+ # Use mock or real API
307
+ if self.use_mock: # True (no HERE_API_KEY configured)
308
+ return self._geocode_mock(address)
309
+ # ↑
310
+ # CALLS THIS NEXT
311
+ else:
312
+ return self._geocode_here(address)
313
+
314
+ ---
315
+ STEP 10: _geocode_mock() - Returns Coordinates
316
+
317
+ FILE: chat/geocoding.py
318
+ LINE: 52-70
319
+
320
+ FUNCTION: _geocode_mock(address)
321
+ address = "123 Main St, San Francisco"
322
+
323
+ Function Code:
324
+ def _geocode_mock(self, address):
325
+ """Mock geocoding using city detection"""
326
+ address_lower = address.lower()
327
+
328
+ # Try to detect city in address
329
+ for city_name, (lat, lng) in CITY_COORDINATES.items():
330
+ if city_name in address_lower:
331
+ logger.info(f"Mock geocoding detected city:
332
+ {city_name}")
333
+ # ↑
334
+ # LOGS: "Mock geocoding detected city: san
335
+ francisco"
336
+
337
+ return {
338
+ "lat": lat, # 37.7749
339
+ "lng": lng, # -122.4194
340
+ "formatted_address": address,
341
+ "confidence": "mock"
342
+ }
343
+
344
+ # Default to San Francisco if no city detected
345
+ return {
346
+ "lat": 37.7749,
347
+ "lng": -122.4194,
348
+ "formatted_address": address,
349
+ "confidence": "mock"
350
+ }
351
+
352
+ RETURNS:
353
+ {
354
+ "lat": 37.7749,
355
+ "lng": -122.4194,
356
+ "formatted_address": "123 Main St, San Francisco",
357
+ "confidence": "mock"
358
+ }
359
+
360
+ ---
361
+ STEP 11: Back to handle_geocode_address()
362
+
363
+ FILE: chat/tools.py
364
+ LINE: 141-150
365
+
366
+ result = {
367
+ "lat": 37.7749,
368
+ "lng": -122.4194,
369
+ "formatted_address": "123 Main St, San Francisco",
370
+ "confidence": "mock"
371
+ }
372
+
373
+ RETURNS:
374
+ {
375
+ "success": True,
376
+ "latitude": 37.7749,
377
+ "longitude": -122.4194,
378
+ "formatted_address": "123 Main St, San Francisco",
379
+ "confidence": "mock",
380
+ "message": "Address geocoded successfully (mock)"
381
+ }
382
+
383
+ ---
384
+ STEP 12: Back to _process_response() - Tool Result Received
385
+
386
+ FILE: chat/providers/gemini_provider.py
387
+ LINE: 285-310
388
+
389
+ tool_result = {
390
+ "success": True,
391
+ "latitude": 37.7749,
392
+ "longitude": -122.4194,
393
+ "formatted_address": "123 Main St, San Francisco",
394
+ "confidence": "mock",
395
+ "message": "Address geocoded successfully (mock)"
396
+ }
397
+
398
+ # Track for transparency
399
+ tool_calls_made.append({
400
+ "tool": "geocode_address",
401
+ "input": {"address": "123 Main St, San Francisco"},
402
+ "result": tool_result
403
+ })
404
+
405
+ conversation.add_tool_result("geocode_address", function_args,
406
+ tool_result)
407
+
408
+ # Send function result back to Gemini
409
+ current_response = chat.send_message(
410
+ genai.protos.Content(
411
+ parts=[genai.protos.Part(
412
+ function_response=genai.protos.FunctionResponse(
413
+ name="geocode_address",
414
+ response={"result": tool_result}
415
+ )
416
+ )]
417
+ )
418
+ )
419
+ # ↑
420
+ # 🌐 API CALL TO GEMINI WITH GEOCODING RESULT
421
+
422
+ ---
423
+ STEP 13: Gemini Receives Geocoding Result
424
+
425
+ 🌐 GOOGLE GEMINI API (External)
426
+
427
+ RECEIVES:
428
+ - Function: geocode_address
429
+ - Result: {
430
+ "success": True,
431
+ "latitude": 37.7749,
432
+ "longitude": -122.4194
433
+ }
434
+
435
+ AI ANALYZES:
436
+ "Great! I now have GPS coordinates:
437
+ βœ… Customer Name: John Doe
438
+ βœ… Address: 123 Main St, San Francisco
439
+ βœ… Latitude: 37.7749
440
+ βœ… Longitude: -122.4194
441
+
442
+ DECISION: Now I can create the order in the database!
443
+ Call create_order tool."
444
+
445
+ RETURNS TO CODE:
446
+ response = {
447
+ candidates: [{
448
+ content: {
449
+ parts: [{
450
+ function_call: {
451
+ name: "create_order",
452
+ args: {
453
+ "customer_name": "John Doe",
454
+ "delivery_address": "123 Main St, San
455
+ Francisco",
456
+ "delivery_lat": 37.7749,
457
+ "delivery_lng": -122.4194,
458
+ "priority": "standard"
459
+ }
460
+ }
461
+ }]
462
+ }
463
+ }]
464
+ }
465
+
466
+ ---
467
+ STEP 14: Loop Continues - Detects create_order
468
+
469
+ FILE: chat/providers/gemini_provider.py
470
+ LINE: 252-285
471
+
472
+ # Still in the for loop (iteration 2)
473
+ first_part = current_response.candidates[0].content.parts[0]
474
+ has_fc = True # Another function call detected
475
+
476
+ function_call = first_part.function_call
477
+ function_name = function_call.name # "create_order"
478
+ function_args = dict(function_call.args) # {customer_name,
479
+ address, lat, lng...}
480
+
481
+ logger.info(f"Gemini executing function: {function_name}
482
+ (iteration 2)")
483
+ # ↑
484
+ # LOGS: "Gemini executing function: create_order
485
+ (iteration 2)"
486
+
487
+ # Execute the tool
488
+ tool_result = execute_tool(function_name, function_args)
489
+ # ↑
490
+ # CALLS execute_tool() AGAIN
491
+
492
+ ---
493
+ STEP 15: execute_tool() - Routes to create_order
494
+
495
+ FILE: chat/tools.py
496
+ LINE: 92-118
497
+
498
+ FUNCTION: execute_tool(tool_name, tool_input)
499
+ tool_name = "create_order"
500
+ tool_input = {
501
+ "customer_name": "John Doe",
502
+ "delivery_address": "123 Main St, San Francisco",
503
+ "delivery_lat": 37.7749,
504
+ "delivery_lng": -122.4194,
505
+ "priority": "standard"
506
+ }
507
+
508
+ Function Code:
509
+ def execute_tool(tool_name, tool_input):
510
+ try:
511
+ if tool_name == "geocode_address":
512
+ return handle_geocode_address(tool_input)
513
+ elif tool_name == "create_order":
514
+ return handle_create_order(tool_input)
515
+ # ↑
516
+ # CALLS THIS NEXT
517
+
518
+ ---
519
+ STEP 16: handle_create_order()
520
+
521
+ FILE: chat/tools.py
522
+ LINE: 153-242
523
+
524
+ FUNCTION: handle_create_order(tool_input)
525
+ tool_input = {
526
+ "customer_name": "John Doe",
527
+ "delivery_address": "123 Main St, San Francisco",
528
+ "delivery_lat": 37.7749,
529
+ "delivery_lng": -122.4194,
530
+ "priority": "standard"
531
+ }
532
+
533
+ Function Code:
534
+ def handle_create_order(tool_input):
535
+ """Execute order creation tool"""
536
+
537
+ # Extract fields with defaults
538
+ customer_name = tool_input.get("customer_name") # "John Doe"
539
+ customer_phone = tool_input.get("customer_phone") # None
540
+ customer_email = tool_input.get("customer_email") # None
541
+ delivery_address = tool_input.get("delivery_address") # "123
542
+ Main St, San Francisco"
543
+ delivery_lat = tool_input.get("delivery_lat") # 37.7749
544
+ delivery_lng = tool_input.get("delivery_lng") # -122.4194
545
+ priority = tool_input.get("priority", "standard") #
546
+ "standard"
547
+ special_instructions = tool_input.get("special_instructions")
548
+ # None
549
+ weight_kg = tool_input.get("weight_kg", 5.0) # 5.0
550
+
551
+ # Validate required fields
552
+ if not all([customer_name, delivery_address, delivery_lat,
553
+ delivery_lng]):
554
+ return {"success": False, "error": "Missing required
555
+ fields..."}
556
+
557
+ # Generate order ID
558
+ now = datetime.now()
559
+ order_id = f"ORD-{now.strftime('%Y%m%d%H%M%S')}"
560
+ # ↑
561
+ # e.g., "ORD-20251114015858"
562
+
563
+ # Handle time window
564
+ time_window_end = now + timedelta(hours=6) # 6 hours from now
565
+ time_window_start = now + timedelta(hours=2) # 2 hours from
566
+ now
567
+
568
+ # Insert into database
569
+ query = """
570
+ INSERT INTO orders (
571
+ order_id, customer_name, customer_phone,
572
+ customer_email,
573
+ delivery_address, delivery_lat, delivery_lng,
574
+ time_window_start, time_window_end,
575
+ priority, weight_kg, status, special_instructions
576
+ ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s,
577
+ %s)
578
+ """
579
+
580
+ params = (
581
+ order_id, # "ORD-20251114015858"
582
+ customer_name, # "John Doe"
583
+ customer_phone, # None
584
+ customer_email, # None
585
+ delivery_address, # "123 Main St, San Francisco"
586
+ delivery_lat, # 37.7749
587
+ delivery_lng, # -122.4194
588
+ time_window_start, # 2025-11-14 03:58:58
589
+ time_window_end, # 2025-11-14 07:58:58
590
+ priority, # "standard"
591
+ weight_kg, # 5.0
592
+ "pending", # status
593
+ special_instructions # None
594
+ )
595
+
596
+ try:
597
+ execute_write(query, params)
598
+ # ↑
599
+ # CALLS THIS NEXT - DATABASE WRITE!
600
+
601
+ ---
602
+ STEP 17: execute_write() - INSERT INTO DATABASE
603
+
604
+ FILE: database/connection.py
605
+ LINE: 71-97
606
+
607
+ FUNCTION: execute_write(query, params)
608
+ query = "INSERT INTO orders (...) VALUES (%s, %s, ...)"
609
+ params = ("ORD-20251114015858", "John Doe", None, None, "123
610
+ Main St...", ...)
611
+
612
+ Function Code:
613
+ def execute_write(query, params=None):
614
+ """Execute a write query (INSERT, UPDATE, DELETE)"""
615
+ try:
616
+ # Connect to PostgreSQL
617
+ conn = get_db_connection()
618
+ # ↑
619
+ # Opens connection to localhost:5432/fleetmind
620
+
621
+ logger.info("Database connection established:
622
+ fleetmind@localhost")
623
+
624
+ cursor = conn.cursor()
625
+
626
+ # Execute INSERT query
627
+ cursor.execute(query, params)
628
+ # ↑
629
+ # πŸ’Ύ EXECUTES SQL:
630
+ # INSERT INTO orders (order_id, customer_name, ...)
631
+ # VALUES ('ORD-20251114015858', 'John Doe', ...)
632
+
633
+ conn.commit() # ← SAVES TO POSTGRESQL PERMANENTLY!
634
+
635
+ rows_affected = cursor.rowcount # 1
636
+
637
+ cursor.close()
638
+ conn.close()
639
+
640
+ logger.info("Database connection closed")
641
+
642
+ return rows_affected # Returns 1
643
+
644
+ DATABASE STATE:
645
+ -- New row added to orders table:
646
+ ORDER_ID: ORD-20251114015858
647
+ CUSTOMER_NAME: John Doe
648
+ CUSTOMER_PHONE: NULL
649
+ CUSTOMER_EMAIL: NULL
650
+ DELIVERY_ADDRESS: 123 Main St, San Francisco
651
+ DELIVERY_LAT: 37.7749
652
+ DELIVERY_LNG: -122.4194
653
+ STATUS: pending
654
+ PRIORITY: standard
655
+ WEIGHT_KG: 5.0
656
+ CREATED_AT: 2025-11-14 01:58:58
657
+
658
+ ---
659
+ STEP 18: Back to handle_create_order() - Success!
660
+
661
+ FILE: chat/tools.py
662
+ LINE: 224-242
663
+
664
+ execute_write(query, params) # Returned 1 (success)
665
+
666
+ logger.info(f"Order created: {order_id}")
667
+ # ↑
668
+ # LOGS: "Order created: ORD-20251114015858"
669
+
670
+ return {
671
+ "success": True,
672
+ "order_id": "ORD-20251114015858",
673
+ "status": "pending",
674
+ "customer": "John Doe",
675
+ "address": "123 Main St, San Francisco",
676
+ "deadline": "2025-11-14 07:58",
677
+ "priority": "standard",
678
+ "message": "Order ORD-20251114015858 created successfully!"
679
+ }
680
+
681
+ ---
682
+ STEP 19: Back to _process_response() - Second Tool Complete
683
+
684
+ FILE: chat/providers/gemini_provider.py
685
+ LINE: 285-310
686
+
687
+ tool_result = {
688
+ "success": True,
689
+ "order_id": "ORD-20251114015858",
690
+ "status": "pending",
691
+ "customer": "John Doe",
692
+ ...
693
+ }
694
+
695
+ # Track for transparency
696
+ tool_calls_made.append({
697
+ "tool": "create_order",
698
+ "input": {
699
+ "customer_name": "John Doe",
700
+ "delivery_address": "123 Main St, San Francisco",
701
+ "delivery_lat": 37.7749,
702
+ "delivery_lng": -122.4194
703
+ },
704
+ "result": tool_result
705
+ })
706
+
707
+ conversation.add_tool_result("create_order", function_args,
708
+ tool_result)
709
+
710
+ # Send function result back to Gemini
711
+ current_response = chat.send_message(
712
+ genai.protos.Content(
713
+ parts=[genai.protos.Part(
714
+ function_response=genai.protos.FunctionResponse(
715
+ name="create_order",
716
+ response={"result": tool_result}
717
+ )
718
+ )]
719
+ )
720
+ )
721
+ # ↑
722
+ # 🌐 API CALL TO GEMINI WITH ORDER CREATION RESULT
723
+
724
+ ---
725
+ STEP 20: Gemini Generates Final Response
726
+
727
+ 🌐 GOOGLE GEMINI API (External)
728
+
729
+ RECEIVES:
730
+ - Function: create_order
731
+ - Result: {
732
+ "success": True,
733
+ "order_id": "ORD-20251114015858",
734
+ "customer": "John Doe",
735
+ "address": "123 Main St, San Francisco"
736
+ }
737
+
738
+ AI ANALYZES:
739
+ "Perfect! Order successfully created in database!
740
+ Let me tell the user with a nice summary."
741
+
742
+ GENERATES TEXT RESPONSE:
743
+ "Awesome! I have created the order for John Doe.
744
+
745
+ **Here's a summary:**
746
+ β€’ Order ID: ORD-20251114015858
747
+ β€’ Customer: John Doe
748
+ β€’ Address: 123 Main St, San Francisco
749
+ β€’ Status: Pending
750
+ β€’ Priority: Standard
751
+
752
+ The order has been successfully saved to the database!"
753
+
754
+ RETURNS TO CODE:
755
+ response = {
756
+ candidates: [{
757
+ content: {
758
+ parts: [{
759
+ text: "Awesome! I have created the order for John
760
+ Doe.\n\n**Here's a summary:**..."
761
+ }]
762
+ }
763
+ }]
764
+ }
765
+
766
+ ---
767
+ STEP 21: _process_response() - Extract Final Text
768
+
769
+ FILE: chat/providers/gemini_provider.py
770
+ LINE: 272-356
771
+
772
+ # Loop detects no more function calls
773
+ logger.info(f"No more function calls after iteration 2")
774
+
775
+ # Extract text from final response
776
+ parts = current_response.candidates[0].content.parts
777
+ logger.info(f"Extracting text from {len(parts)} parts")
778
+
779
+ for idx, part in enumerate(parts):
780
+ if hasattr(part, 'text') and part.text:
781
+ logger.info(f"Part {idx} has text: {part.text[:50]}...")
782
+ final_text += part.text
783
+
784
+ # final_text = "Awesome! I have created the order for John Doe..."
785
+
786
+ logger.info(f"Returning response: {final_text[:100]}")
787
+
788
+ conversation.add_message("assistant", final_text)
789
+
790
+ return final_text, tool_calls_made
791
+ # ↑ ↑
792
+ # Response List of 2 tool calls [geocode, create_order]
793
+
794
+ RETURNS:
795
+ (
796
+ "Awesome! I have created the order for John Doe.\n\n**Here's a
797
+ summary:**...",
798
+ [
799
+ {"tool": "geocode_address", "input": {...}, "result":
800
+ {...}},
801
+ {"tool": "create_order", "input": {...}, "result": {...}}
802
+ ]
803
+ )
804
+
805
+ ---
806
+ STEP 22: Back Through All Functions
807
+
808
+ ← Returns to: GeminiProvider.process_message() (line 206)
809
+ ← Returns to: ChatEngine.process_message() (line 58)
810
+ ← Returns to: handle_chat_message() (line 223)
811
+ ← Returns to: send_message() (line 443)
812
+ ← Returns to: Gradio UI (line 448)
813
+
814
+ ---
815
+ STEP 23: Gradio Updates UI
816
+
817
+ FILE: ui/app.py
818
+ LINE: 448-452
819
+
820
+ send_btn.click(
821
+ fn=send_message,
822
+ outputs=[chatbot, tool_display, conversation_state, msg_input]
823
+ # ↑ ↑
824
+ # Updates Shows tool calls
825
+ )
826
+
827
+ CHATBOT DISPLAYS:
828
+ User: "Create an order for John Doe at 123 Main St, San Francisco"
generate_api_key.py DELETED
@@ -1,142 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- FleetMind API Key Generator
4
-
5
- Generate API keys for users to authenticate with FleetMind MCP Server.
6
-
7
- Usage:
8
- python generate_api_key.py
9
- python generate_api_key.py --email user@example.com --name "John Doe"
10
- python generate_api_key.py --list
11
- python generate_api_key.py --revoke user@example.com
12
- """
13
-
14
- import argparse
15
- import sys
16
- from database.api_keys import generate_api_key, list_api_keys, revoke_api_key
17
-
18
- def main():
19
- parser = argparse.ArgumentParser(
20
- description='FleetMind API Key Management',
21
- formatter_class=argparse.RawDescriptionHelpFormatter,
22
- epilog="""
23
- Examples:
24
- # Interactive mode (prompts for email and name)
25
- python generate_api_key.py
26
-
27
- # Generate key with arguments
28
- python generate_api_key.py --email alice@company.com --name "Alice Smith"
29
-
30
- # List all API keys
31
- python generate_api_key.py --list
32
-
33
- # Revoke a key
34
- python generate_api_key.py --revoke alice@company.com
35
- """
36
- )
37
-
38
- parser.add_argument('--email', help='User email address')
39
- parser.add_argument('--name', help='User display name')
40
- parser.add_argument('--list', action='store_true', help='List all API keys')
41
- parser.add_argument('--revoke', metavar='EMAIL', help='Revoke API key for email')
42
-
43
- args = parser.parse_args()
44
-
45
- # List API keys
46
- if args.list:
47
- print("\n" + "="*80)
48
- print("FLEETMIND API KEYS")
49
- print("="*80 + "\n")
50
-
51
- keys = list_api_keys()
52
- if not keys:
53
- print("No API keys found.\n")
54
- return
55
-
56
- for key in keys:
57
- status = "βœ… Active" if key['is_active'] else "❌ Revoked"
58
- print(f"Email: {key['email']}")
59
- print(f"Name: {key['name']}")
60
- print(f"User ID: {key['user_id']}")
61
- print(f"Key: {key['key_preview']}")
62
- print(f"Created: {key['created_at']}")
63
- print(f"Last Used: {key['last_used_at'] or 'Never'}")
64
- print(f"Status: {status}")
65
- print("-" * 80)
66
-
67
- print()
68
- return
69
-
70
- # Revoke API key
71
- if args.revoke:
72
- print(f"\n⚠️ Revoking API key for {args.revoke}...")
73
- result = revoke_api_key(args.revoke)
74
-
75
- if result['success']:
76
- print(f"βœ… {result['message']}\n")
77
- else:
78
- print(f"❌ Error: {result['error']}\n")
79
- sys.exit(1)
80
- return
81
-
82
- # Generate new API key
83
- if not args.email:
84
- # Interactive mode
85
- print("\n" + "="*80)
86
- print("GENERATE NEW FLEETMIND API KEY")
87
- print("="*80 + "\n")
88
-
89
- email = input("Enter user email: ").strip()
90
- if not email:
91
- print("❌ Email is required")
92
- sys.exit(1)
93
-
94
- name = input("Enter user name (optional): ").strip() or None
95
- else:
96
- email = args.email
97
- name = args.name
98
-
99
- print(f"\nGenerating API key for {email}...")
100
- result = generate_api_key(email, name)
101
-
102
- if not result['success']:
103
- print(f"\n❌ Error: {result['error']}\n")
104
- sys.exit(1)
105
-
106
- # Success! Display the API key
107
- print("\n" + "="*80)
108
- print("βœ… API KEY GENERATED SUCCESSFULLY")
109
- print("="*80 + "\n")
110
-
111
- print(f"Email: {result['email']}")
112
- print(f"Name: {result['name']}")
113
- print(f"User ID: {result['user_id']}")
114
- print(f"Created: {result['created_at']}")
115
- print()
116
- print("πŸ”‘ API KEY (copy this now - it won't be shown again!):")
117
- print("-" * 80)
118
- print(result['api_key'])
119
- print("-" * 80)
120
- print()
121
- print("πŸ“‹ Add this to your Claude Desktop config:")
122
- print()
123
- print(' {')
124
- print(' "mcpServers": {')
125
- print(' "fleetmind": {')
126
- print(' "command": "npx",')
127
- print(' "args": [')
128
- print(' "mcp-remote",')
129
- print(' "https://mcp-1st-birthday-fleetmind-dispatch-ai.hf.space/sse"')
130
- print(' ],')
131
- print(' "env": {')
132
- print(f' "FLEETMIND_API_KEY": "{result["api_key"]}"')
133
- print(' }')
134
- print(' }')
135
- print(' }')
136
- print(' }')
137
- print()
138
- print("⚠️ IMPORTANT: Save this API key securely. You won't be able to see it again!")
139
- print()
140
-
141
- if __name__ == "__main__":
142
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
mcp_config.json DELETED
@@ -1,30 +0,0 @@
1
- {
2
- "name": "fleetmind",
3
- "version": "1.0.0",
4
- "description": "FleetMind Dispatch Coordinator - AI-powered delivery dispatch management system",
5
- "author": "FleetMind Team",
6
- "license": "MIT",
7
- "server": {
8
- "command": "python",
9
- "args": ["server.py"],
10
- "env": {
11
- "GOOGLE_MAPS_API_KEY": "${GOOGLE_MAPS_API_KEY}",
12
- "DB_HOST": "${DB_HOST}",
13
- "DB_PORT": "${DB_PORT}",
14
- "DB_NAME": "${DB_NAME}",
15
- "DB_USER": "${DB_USER}",
16
- "DB_PASSWORD": "${DB_PASSWORD}"
17
- }
18
- },
19
- "capabilities": {
20
- "tools": 18,
21
- "resources": 2,
22
- "prompts": 3
23
- },
24
- "categories": [
25
- "logistics",
26
- "dispatch",
27
- "delivery",
28
- "fleet-management"
29
- ]
30
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
proxy.py DELETED
@@ -1,244 +0,0 @@
1
- """
2
- FleetMind MCP Authentication Proxy
3
- Captures API keys from initial SSE connections and injects them into tool requests.
4
-
5
- This proxy sits between MCP clients and the FastMCP server, solving the
6
- multi-tenant authentication problem by:
7
- 1. Capturing api_key from initial /sse?api_key=xxx connection
8
- 2. Storing api_key mapped to session_id
9
- 3. Injecting api_key into subsequent /messages/?session_id=xxx requests
10
-
11
- Architecture:
12
- MCP Client -> Proxy (port 7860) -> FastMCP (port 7861)
13
- """
14
-
15
- import asyncio
16
- import logging
17
- import os
18
- from aiohttp import web, ClientSession, ClientTimeout
19
- from aiohttp.client_exceptions import ClientConnectionResetError
20
- from urllib.parse import urlencode, parse_qs, urlparse, urlunparse
21
- import sys
22
-
23
- # Configure logging
24
- logging.basicConfig(
25
- level=logging.INFO,
26
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
27
- handlers=[logging.StreamHandler()]
28
- )
29
- logger = logging.getLogger(__name__)
30
-
31
- # Proxy configuration
32
- # On HuggingFace, PORT env var is set to 7860
33
- PROXY_PORT = int(os.getenv("PORT", 7860)) # Public-facing port
34
- FASTMCP_PORT = 7861 # Internal FastMCP server port (fixed)
35
- FASTMCP_HOST = "localhost"
36
-
37
- # Session storage: session_id -> api_key
38
- session_api_keys = {}
39
-
40
-
41
- async def proxy_handler(request):
42
- """
43
- Main proxy handler - forwards all requests to FastMCP server.
44
- Captures API keys from SSE connections and injects them into tool calls.
45
- """
46
- path = request.path
47
- query_params = dict(request.query)
48
-
49
- # Extract API key if present (initial SSE connection)
50
- api_key = query_params.get('api_key')
51
- session_id = query_params.get('session_id')
52
-
53
- # STEP 1: Capture API key from initial SSE connection
54
- if api_key and path == '/sse':
55
- logger.info(f"[AUTH] Captured API key from SSE connection: {api_key[:20]}...")
56
- # Store temporarily - will be linked to session when we see it
57
- session_api_keys['_pending_api_key'] = api_key
58
-
59
- # STEP 2: Link session_id to API key (from /messages requests)
60
- if session_id and path.startswith('/messages'):
61
- # Check if we have a stored API key for this session
62
- if session_id not in session_api_keys:
63
- # Link this session to the pending API key
64
- if '_pending_api_key' in session_api_keys:
65
- api_key_to_store = session_api_keys['_pending_api_key']
66
- session_api_keys[session_id] = api_key_to_store
67
- logger.info(f"[AUTH] Linked session {session_id[:12]}... to API key")
68
-
69
- # STEP 3: Inject API key into request for FastMCP
70
- stored_api_key = session_api_keys.get(session_id)
71
- if stored_api_key:
72
- query_params['api_key'] = stored_api_key
73
- logger.debug(f"[AUTH] Injected API key into request for session {session_id[:12]}...")
74
-
75
- # Build target URL for FastMCP server
76
- query_string = urlencode(query_params) if query_params else ""
77
- target_url = f"http://{FASTMCP_HOST}:{FASTMCP_PORT}{path}"
78
- if query_string:
79
- target_url += f"?{query_string}"
80
-
81
- # Forward request to FastMCP
82
- # For SSE connections: total=None disables overall timeout (keeps connection alive)
83
- # Still use socket timeouts for safety (sock_connect, sock_read)
84
- async with ClientSession(
85
- timeout=ClientTimeout(
86
- total=None, # No total timeout for long-lived SSE connections
87
- sock_connect=30, # 30 seconds for initial connection
88
- sock_read=300 # 5 minutes for individual socket reads
89
- )
90
- ) as session:
91
- try:
92
- # Copy headers
93
- headers = dict(request.headers)
94
- # Remove host header to avoid conflicts
95
- headers.pop('Host', None)
96
-
97
- # Forward request based on method
98
- if request.method == 'GET':
99
- async with session.get(target_url, headers=headers) as resp:
100
- # For SSE, stream the response
101
- if 'text/event-stream' in resp.content_type:
102
- # Create streaming response for SSE
103
- response = web.StreamResponse(
104
- status=resp.status,
105
- reason=resp.reason,
106
- headers=dict(resp.headers)
107
- )
108
- await response.prepare(request)
109
-
110
- # Background task to send keep-alive pings (prevents timeout)
111
- async def send_keepalive():
112
- try:
113
- while True:
114
- await asyncio.sleep(30) # Send ping every 30 seconds
115
- await response.write(b":\n\n") # SSE comment (ignored by client)
116
- except asyncio.CancelledError:
117
- pass
118
-
119
- keepalive_task = asyncio.create_task(send_keepalive())
120
-
121
- try:
122
- # Stream chunks from FastMCP to client
123
- async for chunk in resp.content.iter_any():
124
- await response.write(chunk)
125
-
126
- await response.write_eof()
127
- finally:
128
- # Cancel keep-alive task when streaming completes
129
- keepalive_task.cancel()
130
- try:
131
- await keepalive_task
132
- except asyncio.CancelledError:
133
- pass
134
-
135
- return response
136
- else:
137
- # For regular responses, read entire body
138
- body = await resp.read()
139
- resp_headers = dict(resp.headers)
140
- return web.Response(
141
- body=body,
142
- status=resp.status,
143
- headers=resp_headers
144
- )
145
-
146
- elif request.method == 'POST':
147
- body = await request.read()
148
- async with session.post(target_url, data=body, headers=headers) as resp:
149
- resp_body = await resp.read()
150
- # Don't pass content_type separately - it's already in headers
151
- resp_headers = dict(resp.headers)
152
- return web.Response(
153
- body=resp_body,
154
- status=resp.status,
155
- headers=resp_headers
156
- )
157
-
158
- else:
159
- # Forward other methods
160
- async with session.request(
161
- request.method,
162
- target_url,
163
- data=await request.read(),
164
- headers=headers
165
- ) as resp:
166
- body = await resp.read()
167
- return web.Response(
168
- body=body,
169
- status=resp.status,
170
- headers=dict(resp.headers)
171
- )
172
-
173
- except (ClientConnectionResetError, ConnectionResetError) as e:
174
- # Client disconnected - this is normal for SSE connections
175
- # Log at DEBUG level to reduce noise
176
- logger.debug(f"[SSE] Client disconnected: {e}")
177
- return web.Response(text="Client disconnected", status=499)
178
-
179
- except Exception as e:
180
- import traceback
181
- error_details = traceback.format_exc()
182
- logger.error(f"[ERROR] Proxy error: {type(e).__name__}: {e}")
183
- logger.error(f"[ERROR] Traceback:\n{error_details}")
184
- return web.Response(
185
- text=f"Proxy error: {type(e).__name__}: {str(e)}",
186
- status=502
187
- )
188
-
189
-
190
- async def health_check(request):
191
- """Health check endpoint"""
192
- return web.Response(text="FleetMind Proxy OK", status=200)
193
-
194
-
195
- def create_app():
196
- """Create and configure the proxy application"""
197
- app = web.Application()
198
-
199
- # Health check endpoint
200
- app.router.add_get('/health', health_check)
201
-
202
- # Proxy all other requests
203
- app.router.add_route('*', '/{path:.*}', proxy_handler)
204
-
205
- return app
206
-
207
-
208
- async def main():
209
- """Start the proxy server"""
210
- print("\n" + "=" * 70)
211
- print("FleetMind MCP Authentication Proxy")
212
- print("=" * 70)
213
- print(f"Proxy listening on: http://0.0.0.0:{PROXY_PORT}")
214
- print(f"Forwarding to FastMCP: http://{FASTMCP_HOST}:{FASTMCP_PORT}")
215
- print("=" * 70)
216
- print("[OK] Multi-tenant authentication enabled")
217
- print("[OK] API keys captured from SSE connections")
218
- print("[OK] Sessions automatically linked to API keys")
219
- print("=" * 70 + "\n")
220
-
221
- app = create_app()
222
- runner = web.AppRunner(app)
223
- await runner.setup()
224
-
225
- site = web.TCPSite(runner, '0.0.0.0', PROXY_PORT)
226
- await site.start()
227
-
228
- logger.info(f"[OK] Proxy server started on port {PROXY_PORT}")
229
- logger.info(f"[OK] Forwarding to FastMCP on {FASTMCP_HOST}:{FASTMCP_PORT}")
230
-
231
- # Keep running
232
- try:
233
- await asyncio.Event().wait()
234
- except KeyboardInterrupt:
235
- logger.info("Shutting down proxy server...")
236
- await runner.cleanup()
237
-
238
-
239
- if __name__ == "__main__":
240
- try:
241
- asyncio.run(main())
242
- except KeyboardInterrupt:
243
- print("\nProxy server stopped.")
244
- sys.exit(0)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
pyproject.toml DELETED
@@ -1,69 +0,0 @@
1
- [project]
2
- name = "fleetmind-mcp"
3
- version = "1.0.0"
4
- description = "FleetMind Dispatch Coordinator MCP Server - AI-powered delivery management"
5
- readme = "README.md"
6
- requires-python = ">=3.10"
7
- license = {text = "MIT"}
8
- authors = [
9
- {name = "FleetMind Team"}
10
- ]
11
- keywords = ["mcp", "dispatch", "delivery", "logistics", "ai", "anthropic"]
12
- classifiers = [
13
- "Development Status :: 4 - Beta",
14
- "Intended Audience :: Developers",
15
- "License :: OSI Approved :: MIT License",
16
- "Programming Language :: Python :: 3",
17
- "Programming Language :: Python :: 3.10",
18
- "Programming Language :: Python :: 3.11",
19
- "Programming Language :: Python :: 3.12",
20
- ]
21
-
22
- dependencies = [
23
- "fastmcp>=0.3.0",
24
- "psycopg2-binary>=2.9.9",
25
- "googlemaps>=4.10.0",
26
- "python-dotenv>=1.0.0",
27
- "pydantic>=2.8.2"
28
- ]
29
-
30
- [project.optional-dependencies]
31
- dev = [
32
- "pytest>=8.0.0",
33
- "pytest-asyncio>=0.23.0",
34
- "mypy>=1.8.0",
35
- "black>=24.0.0",
36
- "ruff>=0.1.0"
37
- ]
38
-
39
- [project.scripts]
40
- fleetmind-mcp = "server:main"
41
-
42
- [project.urls]
43
- Homepage = "https://github.com/your-org/fleetmind-mcp"
44
- Repository = "https://github.com/your-org/fleetmind-mcp"
45
- Issues = "https://github.com/your-org/fleetmind-mcp/issues"
46
-
47
- [build-system]
48
- requires = ["setuptools>=65.0", "wheel"]
49
- build-backend = "setuptools.build_meta"
50
-
51
- [tool.pytest.ini_options]
52
- testpaths = ["tests"]
53
- python_files = "test_*.py"
54
- python_functions = "test_*"
55
- addopts = "-v --strict-markers"
56
-
57
- [tool.mypy]
58
- python_version = "3.10"
59
- warn_return_any = true
60
- warn_unused_configs = true
61
- disallow_untyped_defs = false
62
-
63
- [tool.black]
64
- line-length = 100
65
- target-version = ['py310', 'py311', 'py312']
66
-
67
- [tool.ruff]
68
- line-length = 100
69
- target-version = "py310"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
requirements.txt CHANGED
@@ -1,33 +1,27 @@
1
- # ============================================================================
2
- # FleetMind MCP Server - HuggingFace Space (Track 1)
3
- # MCP-1st-Birthday Hackathon - Building MCP Servers
4
- # ============================================================================
5
-
6
- # Core MCP Framework
7
  fastmcp>=0.3.0
8
- pydantic>=2.8.2
9
-
10
- # Web Framework (for landing page)
11
- fastapi>=0.104.0
12
- uvicorn>=0.24.0
13
 
14
- # Authentication Proxy
15
- aiohttp>=3.9.0
 
16
 
17
- # Database
 
 
18
  psycopg2-binary>=2.9.9
19
 
20
  # API Clients
21
- googlemaps>=4.10.0
22
- google-generativeai>=0.8.0
23
  requests>=2.31.0
 
24
 
25
  # Utilities
26
  python-dotenv>=1.0.0
 
 
 
 
 
27
 
28
- # Development & Testing (optional - not needed for HF Space deployment)
29
- # pytest>=8.0.0
30
- # pytest-asyncio>=0.23.0
31
- # mypy>=1.8.0
32
- # black>=24.0.0
33
- # ruff>=0.1.0
 
1
+ # Core Framework
2
+ gradio>=5.9.0
 
 
 
 
3
  fastmcp>=0.3.0
 
 
 
 
 
4
 
5
+ # AI/ML
6
+ anthropic>=0.40.0
7
+ google-generativeai>=0.3.0
8
 
9
+ # Data & Database
10
+ pandas>=2.2.0
11
+ faker>=23.0.0
12
  psycopg2-binary>=2.9.9
13
 
14
  # API Clients
 
 
15
  requests>=2.31.0
16
+ httpx>=0.27.1
17
 
18
  # Utilities
19
  python-dotenv>=1.0.0
20
+ pydantic>=2.5.3
21
+
22
+ # Testing
23
+ pytest>=8.0.0
24
+ pytest-asyncio>=0.23.0
25
 
26
+ # Type Checking
27
+ mypy>=1.8.0
 
 
 
 
scripts/test_db.py ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Test script to verify PostgreSQL database operations
3
+ """
4
+
5
+ import sys
6
+ from pathlib import Path
7
+ from datetime import datetime, timedelta
8
+
9
+ # Add parent directory to path
10
+ sys.path.insert(0, str(Path(__file__).parent.parent))
11
+
12
+ from database.connection import execute_query, execute_write
13
+ import logging
14
+
15
+ logging.basicConfig(level=logging.INFO)
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ def test_insert_order():
20
+ """Test inserting a new order"""
21
+ logger.info("Testing order insertion...")
22
+
23
+ now = datetime.now()
24
+ time_window_start = now + timedelta(hours=2)
25
+ time_window_end = now + timedelta(hours=6)
26
+
27
+ query = """
28
+ INSERT INTO orders (
29
+ order_id, customer_name, customer_phone, customer_email,
30
+ delivery_address, delivery_lat, delivery_lng,
31
+ time_window_start, time_window_end,
32
+ priority, weight_kg, status
33
+ ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
34
+ """
35
+
36
+ params = (
37
+ "ORD-TEST-001",
38
+ "John Doe",
39
+ "+1-555-0123",
40
+ "john.doe@example.com",
41
+ "123 Main Street, San Francisco, CA 94103",
42
+ 37.7749,
43
+ -122.4194,
44
+ time_window_start,
45
+ time_window_end,
46
+ "standard",
47
+ 5.5,
48
+ "pending"
49
+ )
50
+
51
+ try:
52
+ result = execute_write(query, params)
53
+ logger.info(f"βœ“ Order inserted successfully (rows affected: {result})")
54
+ return True
55
+ except Exception as e:
56
+ logger.error(f"βœ— Failed to insert order: {e}")
57
+ return False
58
+
59
+
60
+ def test_query_orders():
61
+ """Test querying orders"""
62
+ logger.info("Testing order query...")
63
+
64
+ query = "SELECT * FROM orders WHERE status = %s"
65
+ params = ("pending",)
66
+
67
+ try:
68
+ results = execute_query(query, params)
69
+ logger.info(f"βœ“ Query successful: Found {len(results)} pending orders")
70
+
71
+ for row in results:
72
+ logger.info(f" Order ID: {row['order_id']}")
73
+ logger.info(f" Customer: {row['customer_name']}")
74
+ logger.info(f" Address: {row['delivery_address']}")
75
+ logger.info(f" Priority: {row['priority']}")
76
+ logger.info(f" Status: {row['status']}")
77
+ logger.info(" ---")
78
+
79
+ return True
80
+ except Exception as e:
81
+ logger.error(f"βœ— Failed to query orders: {e}")
82
+ return False
83
+
84
+
85
+ def test_update_order():
86
+ """Test updating an order"""
87
+ logger.info("Testing order update...")
88
+
89
+ query = "UPDATE orders SET status = %s, assigned_driver_id = %s WHERE order_id = %s"
90
+ params = ("assigned", "DRV-001", "ORD-TEST-001")
91
+
92
+ try:
93
+ result = execute_write(query, params)
94
+ logger.info(f"βœ“ Order updated successfully (rows affected: {result})")
95
+
96
+ # Verify update
97
+ verify_query = "SELECT status, assigned_driver_id FROM orders WHERE order_id = %s"
98
+ verify_result = execute_query(verify_query, ("ORD-TEST-001",))
99
+
100
+ if verify_result:
101
+ row = verify_result[0]
102
+ logger.info(f" New status: {row['status']}")
103
+ logger.info(f" Assigned driver: {row['assigned_driver_id']}")
104
+
105
+ return True
106
+ except Exception as e:
107
+ logger.error(f"βœ— Failed to update order: {e}")
108
+ return False
109
+
110
+
111
+ def test_delete_order():
112
+ """Test deleting the test order"""
113
+ logger.info("Testing order deletion (cleanup)...")
114
+
115
+ query = "DELETE FROM orders WHERE order_id = %s"
116
+ params = ("ORD-TEST-001",)
117
+
118
+ try:
119
+ result = execute_write(query, params)
120
+ logger.info(f"βœ“ Order deleted successfully (rows affected: {result})")
121
+ return True
122
+ except Exception as e:
123
+ logger.error(f"βœ— Failed to delete order: {e}")
124
+ return False
125
+
126
+
127
+ def main():
128
+ """Run all database tests"""
129
+ logger.info("=" * 50)
130
+ logger.info("Starting FleetMind PostgreSQL Database Tests")
131
+ logger.info("=" * 50)
132
+
133
+ tests = [
134
+ ("Insert Order", test_insert_order),
135
+ ("Query Orders", test_query_orders),
136
+ ("Update Order", test_update_order),
137
+ ("Delete Order", test_delete_order),
138
+ ]
139
+
140
+ results = []
141
+ for test_name, test_func in tests:
142
+ logger.info(f"\n--- {test_name} ---")
143
+ success = test_func()
144
+ results.append((test_name, success))
145
+
146
+ # Summary
147
+ logger.info("\n" + "=" * 50)
148
+ logger.info("Test Summary")
149
+ logger.info("=" * 50)
150
+
151
+ passed = sum(1 for _, success in results if success)
152
+ total = len(results)
153
+
154
+ for test_name, success in results:
155
+ status = "βœ“ PASSED" if success else "βœ— FAILED"
156
+ logger.info(f"{test_name}: {status}")
157
+
158
+ logger.info(f"\nTotal: {passed}/{total} tests passed")
159
+
160
+ if passed == total:
161
+ logger.info("\nπŸŽ‰ All tests passed! Your PostgreSQL database is working correctly!")
162
+ return 0
163
+ else:
164
+ logger.error("\n❌ Some tests failed. Please check the errors above.")
165
+ return 1
166
+
167
+
168
+ if __name__ == "__main__":
169
+ sys.exit(main())
server.py DELETED
@@ -1,2033 +0,0 @@
1
- """
2
- FleetMind Dispatch Coordinator - MCP Server
3
- Industry-standard Model Context Protocol server for delivery dispatch management
4
-
5
- Provides 18 AI tools for order and driver management via standardized MCP protocol.
6
- Compatible with Claude Desktop, Continue, Cline, and all MCP clients.
7
- """
8
-
9
- import os
10
- import sys
11
- import json
12
- import logging
13
- from pathlib import Path
14
- from typing import Literal
15
- from datetime import datetime
16
- from contextvars import ContextVar
17
-
18
- # Add project root to path
19
- sys.path.insert(0, str(Path(__file__).parent))
20
-
21
- # Ensure logs directory exists
22
- logs_dir = Path(__file__).parent / 'logs'
23
- logs_dir.mkdir(exist_ok=True)
24
-
25
- from fastmcp import FastMCP
26
-
27
- # Import existing services (unchanged)
28
- from chat.geocoding import GeocodingService
29
- from database.connection import execute_query, execute_write, test_connection
30
-
31
- # Import permission checking
32
- from database.user_context import check_permission, get_required_scope
33
-
34
- # Configure logging
35
- logging.basicConfig(
36
- level=logging.INFO,
37
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
38
- handlers=[
39
- logging.FileHandler(logs_dir / 'fleetmind_mcp.log'),
40
- logging.StreamHandler()
41
- ]
42
- )
43
- logger = logging.getLogger(__name__)
44
-
45
- # ============================================================================
46
- # API KEY EXTRACTION FROM REQUEST
47
- # ============================================================================
48
-
49
- # Store API key per request using context variable
50
- _current_api_key: ContextVar[str] = ContextVar('api_key', default=None)
51
-
52
- # Session-to-API-key store: maps session_id -> api_key
53
- # This allows tool calls to authenticate using the API key from the SSE connection
54
- _session_auth_store: dict[str, str] = {}
55
-
56
- def store_session_api_key(session_id: str, api_key: str):
57
- """Store API key for a session (called when SSE connection is made)"""
58
- _session_auth_store[session_id] = api_key
59
- logger.debug(f"Stored API key for session {session_id[:16]}...")
60
-
61
- def get_api_key_from_session(session_id: str) -> str:
62
- """Get API key from session store"""
63
- return _session_auth_store.get(session_id)
64
-
65
- def cleanup_session(session_id: str):
66
- """Remove session from store when disconnected"""
67
- if session_id in _session_auth_store:
68
- del _session_auth_store[session_id]
69
- logger.debug(f"Cleaned up session {session_id[:16]}...")
70
-
71
- def get_api_key_from_context() -> str:
72
- """
73
- Get API key from the current request context.
74
- This is set by our custom dependencies.
75
- """
76
- return _current_api_key.get(None)
77
-
78
- def extract_api_key_from_request():
79
- """
80
- Extract API key from HTTP request and store in context variable.
81
- Called at the start of each tool execution.
82
-
83
- Checks multiple sources:
84
- 1. api_key query parameter in current request
85
- 2. session_id query parameter -> lookup in session store
86
- """
87
- try:
88
- from fastmcp.server.dependencies import get_http_request
89
- request = get_http_request()
90
-
91
- # METHOD 1: Direct api_key in query params
92
- api_key = request.query_params.get('api_key')
93
- if api_key:
94
- _current_api_key.set(api_key)
95
- logger.debug(f"API key extracted from request: {api_key[:10]}...")
96
- return api_key
97
-
98
- # METHOD 2: Look up by session_id (for MCP tool calls)
99
- session_id = request.query_params.get('session_id')
100
- if session_id:
101
- api_key = get_api_key_from_session(session_id)
102
- if api_key:
103
- _current_api_key.set(api_key)
104
- logger.debug(f"API key found via session {session_id[:16]}...")
105
- return api_key
106
- else:
107
- logger.debug(f"No API key found for session {session_id[:16]}...")
108
-
109
- except RuntimeError:
110
- # No HTTP request available (e.g., stdio transport)
111
- logger.debug("No HTTP request available for API key extraction")
112
- return None
113
-
114
- # ============================================================================
115
- # MCP SERVER INITIALIZATION
116
- # ============================================================================
117
- mcp = FastMCP(
118
- name="FleetMind Dispatch Coordinator",
119
- version="1.0.0"
120
- # Note: request_timeout not supported in FastMCP 2.13.1
121
- # Timeout handling is done in proxy.py instead
122
- )
123
-
124
- # ============================================================================
125
- # FASTMCP AUTHENTICATION MIDDLEWARE
126
- # Uses FastMCP's native middleware system to properly pass user context to tools
127
- # Reference: https://gelembjuk.com/blog/post/authentication-remote-mcp-server-python/
128
- # ============================================================================
129
- try:
130
- from fastmcp.server.middleware import Middleware, MiddlewareContext
131
- from fastmcp.exceptions import ToolError
132
-
133
- class ApiKeyAuthMiddleware(Middleware):
134
- """
135
- FastMCP middleware for API key authentication.
136
- Validates API key from request query params and injects user info into context.
137
- """
138
-
139
- async def on_call_tool(self, context: MiddlewareContext, call_next):
140
- """Intercept tool calls to validate authentication"""
141
- try:
142
- from fastmcp.server.dependencies import get_http_request
143
- request = get_http_request()
144
-
145
- # Get API key from query params
146
- api_key = request.query_params.get('api_key')
147
-
148
- # Also check session store if no direct api_key
149
- if not api_key:
150
- session_id = request.query_params.get('session_id')
151
- if session_id:
152
- api_key = get_api_key_from_session(session_id)
153
-
154
- if api_key:
155
- # Validate API key and get user info
156
- from database.api_keys import verify_api_key
157
- user_info = verify_api_key(api_key)
158
-
159
- if user_info:
160
- # Store user info in context for tools to access
161
- context.fastmcp_context.set_state("user_id", user_info['user_id'])
162
- context.fastmcp_context.set_state("user_email", user_info['email'])
163
- context.fastmcp_context.set_state("user_scopes", user_info.get('scopes', []))
164
- context.fastmcp_context.set_state("user_name", user_info.get('name', ''))
165
- logger.info(f"βœ… Auth middleware: User {user_info['email']} authenticated")
166
- return await call_next(context)
167
- else:
168
- logger.warning(f"❌ Auth middleware: Invalid API key {api_key[:10]}...")
169
-
170
- # Check SKIP_AUTH for development
171
- env = os.getenv("ENV") or os.getenv("ENVIRONMENT", "production")
172
- skip_auth = os.getenv("SKIP_AUTH", "false").lower() == "true"
173
-
174
- if skip_auth and env.lower() != "production":
175
- # Development mode - use dev user
176
- context.fastmcp_context.set_state("user_id", "dev-user")
177
- context.fastmcp_context.set_state("user_email", "dev@fleetmind.local")
178
- context.fastmcp_context.set_state("user_scopes", ["admin"])
179
- context.fastmcp_context.set_state("user_name", "Development User")
180
- logger.warning(f"⚠️ Auth middleware: SKIP_AUTH enabled, using dev user")
181
- return await call_next(context)
182
-
183
- # No valid authentication
184
- raise ToolError("Authentication required. Please provide a valid API key.")
185
-
186
- except ImportError:
187
- # get_http_request not available (stdio transport)
188
- logger.debug("Auth middleware: No HTTP request available")
189
- return await call_next(context)
190
-
191
- # Register the middleware with FastMCP
192
- mcp.add_middleware(ApiKeyAuthMiddleware())
193
- logger.info("[Auth] FastMCP ApiKeyAuthMiddleware registered")
194
-
195
- except ImportError as e:
196
- logger.warning(f"[Auth] Could not import FastMCP middleware: {e}")
197
- logger.warning("[Auth] Falling back to per-tool authentication")
198
-
199
- # Initialize shared services
200
- logger.info("Initializing FleetMind MCP Server...")
201
- geocoding_service = GeocodingService()
202
- logger.info(f"Geocoding Service: {geocoding_service.get_status()}")
203
-
204
- # Test database connection
205
- try:
206
- test_connection()
207
- logger.info("Database: Connected to PostgreSQL")
208
- except Exception as e:
209
- logger.error(f"Database: Connection failed - {e}")
210
-
211
- # ============================================================================
212
- # AUTHENTICATION - API KEY SYSTEM
213
- # ============================================================================
214
-
215
- def get_authenticated_user(ctx=None):
216
- """
217
- Get authenticated user from multiple sources:
218
- 1. FastMCP Context state (set by middleware) - PREFERRED
219
- 2. HTTP request API key extraction (fallback)
220
- 3. Environment variable (fallback for testing)
221
- 4. SKIP_AUTH bypass (development only)
222
-
223
- Args:
224
- ctx: Optional FastMCP Context object from tool function
225
-
226
- Returns:
227
- User info dict with user_id, email, scopes, name or None if not authenticated
228
- """
229
- try:
230
- # METHOD 1: Get user from FastMCP Context state (set by middleware)
231
- # This is the preferred method when using FastMCP middleware
232
- if ctx is not None:
233
- try:
234
- user_id = ctx.get_state("user_id")
235
- if user_id:
236
- return {
237
- 'user_id': user_id,
238
- 'email': ctx.get_state("user_email") or "",
239
- 'scopes': ctx.get_state("user_scopes") or [],
240
- 'name': ctx.get_state("user_name") or ""
241
- }
242
- except Exception:
243
- pass # Context state not available, try other methods
244
-
245
- # METHOD 2: Extract API key from current HTTP request
246
- api_key = extract_api_key_from_request()
247
-
248
- # METHOD 3: Fallback to environment variable for testing
249
- if not api_key:
250
- api_key = os.getenv("FLEETMIND_API_KEY")
251
- if api_key:
252
- logger.debug("Using API key from environment")
253
-
254
- # Validate the API key
255
- if api_key:
256
- from database.api_keys import verify_api_key
257
- user_info = verify_api_key(api_key)
258
- if user_info:
259
- logger.info(f"βœ… Authenticated: {user_info['email']} (user_id: {user_info['user_id']})")
260
- return user_info
261
- else:
262
- logger.warning(f"❌ Invalid API key: {api_key[:10]}...")
263
- return None
264
-
265
- # METHOD 4: Development bypass mode (local testing only)
266
- # SECURITY: Only allow SKIP_AUTH in development environments
267
- # Check both ENV and ENVIRONMENT variables for compatibility
268
- env = os.getenv("ENV") or os.getenv("ENVIRONMENT", "production")
269
- env = env.lower()
270
- skip_auth = os.getenv("SKIP_AUTH", "false").lower() == "true"
271
-
272
- if skip_auth:
273
- if env == "production":
274
- logger.error("⚠️ SKIP_AUTH is enabled but ENV=production - DENYING access for security")
275
- return None
276
- else:
277
- logger.warning(f"⚠️ SKIP_AUTH enabled in {env} environment - using development user")
278
- return {
279
- 'user_id': 'dev-user',
280
- 'email': 'dev@fleetmind.local',
281
- 'scopes': ['admin'],
282
- 'name': 'Development User'
283
- }
284
-
285
- return None
286
-
287
- except Exception as e:
288
- logger.error(f"Authentication error: {e}")
289
- return None
290
-
291
- # ============================================================================
292
- # MCP RESOURCES
293
- # ============================================================================
294
-
295
- @mcp.resource("orders://all")
296
- def get_orders_resource() -> str:
297
- """
298
- Real-time orders dataset for AI context.
299
- Returns authenticated user's orders from the last 30 days.
300
-
301
- Returns:
302
- JSON string containing orders array with key fields:
303
- - order_id, customer_name, delivery_address
304
- - status, priority, created_at, assigned_driver_id
305
- """
306
- try:
307
- # Authenticate user
308
- user = get_authenticated_user()
309
- if not user:
310
- logger.warning("Resource orders://all - Authentication required")
311
- return json.dumps({
312
- "error": "Authentication required",
313
- "message": "Please provide a valid API key to access orders"
314
- })
315
-
316
- # Query only this user's orders
317
- query = """
318
- SELECT order_id, customer_name, delivery_address,
319
- status, priority, created_at, assigned_driver_id
320
- FROM orders
321
- WHERE user_id = %s
322
- AND created_at > NOW() - INTERVAL '30 days'
323
- ORDER BY created_at DESC
324
- LIMIT 1000
325
- """
326
- orders = execute_query(query, (user['user_id'],))
327
- logger.info(f"Resource orders://all - User {user['user_id']} retrieved {len(orders) if orders else 0} orders")
328
- return json.dumps(orders, default=str, indent=2)
329
- except Exception as e:
330
- logger.error(f"Resource orders://all failed: {e}")
331
- return json.dumps({"error": str(e)})
332
-
333
-
334
- @mcp.resource("drivers://all")
335
- def get_drivers_resource() -> str:
336
- """
337
- Real-time drivers dataset for AI context.
338
- Returns authenticated user's drivers with current locations and status.
339
-
340
- Returns:
341
- JSON string containing drivers array with key fields:
342
- - driver_id, name, status, vehicle_type, vehicle_plate
343
- - current_lat, current_lng, last_location_update
344
- """
345
- try:
346
- # Authenticate user
347
- user = get_authenticated_user()
348
- if not user:
349
- logger.warning("Resource drivers://all - Authentication required")
350
- return json.dumps({
351
- "error": "Authentication required",
352
- "message": "Please provide a valid API key to access drivers"
353
- })
354
-
355
- # Query only this user's drivers
356
- query = """
357
- SELECT driver_id, name, status, vehicle_type, vehicle_plate,
358
- current_lat, current_lng, last_location_update
359
- FROM drivers
360
- WHERE user_id = %s
361
- ORDER BY name ASC
362
- """
363
- drivers = execute_query(query, (user['user_id'],))
364
- logger.info(f"Resource drivers://all - User {user['user_id']} retrieved {len(drivers) if drivers else 0} drivers")
365
- return json.dumps(drivers, default=str, indent=2)
366
- except Exception as e:
367
- logger.error(f"Resource drivers://all failed: {e}")
368
- return json.dumps({"error": str(e)})
369
-
370
-
371
- # ============================================================================
372
- # MCP PROMPTS (Workflows)
373
- # ============================================================================
374
-
375
- # TODO: Add prompts once FastMCP prompt API is confirmed
376
- # Prompts will provide guided workflows for:
377
- # - create_order_workflow: Interactive order creation with validation
378
- # - assign_driver_workflow: Smart driver assignment with route optimization
379
- # - order_status_check: Quick order status queries
380
-
381
-
382
- # ============================================================================
383
- # MCP TOOLS - ORDER CREATION & VALIDATION
384
- # ============================================================================
385
-
386
- @mcp.tool()
387
- def geocode_address(address: str) -> dict:
388
- """
389
- Convert a delivery address to GPS coordinates and validate the address format.
390
- Use this before creating an order to ensure the address is valid.
391
-
392
- Args:
393
- address: The full delivery address to geocode (e.g., '123 Main St, San Francisco, CA')
394
-
395
- Returns:
396
- dict: {
397
- success: bool,
398
- latitude: float,
399
- longitude: float,
400
- formatted_address: str,
401
- confidence: str (high/medium/low),
402
- message: str
403
- }
404
- """
405
- from chat.tools import handle_geocode_address
406
- logger.info(f"Tool: geocode_address('{address}')")
407
- return handle_geocode_address({"address": address})
408
-
409
-
410
- @mcp.tool()
411
- def calculate_route(
412
- origin: str,
413
- destination: str,
414
- mode: Literal["driving", "walking", "bicycling", "transit"] = "driving",
415
- vehicle_type: Literal["car", "van", "truck", "motorcycle", "bicycle"] = "car",
416
- alternatives: bool = False,
417
- include_steps: bool = False,
418
- avoid_tolls: bool = False,
419
- avoid_highways: bool = False,
420
- avoid_ferries: bool = False,
421
- emission_type: Literal["GASOLINE", "ELECTRIC", "HYBRID", "DIESEL"] = "GASOLINE",
422
- request_fuel_efficient: bool = False
423
- ) -> dict:
424
- """
425
- Calculate the shortest route between two locations with vehicle-specific optimization.
426
- Uses Google Routes API for accurate real-time traffic, toll info, and fuel consumption.
427
-
428
- Args:
429
- origin: Starting location - either full address or coordinates as 'lat,lng'
430
- destination: Destination location - either full address or coordinates as 'lat,lng'
431
- mode: Travel mode for route calculation (default: driving)
432
- vehicle_type: Type of vehicle for route optimization (default: car)
433
- - motorcycle: Uses TWO_WHEELER mode for motorcycle-specific routing
434
- - bicycle: Uses bike lanes and paths
435
- - car/van/truck: Uses DRIVE mode (no truck-specific routing available)
436
- alternatives: Return multiple route options if available (default: false)
437
- include_steps: Include turn-by-turn navigation steps in response (default: false)
438
- avoid_tolls: Avoid toll roads (for cars and motorcycles) (default: false)
439
- avoid_highways: Avoid highways (for cars and motorcycles) (default: false)
440
- avoid_ferries: Avoid ferry routes (for cars and motorcycles) (default: false)
441
- emission_type: Vehicle emission type for eco-routing (cars/vans/trucks only) (default: GASOLINE)
442
- request_fuel_efficient: Request eco-friendly route alternative (cars/vans/trucks only) (default: false)
443
-
444
- Returns:
445
- dict: {
446
- success: bool,
447
- origin: str,
448
- destination: str,
449
- distance: {meters: int, text: str},
450
- duration: {seconds: int, text: str} (without traffic),
451
- duration_in_traffic: {seconds: int, text: str} (with traffic),
452
- traffic_delay: {seconds: int, text: str},
453
- mode: str,
454
- vehicle_type: str,
455
- route_summary: str,
456
- route_labels: list,
457
- confidence: str,
458
- toll_info: {has_tolls: bool, details: str} (if applicable),
459
- fuel_consumption: {liters: float, text: str} (if DRIVE mode),
460
- traffic_data_available: bool,
461
- warning: str (if TWO_WHEELER or BICYCLE mode),
462
- steps: list (if include_steps=True)
463
- }
464
- """
465
- from chat.tools import handle_calculate_route
466
- logger.info(f"Tool: calculate_route('{origin}' -> '{destination}', vehicle={vehicle_type}, mode={mode})")
467
- return handle_calculate_route({
468
- "origin": origin,
469
- "destination": destination,
470
- "mode": mode,
471
- "vehicle_type": vehicle_type,
472
- "alternatives": alternatives,
473
- "include_steps": include_steps,
474
- "avoid_tolls": avoid_tolls,
475
- "avoid_highways": avoid_highways,
476
- "avoid_ferries": avoid_ferries,
477
- "emission_type": emission_type,
478
- "request_fuel_efficient": request_fuel_efficient
479
- })
480
-
481
-
482
- @mcp.tool()
483
- def calculate_intelligent_route(
484
- origin: str,
485
- destination: str,
486
- vehicle_type: Literal["car", "van", "truck", "motorcycle"] = "car",
487
- consider_weather: bool = True,
488
- consider_traffic: bool = True
489
- ) -> dict:
490
- """
491
- Calculate the optimal route considering real-time traffic, weather conditions, and vehicle type.
492
- This is an intelligent routing tool that factors in:
493
- - Real-time traffic delays
494
- - Weather conditions (rain, visibility, wind)
495
- - Vehicle-specific capabilities (motorcycle vs car vs truck)
496
- - Safety warnings and recommendations
497
-
498
- Use this when you need smart routing that accounts for current conditions.
499
-
500
- Args:
501
- origin: Starting location - either full address or coordinates as 'lat,lng'
502
- destination: Destination location - either full address or coordinates as 'lat,lng'
503
- vehicle_type: Type of vehicle for route optimization (default: car)
504
- consider_weather: Factor in weather conditions (default: true)
505
- consider_traffic: Factor in real-time traffic (default: true)
506
-
507
- Returns:
508
- dict: {
509
- success: bool,
510
- route: {
511
- origin: str,
512
- destination: str,
513
- distance: {meters: int, text: str},
514
- vehicle_type: str,
515
- route_summary: str
516
- },
517
- timing: {
518
- base_duration: {seconds: int, text: str},
519
- with_traffic: {seconds: int, text: str},
520
- adjusted_duration: {seconds: int, text: str},
521
- traffic_delay_percent: int,
522
- weather_delay_percent: int,
523
- total_delay_percent: int
524
- },
525
- conditions: {
526
- traffic_status: str (clear|light|moderate|heavy|severe),
527
- weather_considered: bool
528
- },
529
- weather: {
530
- conditions: str,
531
- temperature_c: float,
532
- precipitation_mm: float,
533
- visibility_m: int,
534
- impact_severity: str (none|minor|moderate|severe)
535
- },
536
- recommendations: list[str],
537
- warnings: list[str],
538
- alternatives: list (if available)
539
- }
540
-
541
- Examples:
542
- - "Find the best route from SF to Oakland for a motorcycle considering weather"
543
- - "What's the fastest route from downtown to airport with current traffic?"
544
- - "Calculate delivery route for a truck from warehouse to customer address"
545
- """
546
- from chat.route_optimizer import calculate_intelligent_route as calc_route
547
- logger.info(f"Tool: calculate_intelligent_route('{origin}' -> '{destination}', vehicle={vehicle_type})")
548
- return calc_route(origin, destination, vehicle_type, consider_weather, consider_traffic)
549
-
550
-
551
- @mcp.tool()
552
- def create_order(
553
- customer_name: str,
554
- delivery_address: str,
555
- delivery_lat: float,
556
- delivery_lng: float,
557
- expected_delivery_time: str,
558
- customer_phone: str | None = None,
559
- customer_email: str | None = None,
560
- priority: Literal["standard", "express", "urgent"] = "standard",
561
- weight_kg: float = 5.0,
562
- special_instructions: str | None = None,
563
- sla_grace_period_minutes: int = 15,
564
- time_window_end: str | None = None
565
- ) -> dict:
566
- """
567
- Create a new delivery order in the database with MANDATORY delivery deadline.
568
-
569
- IMPORTANT: expected_delivery_time is REQUIRED. This is the promised delivery time to the customer.
570
- Only call this after geocoding the address successfully.
571
-
572
- Args:
573
- customer_name: Full name of the customer
574
- delivery_address: Complete delivery address
575
- delivery_lat: Latitude from geocoding
576
- delivery_lng: Longitude from geocoding
577
- expected_delivery_time: REQUIRED - Promised delivery deadline in ISO 8601 format
578
- Must be future timestamp. Examples:
579
- - '2025-11-15T18:00:00' (6 PM today)
580
- - '2025-11-16T12:00:00' (noon tomorrow)
581
- customer_phone: Customer phone number (optional)
582
- customer_email: Customer email address (optional)
583
- priority: Delivery priority level (default: standard)
584
- weight_kg: Package weight in kilograms (default: 5.0)
585
- special_instructions: Special delivery instructions (optional)
586
- sla_grace_period_minutes: Grace period after deadline (default: 15 mins)
587
- Deliveries within grace period marked as 'late' but acceptable
588
- time_window_end: Legacy field, defaults to expected_delivery_time if not provided
589
-
590
- Returns:
591
- dict: {
592
- success: bool,
593
- order_id: str,
594
- status: str,
595
- customer: str,
596
- address: str,
597
- expected_delivery: str (new),
598
- sla_grace_period_minutes: int (new),
599
- priority: str,
600
- message: str
601
- }
602
- """
603
- # STEP 1: Authenticate user
604
- user = get_authenticated_user()
605
- if not user:
606
- return {
607
- "success": False,
608
- "error": "Authentication required. Please login first.",
609
- "auth_required": True
610
- }
611
-
612
- # STEP 2: Check permissions
613
- required_scope = get_required_scope('create_order')
614
- if not check_permission(user.get('scopes', []), required_scope):
615
- return {
616
- "success": False,
617
- "error": f"Permission denied. Required scope: {required_scope}"
618
- }
619
-
620
- # STEP 3: Execute tool with user_id
621
- from chat.tools import handle_create_order
622
- logger.info(f"Tool: create_order by user {user.get('email')} (customer='{customer_name}')")
623
-
624
- return handle_create_order(
625
- tool_input={
626
- "customer_name": customer_name,
627
- "delivery_address": delivery_address,
628
- "delivery_lat": delivery_lat,
629
- "delivery_lng": delivery_lng,
630
- "expected_delivery_time": expected_delivery_time,
631
- "customer_phone": customer_phone,
632
- "customer_email": customer_email,
633
- "priority": priority,
634
- "weight_kg": weight_kg,
635
- "special_instructions": special_instructions,
636
- "sla_grace_period_minutes": sla_grace_period_minutes,
637
- "time_window_end": time_window_end
638
- },
639
- user_id=user['user_id'] # Pass user_id for data isolation
640
- )
641
-
642
-
643
- # ============================================================================
644
- # MCP TOOLS - ORDER QUERYING
645
- # ============================================================================
646
-
647
- @mcp.tool()
648
- def count_orders(
649
- status: Literal["pending", "assigned", "in_transit", "delivered", "failed", "cancelled"] | None = None,
650
- priority: Literal["standard", "express", "urgent"] | None = None,
651
- payment_status: Literal["pending", "paid", "cod"] | None = None,
652
- assigned_driver_id: str | None = None,
653
- is_fragile: bool | None = None,
654
- requires_signature: bool | None = None,
655
- requires_cold_storage: bool | None = None
656
- ) -> dict:
657
- """
658
- Count total orders in the database with optional filters.
659
- Use this when user asks 'how many orders', 'fetch orders', or wants to know order statistics.
660
-
661
- Args:
662
- status: Filter by order status (optional)
663
- priority: Filter by priority level (optional)
664
- payment_status: Filter by payment status (optional)
665
- assigned_driver_id: Filter by assigned driver ID (optional)
666
- is_fragile: Filter fragile packages only (optional)
667
- requires_signature: Filter orders requiring signature (optional)
668
- requires_cold_storage: Filter orders requiring cold storage (optional)
669
-
670
- Returns:
671
- dict: {
672
- success: bool,
673
- total: int,
674
- status_breakdown: dict,
675
- priority_breakdown: dict,
676
- message: str
677
- }
678
- """
679
- from chat.tools import handle_count_orders
680
- logger.info(f"Tool: count_orders(status={status}, priority={priority})")
681
-
682
- # STEP 1: Authenticate user
683
- user = get_authenticated_user()
684
- if not user:
685
- return {"success": False, "error": "Authentication required"}
686
-
687
- # STEP 2: Check permissions
688
- required_scope = get_required_scope('count_orders')
689
- if not check_permission(user.get('scopes', []), required_scope):
690
- return {"success": False, "error": "Permission denied"}
691
-
692
- # STEP 3: Execute with user_id
693
- tool_input = {}
694
- if status is not None:
695
- tool_input["status"] = status
696
- if priority is not None:
697
- tool_input["priority"] = priority
698
- if payment_status is not None:
699
- tool_input["payment_status"] = payment_status
700
- if assigned_driver_id is not None:
701
- tool_input["assigned_driver_id"] = assigned_driver_id
702
- if is_fragile is not None:
703
- tool_input["is_fragile"] = is_fragile
704
- if requires_signature is not None:
705
- tool_input["requires_signature"] = requires_signature
706
- if requires_cold_storage is not None:
707
- tool_input["requires_cold_storage"] = requires_cold_storage
708
- return handle_count_orders(tool_input, user_id=user['user_id'])
709
-
710
-
711
- @mcp.tool()
712
- def fetch_orders(
713
- limit: int = 10,
714
- offset: int = 0,
715
- status: Literal["pending", "assigned", "in_transit", "delivered", "failed", "cancelled"] | None = None,
716
- priority: Literal["standard", "express", "urgent"] | None = None,
717
- payment_status: Literal["pending", "paid", "cod"] | None = None,
718
- assigned_driver_id: str | None = None,
719
- is_fragile: bool | None = None,
720
- requires_signature: bool | None = None,
721
- requires_cold_storage: bool | None = None,
722
- sort_by: Literal["created_at", "priority", "time_window_start"] = "created_at",
723
- sort_order: Literal["ASC", "DESC"] = "DESC"
724
- ) -> dict:
725
- """
726
- Fetch orders from the database with optional filters, pagination, and sorting.
727
- Use after counting to show specific number of orders.
728
-
729
- Args:
730
- limit: Number of orders to fetch (default: 10, max: 100)
731
- offset: Number of orders to skip for pagination (default: 0)
732
- status: Filter by order status (optional)
733
- priority: Filter by priority level (optional)
734
- payment_status: Filter by payment status (optional)
735
- assigned_driver_id: Filter by assigned driver ID (optional)
736
- is_fragile: Filter fragile packages only (optional)
737
- requires_signature: Filter orders requiring signature (optional)
738
- requires_cold_storage: Filter orders requiring cold storage (optional)
739
- sort_by: Field to sort by (default: created_at)
740
- sort_order: Sort order (default: DESC for newest first)
741
-
742
- Returns:
743
- dict: {
744
- success: bool,
745
- orders: list[dict],
746
- count: int,
747
- message: str
748
- }
749
- """
750
- from chat.tools import handle_fetch_orders
751
- logger.info(f"Tool: fetch_orders(limit={limit}, offset={offset}, status={status})")
752
-
753
- # STEP 1: Authenticate user
754
- user = get_authenticated_user()
755
- if not user:
756
- return {"success": False, "error": "Authentication required"}
757
-
758
- # STEP 2: Check permissions
759
- required_scope = get_required_scope('fetch_orders')
760
- if not check_permission(user.get('scopes', []), required_scope):
761
- return {"success": False, "error": "Permission denied"}
762
-
763
- # STEP 3: Execute with user_id
764
- tool_input = {
765
- "limit": limit,
766
- "offset": offset,
767
- "sort_by": sort_by,
768
- "sort_order": sort_order
769
- }
770
- if status is not None:
771
- tool_input["status"] = status
772
- if priority is not None:
773
- tool_input["priority"] = priority
774
- if payment_status is not None:
775
- tool_input["payment_status"] = payment_status
776
- if assigned_driver_id is not None:
777
- tool_input["assigned_driver_id"] = assigned_driver_id
778
- if is_fragile is not None:
779
- tool_input["is_fragile"] = is_fragile
780
- if requires_signature is not None:
781
- tool_input["requires_signature"] = requires_signature
782
- if requires_cold_storage is not None:
783
- tool_input["requires_cold_storage"] = requires_cold_storage
784
- return handle_fetch_orders(tool_input, user_id=user['user_id'])
785
-
786
-
787
- @mcp.tool()
788
- def get_order_details(order_id: str) -> dict:
789
- """
790
- Get complete details of a specific order by order ID.
791
- Use when user asks 'tell me about order X' or wants detailed information about a specific order.
792
-
793
- Args:
794
- order_id: The order ID to fetch details for (e.g., 'ORD-20251114163800')
795
-
796
- Returns:
797
- dict: {
798
- success: bool,
799
- order: dict (with all 26 fields),
800
- message: str
801
- }
802
- """
803
- from chat.tools import handle_get_order_details
804
- logger.info(f"Tool: get_order_details(order_id='{order_id}')")
805
-
806
- # STEP 1: Authenticate user
807
- user = get_authenticated_user()
808
- if not user:
809
- return {"success": False, "error": "Authentication required"}
810
-
811
- # STEP 2: Check permissions
812
- required_scope = get_required_scope('get_order_details')
813
- if not check_permission(user.get('scopes', []), required_scope):
814
- return {"success": False, "error": "Permission denied"}
815
-
816
- # STEP 3: Execute with user_id
817
- return handle_get_order_details({"order_id": order_id}, user_id=user['user_id'])
818
-
819
-
820
- @mcp.tool()
821
- def search_orders(search_term: str) -> dict:
822
- """
823
- Search for orders by customer name, email, phone, or order ID pattern.
824
- Use when user provides partial information to find orders.
825
-
826
- Args:
827
- search_term: Search term to match against customer_name, customer_email, customer_phone, or order_id
828
-
829
- Returns:
830
- dict: {
831
- success: bool,
832
- orders: list[dict],
833
- count: int,
834
- message: str
835
- }
836
- """
837
- from chat.tools import handle_search_orders
838
- logger.info(f"Tool: search_orders(search_term='{search_term}')")
839
-
840
- # STEP 1: Authenticate user
841
- user = get_authenticated_user()
842
- if not user:
843
- return {"success": False, "error": "Authentication required"}
844
-
845
- # STEP 2: Check permissions
846
- required_scope = get_required_scope('search_orders')
847
- if not check_permission(user.get('scopes', []), required_scope):
848
- return {"success": False, "error": "Permission denied"}
849
-
850
- # STEP 3: Execute with user_id
851
- return handle_search_orders({"search_term": search_term}, user_id=user['user_id'])
852
-
853
-
854
- @mcp.tool()
855
- def get_incomplete_orders(limit: int = 20) -> dict:
856
- """
857
- Get all orders that are not yet completed (excludes delivered and cancelled orders).
858
- Shortcut for finding orders in progress (pending, assigned, in_transit).
859
-
860
- Args:
861
- limit: Number of orders to fetch (default: 20)
862
-
863
- Returns:
864
- dict: {
865
- success: bool,
866
- orders: list[dict],
867
- count: int,
868
- message: str
869
- }
870
- """
871
- from chat.tools import handle_get_incomplete_orders
872
- logger.info(f"Tool: get_incomplete_orders(limit={limit})")
873
-
874
- # STEP 1: Authenticate user
875
- user = get_authenticated_user()
876
- if not user:
877
- return {"success": False, "error": "Authentication required"}
878
-
879
- # STEP 2: Check permissions
880
- required_scope = get_required_scope('get_incomplete_orders')
881
- if not check_permission(user.get('scopes', []), required_scope):
882
- return {"success": False, "error": "Permission denied"}
883
-
884
- # STEP 3: Execute with user_id
885
- return handle_get_incomplete_orders({"limit": limit}, user_id=user['user_id'])
886
-
887
-
888
- # ============================================================================
889
- # MCP TOOLS - ORDER MANAGEMENT
890
- # ============================================================================
891
-
892
- @mcp.tool()
893
- def update_order(
894
- order_id: str,
895
- customer_name: str | None = None,
896
- customer_phone: str | None = None,
897
- customer_email: str | None = None,
898
- delivery_address: str | None = None,
899
- delivery_lat: float | None = None,
900
- delivery_lng: float | None = None,
901
- status: Literal["pending", "assigned", "in_transit", "delivered", "failed", "cancelled"] | None = None,
902
- priority: Literal["standard", "express", "urgent"] | None = None,
903
- special_instructions: str | None = None,
904
- time_window_end: str | None = None,
905
- payment_status: Literal["pending", "paid", "cod"] | None = None,
906
- weight_kg: float | None = None,
907
- order_value: float | None = None
908
- ) -> dict:
909
- """
910
- Update an existing order's details. You can update any combination of fields.
911
- Only provide the fields you want to change. Auto-geocodes if delivery_address updated without coordinates.
912
-
913
- Args:
914
- order_id: Order ID to update (e.g., 'ORD-20250114123456')
915
- customer_name: Updated customer name (optional)
916
- customer_phone: Updated customer phone number (optional)
917
- customer_email: Updated customer email address (optional)
918
- delivery_address: Updated delivery address (optional)
919
- delivery_lat: Updated delivery latitude (required if updating address) (optional)
920
- delivery_lng: Updated delivery longitude (required if updating address) (optional)
921
- status: Updated order status (optional)
922
- priority: Updated priority level (optional)
923
- special_instructions: Updated special delivery instructions (optional)
924
- time_window_end: Updated delivery deadline (ISO format datetime) (optional)
925
- payment_status: Updated payment status (optional)
926
- weight_kg: Updated package weight in kilograms (optional)
927
- order_value: Updated order value in currency (optional)
928
-
929
- Returns:
930
- dict: {
931
- success: bool,
932
- order_id: str,
933
- updated_fields: list[str],
934
- message: str
935
- }
936
- """
937
- from chat.tools import handle_update_order
938
- logger.info(f"Tool: update_order(order_id='{order_id}')")
939
-
940
- # STEP 1: Authenticate user
941
- user = get_authenticated_user()
942
- if not user:
943
- return {"success": False, "error": "Authentication required"}
944
-
945
- # STEP 2: Check permissions
946
- required_scope = get_required_scope('update_order')
947
- if not check_permission(user.get('scopes', []), required_scope):
948
- return {"success": False, "error": "Permission denied"}
949
-
950
- # STEP 3: Execute with user_id
951
- tool_input = {"order_id": order_id}
952
- if customer_name is not None:
953
- tool_input["customer_name"] = customer_name
954
- if customer_phone is not None:
955
- tool_input["customer_phone"] = customer_phone
956
- if customer_email is not None:
957
- tool_input["customer_email"] = customer_email
958
- if delivery_address is not None:
959
- tool_input["delivery_address"] = delivery_address
960
- if delivery_lat is not None:
961
- tool_input["delivery_lat"] = delivery_lat
962
- if delivery_lng is not None:
963
- tool_input["delivery_lng"] = delivery_lng
964
- if status is not None:
965
- tool_input["status"] = status
966
- if priority is not None:
967
- tool_input["priority"] = priority
968
- if special_instructions is not None:
969
- tool_input["special_instructions"] = special_instructions
970
- if time_window_end is not None:
971
- tool_input["time_window_end"] = time_window_end
972
- if payment_status is not None:
973
- tool_input["payment_status"] = payment_status
974
- if weight_kg is not None:
975
- tool_input["weight_kg"] = weight_kg
976
- if order_value is not None:
977
- tool_input["order_value"] = order_value
978
- return handle_update_order(tool_input, user_id=user['user_id'])
979
-
980
-
981
- @mcp.tool()
982
- def delete_order(order_id: str, confirm: bool) -> dict:
983
- """
984
- Permanently delete an order from the database. This action cannot be undone. Use with caution.
985
-
986
- Args:
987
- order_id: Order ID to delete (e.g., 'ORD-20250114123456')
988
- confirm: Must be set to true to confirm deletion
989
-
990
- Returns:
991
- dict: {
992
- success: bool,
993
- order_id: str,
994
- message: str
995
- }
996
- """
997
- from chat.tools import handle_delete_order
998
- logger.info(f"Tool: delete_order(order_id='{order_id}', confirm={confirm})")
999
-
1000
- # STEP 1: Authenticate user
1001
- user = get_authenticated_user()
1002
- if not user:
1003
- return {"success": False, "error": "Authentication required"}
1004
-
1005
- # STEP 2: Check permissions
1006
- required_scope = get_required_scope('delete_order')
1007
- if not check_permission(user.get('scopes', []), required_scope):
1008
- return {"success": False, "error": "Permission denied"}
1009
-
1010
- # STEP 3: Execute with user_id
1011
- return handle_delete_order({"order_id": order_id, "confirm": confirm}, user_id=user['user_id'])
1012
-
1013
-
1014
- # ============================================================================
1015
- # MCP TOOLS - DRIVER CREATION
1016
- # ============================================================================
1017
-
1018
- @mcp.tool()
1019
- def create_driver(
1020
- name: str,
1021
- vehicle_type: str,
1022
- current_address: str,
1023
- current_lat: float,
1024
- current_lng: float,
1025
- phone: str | None = None,
1026
- email: str | None = None,
1027
- vehicle_plate: str | None = None,
1028
- capacity_kg: float = 1000.0,
1029
- capacity_m3: float = 12.0,
1030
- skills: list[str] | None = None,
1031
- status: Literal["active", "busy", "offline", "unavailable"] = "active"
1032
- ) -> dict:
1033
- """
1034
- Create a new delivery driver in the database. Use this to onboard new drivers to the fleet.
1035
-
1036
- Args:
1037
- name: Full name of the driver (REQUIRED)
1038
- vehicle_type: Type of vehicle: van, truck, car, motorcycle (REQUIRED)
1039
- current_address: Driver's current location address, e.g. '123 Main St, New York, NY' (REQUIRED)
1040
- current_lat: Driver's current latitude location (REQUIRED, -90 to 90)
1041
- current_lng: Driver's current longitude location (REQUIRED, -180 to 180)
1042
- phone: Driver phone number (optional)
1043
- email: Driver email address (optional)
1044
- vehicle_plate: Vehicle license plate number (optional)
1045
- capacity_kg: Vehicle cargo capacity in kilograms (default: 1000.0)
1046
- capacity_m3: Vehicle cargo volume in cubic meters (default: 12.0)
1047
- skills: List of driver skills/certifications: refrigerated, medical_certified, fragile_handler, overnight, express_delivery (optional)
1048
- status: Driver status (default: active)
1049
-
1050
- Returns:
1051
- dict: {
1052
- success: bool,
1053
- driver_id: str,
1054
- name: str,
1055
- status: str,
1056
- vehicle_type: str,
1057
- vehicle_plate: str,
1058
- capacity_kg: float,
1059
- skills: list[str],
1060
- location: {latitude, longitude, address},
1061
- message: str
1062
- }
1063
- """
1064
- from chat.tools import handle_create_driver
1065
- logger.info(f"Tool: create_driver(name='{name}', vehicle_type='{vehicle_type}', address='{current_address}', location=({current_lat}, {current_lng}))")
1066
-
1067
- # STEP 1: Authenticate user
1068
- user = get_authenticated_user()
1069
- if not user:
1070
- return {"success": False, "error": "Authentication required"}
1071
-
1072
- # STEP 2: Check permissions
1073
- required_scope = get_required_scope('create_driver')
1074
- if not check_permission(user.get('scopes', []), required_scope):
1075
- return {"success": False, "error": "Permission denied"}
1076
-
1077
- # STEP 3: Execute with user_id
1078
- return handle_create_driver({
1079
- "name": name,
1080
- "phone": phone,
1081
- "email": email,
1082
- "vehicle_type": vehicle_type,
1083
- "vehicle_plate": vehicle_plate,
1084
- "capacity_kg": capacity_kg,
1085
- "capacity_m3": capacity_m3,
1086
- "skills": skills or [],
1087
- "status": status,
1088
- "current_address": current_address,
1089
- "current_lat": current_lat,
1090
- "current_lng": current_lng
1091
- }, user_id=user['user_id'])
1092
-
1093
-
1094
- # ============================================================================
1095
- # MCP TOOLS - DRIVER QUERYING
1096
- # ============================================================================
1097
-
1098
- @mcp.tool()
1099
- def count_drivers(
1100
- status: Literal["active", "busy", "offline", "unavailable"] | None = None,
1101
- vehicle_type: str | None = None
1102
- ) -> dict:
1103
- """
1104
- Count total drivers in the database with optional filters.
1105
- Use this when user asks 'how many drivers', 'show drivers', or wants driver statistics.
1106
-
1107
- Args:
1108
- status: Filter by driver status (optional)
1109
- vehicle_type: Filter by vehicle type: van, truck, car, motorcycle, etc. (optional)
1110
-
1111
- Returns:
1112
- dict: {
1113
- success: bool,
1114
- total: int,
1115
- status_breakdown: dict,
1116
- vehicle_breakdown: dict,
1117
- message: str
1118
- }
1119
- """
1120
- from chat.tools import handle_count_drivers
1121
- logger.info(f"Tool: count_drivers(status={status}, vehicle_type={vehicle_type})")
1122
-
1123
- # STEP 1: Authenticate user
1124
- user = get_authenticated_user()
1125
- if not user:
1126
- return {"success": False, "error": "Authentication required"}
1127
-
1128
- # STEP 2: Check permissions
1129
- required_scope = get_required_scope('count_drivers')
1130
- if not check_permission(user.get('scopes', []), required_scope):
1131
- return {"success": False, "error": "Permission denied"}
1132
-
1133
- # STEP 3: Execute with user_id
1134
- tool_input = {}
1135
- if status is not None:
1136
- tool_input["status"] = status
1137
- if vehicle_type is not None:
1138
- tool_input["vehicle_type"] = vehicle_type
1139
- return handle_count_drivers(tool_input, user_id=user['user_id'])
1140
-
1141
-
1142
- @mcp.tool()
1143
- def fetch_drivers(
1144
- limit: int = 10,
1145
- offset: int = 0,
1146
- status: Literal["active", "busy", "offline", "unavailable"] | None = None,
1147
- vehicle_type: str | None = None,
1148
- sort_by: Literal["name", "status", "created_at", "last_location_update"] = "name",
1149
- sort_order: Literal["ASC", "DESC"] = "ASC"
1150
- ) -> dict:
1151
- """
1152
- Fetch drivers from the database with optional filters, pagination, and sorting.
1153
- Use after counting to show specific number of drivers.
1154
-
1155
- Args:
1156
- limit: Number of drivers to fetch (default: 10, max: 100)
1157
- offset: Number of drivers to skip for pagination (default: 0)
1158
- status: Filter by driver status (optional)
1159
- vehicle_type: Filter by vehicle type: van, truck, car, motorcycle, etc. (optional)
1160
- sort_by: Field to sort by (default: name)
1161
- sort_order: Sort order (default: ASC for alphabetical)
1162
-
1163
- Returns:
1164
- dict: {
1165
- success: bool,
1166
- drivers: list[dict],
1167
- count: int,
1168
- message: str
1169
- }
1170
- """
1171
- from chat.tools import handle_fetch_drivers
1172
- logger.info(f"Tool: fetch_drivers(limit={limit}, offset={offset}, status={status})")
1173
-
1174
- # STEP 1: Authenticate user
1175
- user = get_authenticated_user()
1176
- if not user:
1177
- return {"success": False, "error": "Authentication required"}
1178
-
1179
- # STEP 2: Check permissions
1180
- required_scope = get_required_scope('fetch_drivers')
1181
- if not check_permission(user.get('scopes', []), required_scope):
1182
- return {"success": False, "error": "Permission denied"}
1183
-
1184
- # STEP 3: Execute with user_id
1185
- tool_input = {
1186
- "limit": limit,
1187
- "offset": offset,
1188
- "sort_by": sort_by,
1189
- "sort_order": sort_order
1190
- }
1191
- if status is not None:
1192
- tool_input["status"] = status
1193
- if vehicle_type is not None:
1194
- tool_input["vehicle_type"] = vehicle_type
1195
- return handle_fetch_drivers(tool_input, user_id=user['user_id'])
1196
-
1197
-
1198
- @mcp.tool()
1199
- def get_driver_details(driver_id: str) -> dict:
1200
- """
1201
- Get complete details of a specific driver by driver ID, including current location
1202
- (latitude, longitude, and human-readable address via reverse geocoding), contact info,
1203
- vehicle details, status, and skills. Use when user asks about a driver's location,
1204
- coordinates, position, or any other driver information.
1205
-
1206
- Args:
1207
- driver_id: The driver ID to fetch details for (e.g., 'DRV-20251114163800')
1208
-
1209
- Returns:
1210
- dict: {
1211
- success: bool,
1212
- driver: dict (with all fields including reverse-geocoded location address),
1213
- message: str
1214
- }
1215
- """
1216
- from chat.tools import handle_get_driver_details
1217
- logger.info(f"Tool: get_driver_details(driver_id='{driver_id}')")
1218
-
1219
- # STEP 1: Authenticate user
1220
- user = get_authenticated_user()
1221
- if not user:
1222
- return {"success": False, "error": "Authentication required"}
1223
-
1224
- # STEP 2: Check permissions
1225
- required_scope = get_required_scope('get_driver_details')
1226
- if not check_permission(user.get('scopes', []), required_scope):
1227
- return {"success": False, "error": "Permission denied"}
1228
-
1229
- # STEP 3: Execute with user_id
1230
- return handle_get_driver_details({"driver_id": driver_id}, user_id=user['user_id'])
1231
-
1232
-
1233
- @mcp.tool()
1234
- def search_drivers(search_term: str) -> dict:
1235
- """
1236
- Search for drivers by name, email, phone, vehicle plate, or driver ID pattern.
1237
- Use when user provides partial information to find drivers.
1238
-
1239
- Args:
1240
- search_term: Search term to match against name, email, phone, vehicle_plate, or driver_id
1241
-
1242
- Returns:
1243
- dict: {
1244
- success: bool,
1245
- drivers: list[dict],
1246
- count: int,
1247
- message: str
1248
- }
1249
- """
1250
- from chat.tools import handle_search_drivers
1251
- logger.info(f"Tool: search_drivers(search_term='{search_term}')")
1252
-
1253
- # STEP 1: Authenticate user
1254
- user = get_authenticated_user()
1255
- if not user:
1256
- return {"success": False, "error": "Authentication required"}
1257
-
1258
- # STEP 2: Check permissions
1259
- required_scope = get_required_scope('search_drivers')
1260
- if not check_permission(user.get('scopes', []), required_scope):
1261
- return {"success": False, "error": "Permission denied"}
1262
-
1263
- # STEP 3: Execute with user_id
1264
- return handle_search_drivers({"search_term": search_term}, user_id=user['user_id'])
1265
-
1266
-
1267
- @mcp.tool()
1268
- def get_available_drivers(limit: int = 20) -> dict:
1269
- """
1270
- Get all drivers that are available for assignment (active or offline status, excludes busy and unavailable).
1271
- Shortcut for finding drivers ready for dispatch.
1272
-
1273
- Args:
1274
- limit: Number of drivers to fetch (default: 20)
1275
-
1276
- Returns:
1277
- dict: {
1278
- success: bool,
1279
- drivers: list[dict],
1280
- count: int,
1281
- message: str
1282
- }
1283
- """
1284
- from chat.tools import handle_get_available_drivers
1285
- logger.info(f"Tool: get_available_drivers(limit={limit})")
1286
-
1287
- # STEP 1: Authenticate user
1288
- user = get_authenticated_user()
1289
- if not user:
1290
- return {"success": False, "error": "Authentication required"}
1291
-
1292
- # STEP 2: Check permissions
1293
- required_scope = get_required_scope('get_available_drivers')
1294
- if not check_permission(user.get('scopes', []), required_scope):
1295
- return {"success": False, "error": "Permission denied"}
1296
-
1297
- # STEP 3: Execute with user_id
1298
- return handle_get_available_drivers({"limit": limit}, user_id=user['user_id'])
1299
-
1300
-
1301
- # ============================================================================
1302
- # MCP TOOLS - DRIVER MANAGEMENT
1303
- # ============================================================================
1304
-
1305
- @mcp.tool()
1306
- def update_driver(
1307
- driver_id: str,
1308
- name: str | None = None,
1309
- phone: str | None = None,
1310
- email: str | None = None,
1311
- status: Literal["active", "busy", "offline", "unavailable"] | None = None,
1312
- vehicle_type: str | None = None,
1313
- vehicle_plate: str | None = None,
1314
- capacity_kg: float | None = None,
1315
- capacity_m3: float | None = None,
1316
- skills: list[str] | None = None,
1317
- current_address: str | None = None,
1318
- current_lat: float | None = None,
1319
- current_lng: float | None = None
1320
- ) -> dict:
1321
- """
1322
- Update an existing driver's details. You can update any combination of fields.
1323
- Only provide the fields you want to change. Auto-updates last_location_update if coordinates changed.
1324
-
1325
- Args:
1326
- driver_id: Driver ID to update (e.g., 'DRV-20250114123456')
1327
- name: Updated driver name (optional)
1328
- phone: Updated phone number (optional)
1329
- email: Updated email address (optional)
1330
- status: Updated driver status (optional)
1331
- vehicle_type: Updated vehicle type (optional)
1332
- vehicle_plate: Updated vehicle license plate (optional)
1333
- capacity_kg: Updated cargo capacity in kilograms (optional)
1334
- capacity_m3: Updated cargo capacity in cubic meters (optional)
1335
- skills: Updated list of driver skills/certifications (optional)
1336
- current_address: Updated current location address (optional)
1337
- current_lat: Updated current latitude (optional)
1338
- current_lng: Updated current longitude (optional)
1339
-
1340
- Returns:
1341
- dict: {
1342
- success: bool,
1343
- driver_id: str,
1344
- updated_fields: list[str],
1345
- message: str
1346
- }
1347
- """
1348
- from chat.tools import handle_update_driver
1349
- logger.info(f"Tool: update_driver(driver_id='{driver_id}')")
1350
-
1351
- # STEP 1: Authenticate user
1352
- user = get_authenticated_user()
1353
- if not user:
1354
- return {"success": False, "error": "Authentication required"}
1355
-
1356
- # STEP 2: Check permissions
1357
- required_scope = get_required_scope('update_driver')
1358
- if not check_permission(user.get('scopes', []), required_scope):
1359
- return {"success": False, "error": "Permission denied"}
1360
-
1361
- # STEP 3: Execute with user_id
1362
- tool_input = {"driver_id": driver_id}
1363
- if name is not None:
1364
- tool_input["name"] = name
1365
- if phone is not None:
1366
- tool_input["phone"] = phone
1367
- if email is not None:
1368
- tool_input["email"] = email
1369
- if status is not None:
1370
- tool_input["status"] = status
1371
- if vehicle_type is not None:
1372
- tool_input["vehicle_type"] = vehicle_type
1373
- if vehicle_plate is not None:
1374
- tool_input["vehicle_plate"] = vehicle_plate
1375
- if capacity_kg is not None:
1376
- tool_input["capacity_kg"] = capacity_kg
1377
- if capacity_m3 is not None:
1378
- tool_input["capacity_m3"] = capacity_m3
1379
- if skills is not None:
1380
- tool_input["skills"] = skills
1381
- if current_address is not None:
1382
- tool_input["current_address"] = current_address
1383
- if current_lat is not None:
1384
- tool_input["current_lat"] = current_lat
1385
- if current_lng is not None:
1386
- tool_input["current_lng"] = current_lng
1387
- return handle_update_driver(tool_input, user_id=user['user_id'])
1388
-
1389
-
1390
- @mcp.tool()
1391
- def delete_driver(driver_id: str, confirm: bool) -> dict:
1392
- """
1393
- Permanently delete a driver from the database. This action cannot be undone. Use with caution.
1394
-
1395
- Args:
1396
- driver_id: Driver ID to delete (e.g., 'DRV-20250114123456')
1397
- confirm: Must be set to true to confirm deletion
1398
-
1399
- Returns:
1400
- dict: {
1401
- success: bool,
1402
- driver_id: str,
1403
- message: str
1404
- }
1405
- """
1406
- from chat.tools import handle_delete_driver
1407
- logger.info(f"Tool: delete_driver(driver_id='{driver_id}', confirm={confirm})")
1408
-
1409
- # STEP 1: Authenticate user
1410
- user = get_authenticated_user()
1411
- if not user:
1412
- return {"success": False, "error": "Authentication required"}
1413
-
1414
- # STEP 2: Check permissions
1415
- required_scope = get_required_scope('delete_driver')
1416
- if not check_permission(user.get('scopes', []), required_scope):
1417
- return {"success": False, "error": "Permission denied"}
1418
-
1419
- # STEP 3: Execute with user_id
1420
- return handle_delete_driver({"driver_id": driver_id, "confirm": confirm}, user_id=user['user_id'])
1421
-
1422
-
1423
- @mcp.tool()
1424
- def delete_all_orders(confirm: bool, status: str = None) -> dict:
1425
- """
1426
- Bulk delete all orders (or orders with specific status). DANGEROUS - Use with extreme caution!
1427
-
1428
- Safety checks:
1429
- - Requires confirm=true
1430
- - Blocks deletion if any active assignments exist
1431
- - Optional status filter to delete only specific statuses
1432
-
1433
- Args:
1434
- confirm: Must be set to true to confirm bulk deletion
1435
- status: Optional status filter (pending/assigned/in_transit/delivered/failed/cancelled)
1436
-
1437
- Returns:
1438
- dict: {
1439
- success: bool,
1440
- deleted_count: int,
1441
- message: str
1442
- }
1443
- """
1444
- from chat.tools import handle_delete_all_orders
1445
- logger.info(f"Tool: delete_all_orders(confirm={confirm}, status='{status}')")
1446
-
1447
- # STEP 1: Authenticate user
1448
- user = get_authenticated_user()
1449
- if not user:
1450
- return {"success": False, "error": "Authentication required"}
1451
-
1452
- # STEP 2: Check permissions
1453
- required_scope = get_required_scope('delete_all_orders')
1454
- if not check_permission(user.get('scopes', []), required_scope):
1455
- return {"success": False, "error": "Permission denied"}
1456
-
1457
- # STEP 3: Execute with user_id
1458
- return handle_delete_all_orders({"confirm": confirm, "status": status}, user_id=user['user_id'])
1459
-
1460
-
1461
- @mcp.tool()
1462
- def delete_all_drivers(confirm: bool, status: str = None) -> dict:
1463
- """
1464
- Bulk delete all drivers (or drivers with specific status). DANGEROUS - Use with extreme caution!
1465
-
1466
- Safety checks:
1467
- - Requires confirm=true
1468
- - Blocks deletion if ANY assignments exist (due to RESTRICT constraint)
1469
- - Optional status filter to delete only specific statuses
1470
-
1471
- Args:
1472
- confirm: Must be set to true to confirm bulk deletion
1473
- status: Optional status filter (active/busy/offline/unavailable)
1474
-
1475
- Returns:
1476
- dict: {
1477
- success: bool,
1478
- deleted_count: int,
1479
- message: str
1480
- }
1481
- """
1482
- from chat.tools import handle_delete_all_drivers
1483
- logger.info(f"Tool: delete_all_drivers(confirm={confirm}, status='{status}')")
1484
-
1485
- # STEP 1: Authenticate user
1486
- user = get_authenticated_user()
1487
- if not user:
1488
- return {"success": False, "error": "Authentication required"}
1489
-
1490
- # STEP 2: Check permissions
1491
- required_scope = get_required_scope('delete_all_drivers')
1492
- if not check_permission(user.get('scopes', []), required_scope):
1493
- return {"success": False, "error": "Permission denied"}
1494
-
1495
- # STEP 3: Execute with user_id
1496
- return handle_delete_all_drivers({"confirm": confirm, "status": status}, user_id=user['user_id'])
1497
-
1498
-
1499
- # ============================================================================
1500
- # ASSIGNMENT TOOLS
1501
- # ============================================================================
1502
-
1503
- @mcp.tool()
1504
- def create_assignment(order_id: str, driver_id: str) -> dict:
1505
- """
1506
- Assign an order to a driver. Creates an assignment record with route data from driver location to delivery location.
1507
-
1508
- Requirements:
1509
- - Order must be in 'pending' status
1510
- - Driver must be in 'active' or 'available' status
1511
- - Order cannot already have an active assignment
1512
-
1513
- After assignment:
1514
- - Order status changes to 'assigned'
1515
- - Driver status changes to 'busy'
1516
- - Route data (distance, duration, path) is calculated and saved
1517
- - Assignment record is created with all route details
1518
-
1519
- Args:
1520
- order_id: Order ID to assign (e.g., 'ORD-20250114123456')
1521
- driver_id: Driver ID to assign (e.g., 'DRV-20250114123456')
1522
-
1523
- Returns:
1524
- dict: {
1525
- success: bool,
1526
- assignment_id: str,
1527
- order_id: str,
1528
- driver_id: str,
1529
- route: {
1530
- distance: {meters: int, text: str},
1531
- duration: {seconds: int, text: str},
1532
- route_summary: str,
1533
- driver_start: {lat: float, lng: float},
1534
- delivery_location: {lat: float, lng: float, address: str}
1535
- }
1536
- }
1537
- """
1538
- from chat.tools import handle_create_assignment
1539
- logger.info(f"Tool: create_assignment(order_id='{order_id}', driver_id='{driver_id}')")
1540
-
1541
- # STEP 1: Authenticate user
1542
- user = get_authenticated_user()
1543
- if not user:
1544
- return {"success": False, "error": "Authentication required"}
1545
-
1546
- # STEP 2: Check permissions
1547
- required_scope = get_required_scope('create_assignment')
1548
- if not check_permission(user.get('scopes', []), required_scope):
1549
- return {"success": False, "error": "Permission denied"}
1550
-
1551
- # STEP 3: Execute with user_id
1552
- return handle_create_assignment({"order_id": order_id, "driver_id": driver_id}, user_id=user['user_id'])
1553
-
1554
-
1555
- @mcp.tool()
1556
- def auto_assign_order(order_id: str) -> dict:
1557
- """
1558
- Automatically assign order to the nearest available driver (distance + validation based).
1559
-
1560
- Selection Criteria (Auto Algorithm):
1561
- 1. Driver must be 'active' with valid location
1562
- 2. Driver vehicle capacity must meet package weight/volume requirements
1563
- 3. Driver must have required skills (fragile handling, cold storage, etc.)
1564
- 4. Selects nearest driver by real-time route distance
1565
-
1566
- This is a fixed-rule algorithm that prioritizes proximity while ensuring
1567
- the driver has the necessary capacity and skills for the delivery.
1568
-
1569
- After assignment:
1570
- - Order status changes to 'assigned'
1571
- - Driver status changes to 'busy'
1572
- - Route data (distance, duration, path) is calculated and saved
1573
- - Assignment record is created with all route details
1574
-
1575
- Args:
1576
- order_id: Order ID to auto-assign (e.g., 'ORD-20250114123456')
1577
-
1578
- Returns:
1579
- dict: {
1580
- success: bool,
1581
- assignment_id: str,
1582
- method: 'auto_assignment',
1583
- order_id: str,
1584
- driver_id: str,
1585
- driver_name: str,
1586
- driver_phone: str,
1587
- driver_vehicle_type: str,
1588
- selection_reason: str,
1589
- distance_km: float,
1590
- distance_meters: int,
1591
- estimated_duration_minutes: float,
1592
- candidates_evaluated: int,
1593
- suitable_candidates: int,
1594
- route_summary: str,
1595
- estimated_arrival: str
1596
- }
1597
- """
1598
- from chat.tools import handle_auto_assign_order
1599
- logger.info(f"Tool: auto_assign_order(order_id='{order_id}')")
1600
-
1601
- # STEP 1: Authenticate user
1602
- user = get_authenticated_user()
1603
- if not user:
1604
- return {"success": False, "error": "Authentication required"}
1605
-
1606
- # STEP 2: Check permissions
1607
- required_scope = get_required_scope('auto_assign_order')
1608
- if not check_permission(user.get('scopes', []), required_scope):
1609
- return {"success": False, "error": "Permission denied"}
1610
-
1611
- # STEP 3: Execute with user_id
1612
- return handle_auto_assign_order({"order_id": order_id}, user_id=user['user_id'])
1613
-
1614
-
1615
- @mcp.tool()
1616
- def intelligent_assign_order(order_id: str) -> dict:
1617
- """
1618
- Intelligently assign order using Google Gemini 2.0 Flash AI to analyze all parameters and select the best driver.
1619
-
1620
- Uses Gemini 2.0 Flash (latest model) to evaluate:
1621
- - Order characteristics (priority, weight, fragility, time constraints, value)
1622
- - Driver capabilities (location, capacity, skills, vehicle type)
1623
- - Real-time routing data (distance, traffic delays, tolls)
1624
- - Weather conditions and impact on delivery
1625
- - Complex tradeoffs and optimal matching
1626
-
1627
- The AI considers multiple factors holistically:
1628
- - Distance efficiency vs skill requirements
1629
- - Capacity utilization vs delivery urgency
1630
- - Traffic conditions vs time constraints
1631
- - Weather safety vs speed requirements
1632
- - Cost efficiency (tolls, fuel) vs customer satisfaction
1633
-
1634
- Returns assignment with detailed AI reasoning explaining why the
1635
- selected driver is the best match for this specific delivery.
1636
-
1637
- Requirements:
1638
- - GOOGLE_API_KEY environment variable must be set
1639
- - Order must be in 'pending' status
1640
- - At least one active driver with valid location
1641
-
1642
- After assignment:
1643
- - Order status changes to 'assigned'
1644
- - Driver status changes to 'busy'
1645
- - Route data (distance, duration, path) is calculated and saved
1646
- - Assignment record is created with all route details
1647
- - AI reasoning is returned for transparency
1648
-
1649
- Args:
1650
- order_id: Order ID to intelligently assign (e.g., 'ORD-20250114123456')
1651
-
1652
- Returns:
1653
- dict: {
1654
- success: bool,
1655
- assignment_id: str,
1656
- method: 'intelligent_assignment',
1657
- ai_provider: 'Google Gemini 2.0 Flash',
1658
- order_id: str,
1659
- driver_id: str,
1660
- driver_name: str,
1661
- driver_phone: str,
1662
- driver_vehicle_type: str,
1663
- distance_km: float,
1664
- estimated_duration_minutes: float,
1665
- ai_reasoning: {
1666
- primary_factors: [str],
1667
- trade_offs_considered: [str],
1668
- risk_assessment: str,
1669
- decision_summary: str
1670
- },
1671
- confidence_score: float,
1672
- alternatives_considered: [{driver_id: str, reason_not_selected: str}],
1673
- candidates_evaluated: int,
1674
- route_summary: str,
1675
- estimated_arrival: str
1676
- }
1677
- """
1678
- from chat.tools import handle_intelligent_assign_order
1679
- logger.info(f"Tool: intelligent_assign_order(order_id='{order_id}')")
1680
-
1681
- # STEP 1: Authenticate user
1682
- user = get_authenticated_user()
1683
- if not user:
1684
- return {"success": False, "error": "Authentication required"}
1685
-
1686
- # STEP 2: Check permissions
1687
- required_scope = get_required_scope('intelligent_assign_order')
1688
- if not check_permission(user.get('scopes', []), required_scope):
1689
- return {"success": False, "error": "Permission denied"}
1690
-
1691
- # STEP 3: Execute with user_id
1692
- return handle_intelligent_assign_order({"order_id": order_id}, user_id=user['user_id'])
1693
-
1694
-
1695
- @mcp.tool()
1696
- def get_assignment_details(
1697
- assignment_id: str = None,
1698
- order_id: str = None,
1699
- driver_id: str = None
1700
- ) -> dict:
1701
- """
1702
- Get assignment details by assignment ID, order ID, or driver ID.
1703
- Provide at least one parameter to search.
1704
-
1705
- Args:
1706
- assignment_id: Assignment ID (e.g., 'ASN-20250114123456')
1707
- order_id: Order ID to find assignments for (e.g., 'ORD-20250114123456')
1708
- driver_id: Driver ID to find assignments for (e.g., 'DRV-20250114123456')
1709
-
1710
- Returns:
1711
- dict: {
1712
- success: bool,
1713
- assignments: [
1714
- {
1715
- assignment_id: str,
1716
- order_id: str,
1717
- driver_id: str,
1718
- customer_name: str,
1719
- driver_name: str,
1720
- status: str,
1721
- route_distance_meters: int,
1722
- route_duration_seconds: int,
1723
- route_summary: str,
1724
- driver_start_location: {lat: float, lng: float},
1725
- delivery_location: {lat: float, lng: float, address: str},
1726
- estimated_arrival: str,
1727
- assigned_at: str,
1728
- updated_at: str
1729
- }
1730
- ]
1731
- }
1732
- """
1733
- from chat.tools import handle_get_assignment_details
1734
- logger.info(f"Tool: get_assignment_details(assignment_id='{assignment_id}', order_id='{order_id}', driver_id='{driver_id}')")
1735
-
1736
- # STEP 1: Authenticate user
1737
- user = get_authenticated_user()
1738
- if not user:
1739
- return {"success": False, "error": "Authentication required"}
1740
-
1741
- # STEP 2: Check permissions
1742
- required_scope = get_required_scope('get_assignment_details')
1743
- if not check_permission(user.get('scopes', []), required_scope):
1744
- return {"success": False, "error": "Permission denied"}
1745
-
1746
- # STEP 3: Execute with user_id
1747
- return handle_get_assignment_details({
1748
- "assignment_id": assignment_id,
1749
- "order_id": order_id,
1750
- "driver_id": driver_id
1751
- }, user_id=user['user_id'])
1752
-
1753
-
1754
- @mcp.tool()
1755
- def update_assignment(
1756
- assignment_id: str,
1757
- status: str = None,
1758
- actual_arrival: str = None,
1759
- actual_distance_meters: int = None,
1760
- notes: str = None
1761
- ) -> dict:
1762
- """
1763
- Update assignment status or details.
1764
-
1765
- Valid status transitions:
1766
- - active β†’ in_progress (driver starts delivery)
1767
- - in_progress β†’ completed (delivery successful)
1768
- - in_progress β†’ failed (delivery failed)
1769
- - active/in_progress β†’ cancelled (assignment cancelled)
1770
-
1771
- Cascading updates:
1772
- - completed: order status β†’ 'delivered', driver checks for other assignments
1773
- - failed: order status β†’ 'failed', driver checks for other assignments
1774
- - cancelled: order status β†’ 'cancelled', order.assigned_driver_id β†’ NULL, driver β†’ 'active' if no other assignments
1775
-
1776
- Args:
1777
- assignment_id: Assignment ID to update (e.g., 'ASN-20250114123456')
1778
- status: New status (active, in_progress, completed, failed, cancelled)
1779
- actual_arrival: Actual arrival timestamp (ISO format)
1780
- actual_distance_meters: Actual distance traveled in meters
1781
- notes: Additional notes about the assignment
1782
-
1783
- Returns:
1784
- dict: {
1785
- success: bool,
1786
- assignment_id: str,
1787
- updated_fields: list,
1788
- cascading_actions: list,
1789
- message: str
1790
- }
1791
- """
1792
- from chat.tools import handle_update_assignment
1793
- logger.info(f"Tool: update_assignment(assignment_id='{assignment_id}', status='{status}')")
1794
-
1795
- # STEP 1: Authenticate user
1796
- user = get_authenticated_user()
1797
- if not user:
1798
- return {"success": False, "error": "Authentication required"}
1799
-
1800
- # STEP 2: Check permissions
1801
- required_scope = get_required_scope('update_assignment')
1802
- if not check_permission(user.get('scopes', []), required_scope):
1803
- return {"success": False, "error": "Permission denied"}
1804
-
1805
- # STEP 3: Execute with user_id
1806
- return handle_update_assignment({
1807
- "assignment_id": assignment_id,
1808
- "status": status,
1809
- "actual_arrival": actual_arrival,
1810
- "actual_distance_meters": actual_distance_meters,
1811
- "notes": notes
1812
- }, user_id=user['user_id'])
1813
-
1814
-
1815
- @mcp.tool()
1816
- def unassign_order(assignment_id: str, confirm: bool = False) -> dict:
1817
- """
1818
- Unassign an order from a driver by deleting the assignment.
1819
-
1820
- Requirements:
1821
- - Assignment cannot be in 'in_progress' status (must cancel first using update_assignment)
1822
- - Requires confirm=true to proceed
1823
-
1824
- Effects:
1825
- - Assignment is deleted
1826
- - Order status changes back to 'pending'
1827
- - order.assigned_driver_id is set to NULL
1828
- - Driver status changes to 'active' (if no other assignments)
1829
-
1830
- Args:
1831
- assignment_id: Assignment ID to unassign (e.g., 'ASN-20250114123456')
1832
- confirm: Must be set to true to confirm unassignment
1833
-
1834
- Returns:
1835
- dict: {
1836
- success: bool,
1837
- assignment_id: str,
1838
- order_id: str,
1839
- driver_id: str,
1840
- message: str
1841
- }
1842
- """
1843
- from chat.tools import handle_unassign_order
1844
- logger.info(f"Tool: unassign_order(assignment_id='{assignment_id}', confirm={confirm})")
1845
-
1846
- # STEP 1: Authenticate user
1847
- user = get_authenticated_user()
1848
- if not user:
1849
- return {"success": False, "error": "Authentication required"}
1850
-
1851
- # STEP 2: Check permissions
1852
- required_scope = get_required_scope('unassign_order')
1853
- if not check_permission(user.get('scopes', []), required_scope):
1854
- return {"success": False, "error": "Permission denied"}
1855
-
1856
- # STEP 3: Execute with user_id
1857
- return handle_unassign_order({"assignment_id": assignment_id, "confirm": confirm}, user_id=user['user_id'])
1858
-
1859
-
1860
- @mcp.tool()
1861
- def complete_delivery(
1862
- assignment_id: str,
1863
- confirm: bool,
1864
- actual_distance_meters: int = None,
1865
- notes: str = None
1866
- ) -> dict:
1867
- """
1868
- Mark a delivery as successfully completed and automatically update driver location to delivery address.
1869
-
1870
- This is the primary tool for completing deliveries. It handles all necessary updates:
1871
- - Marks assignment as 'completed' with timestamp
1872
- - Updates order status to 'delivered'
1873
- - **Automatically moves driver location to the delivery address**
1874
- - Updates driver status to 'active' (if no other assignments)
1875
- - Records actual distance and notes (optional)
1876
-
1877
- Requirements:
1878
- - Assignment must be in 'active' or 'in_progress' status
1879
- - Delivery location coordinates must exist
1880
- - Requires confirm=true
1881
-
1882
- For failed deliveries: Use fail_delivery tool instead.
1883
-
1884
- Args:
1885
- assignment_id: Assignment ID to complete (e.g., 'ASN-20250114123456')
1886
- confirm: Must be set to true to confirm completion
1887
- actual_distance_meters: Optional actual distance traveled in meters
1888
- notes: Optional completion notes
1889
-
1890
- Returns:
1891
- dict: {
1892
- success: bool,
1893
- assignment_id: str,
1894
- order_id: str,
1895
- driver_id: str,
1896
- customer_name: str,
1897
- driver_name: str,
1898
- completed_at: str (ISO timestamp),
1899
- delivery_location: {lat, lng, address},
1900
- driver_updated: {new_location, location_updated_at},
1901
- cascading_actions: list[str],
1902
- message: str
1903
- }
1904
- """
1905
- from chat.tools import handle_complete_delivery
1906
- logger.info(f"Tool: complete_delivery(assignment_id='{assignment_id}', confirm={confirm})")
1907
-
1908
- # STEP 1: Authenticate user
1909
- user = get_authenticated_user()
1910
- if not user:
1911
- return {"success": False, "error": "Authentication required"}
1912
-
1913
- # STEP 2: Check permissions
1914
- required_scope = get_required_scope('complete_delivery')
1915
- if not check_permission(user.get('scopes', []), required_scope):
1916
- return {"success": False, "error": "Permission denied"}
1917
-
1918
- # STEP 3: Execute with user_id
1919
- return handle_complete_delivery({
1920
- "assignment_id": assignment_id,
1921
- "confirm": confirm,
1922
- "actual_distance_meters": actual_distance_meters,
1923
- "notes": notes
1924
- }, user_id=user['user_id'])
1925
-
1926
-
1927
- @mcp.tool()
1928
- def fail_delivery(
1929
- assignment_id: str,
1930
- current_address: str,
1931
- current_lat: float,
1932
- current_lng: float,
1933
- failure_reason: str,
1934
- confirm: bool,
1935
- notes: str = None
1936
- ) -> dict:
1937
- """
1938
- Mark a delivery as failed with mandatory driver location and failure reason.
1939
-
1940
- IMPORTANT: Driver MUST provide their current location (address + GPS coordinates) and a valid failure reason.
1941
- This ensures accurate location tracking and proper failure documentation.
1942
-
1943
- Handles all necessary updates:
1944
- - Marks assignment as 'failed' with timestamp
1945
- - Updates order status to 'failed'
1946
- - **Updates driver location to the reported current position**
1947
- - Updates driver status to 'active' (if no other assignments)
1948
- - Records structured failure reason and optional notes
1949
-
1950
- Valid failure reasons:
1951
- - customer_not_available: Customer not present or not reachable
1952
- - wrong_address: Incorrect or invalid delivery address
1953
- - refused_delivery: Customer refused to accept delivery
1954
- - damaged_goods: Package damaged during transit
1955
- - payment_issue: Payment problems (for COD orders)
1956
- - vehicle_breakdown: Driver's vehicle broke down
1957
- - access_restricted: Cannot access delivery location
1958
- - weather_conditions: Severe weather preventing delivery
1959
- - other: Other reasons (provide details in notes)
1960
-
1961
- Requirements:
1962
- - Assignment must be in 'active' or 'in_progress' status
1963
- - Driver must provide current address and GPS coordinates
1964
- - Must provide a valid failure_reason from the list above
1965
- - Requires confirm=true
1966
-
1967
- Args:
1968
- assignment_id: Assignment ID to mark as failed (e.g., 'ASN-20250114123456')
1969
- current_address: Driver's current location address (e.g., '123 Main St, New York, NY')
1970
- current_lat: Driver's current latitude (-90 to 90)
1971
- current_lng: Driver's current longitude (-180 to 180)
1972
- failure_reason: Reason for failure (must be from valid list)
1973
- confirm: Must be set to true to confirm failure
1974
- notes: Optional additional details about the failure
1975
-
1976
- Returns:
1977
- dict: {
1978
- success: bool,
1979
- assignment_id: str,
1980
- order_id: str,
1981
- driver_id: str,
1982
- customer_name: str,
1983
- driver_name: str,
1984
- failed_at: str (ISO timestamp),
1985
- failure_reason: str,
1986
- failure_reason_display: str (human-readable),
1987
- delivery_address: str,
1988
- driver_location: {lat, lng, address, updated_at},
1989
- cascading_actions: list[str],
1990
- message: str
1991
- }
1992
- """
1993
- from chat.tools import handle_fail_delivery
1994
- logger.info(f"Tool: fail_delivery(assignment_id='{assignment_id}', reason='{failure_reason}')")
1995
-
1996
- # STEP 1: Authenticate user
1997
- user = get_authenticated_user()
1998
- if not user:
1999
- return {"success": False, "error": "Authentication required"}
2000
-
2001
- # STEP 2: Check permissions
2002
- required_scope = get_required_scope('fail_delivery')
2003
- if not check_permission(user.get('scopes', []), required_scope):
2004
- return {"success": False, "error": "Permission denied"}
2005
-
2006
- # STEP 3: Execute with user_id
2007
- return handle_fail_delivery({
2008
- "assignment_id": assignment_id,
2009
- "current_address": current_address,
2010
- "current_lat": current_lat,
2011
- "current_lng": current_lng,
2012
- "failure_reason": failure_reason,
2013
- "confirm": confirm,
2014
- "notes": notes
2015
- }, user_id=user['user_id'])
2016
-
2017
-
2018
- # ============================================================================
2019
- # MAIN ENTRY POINT
2020
- # ============================================================================
2021
-
2022
- if __name__ == "__main__":
2023
- logger.info("=" * 60)
2024
- logger.info("FleetMind MCP Server v1.0.0")
2025
- logger.info("=" * 60)
2026
- logger.info(f"Geocoding: {geocoding_service.get_status()}")
2027
- logger.info("Tools: 27 tools registered (19 core + 6 assignment + 2 bulk delete)")
2028
- logger.info("Resources: 2 resources available")
2029
- logger.info("Prompts: 3 workflow templates")
2030
- logger.info("Authentication: Multi-tenant API key via URL query params")
2031
- logger.info("Usage: Connect with ?api_key=YOUR_KEY in the SSE URL")
2032
- logger.info("Starting MCP server...")
2033
- mcp.run()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
start_with_proxy.py DELETED
@@ -1,164 +0,0 @@
1
- """
2
- FleetMind MCP Server with Authentication Proxy
3
- Launches both the FastMCP server and the authentication proxy.
4
-
5
- This script:
6
- 1. Starts FastMCP server on port 7861 (internal)
7
- 2. Starts authentication proxy on port 7860 (public)
8
- 3. Handles graceful shutdown of both processes
9
-
10
- Usage:
11
- python start_with_proxy.py
12
- """
13
-
14
- import subprocess
15
- import sys
16
- import time
17
- import signal
18
- import os
19
- from pathlib import Path
20
-
21
- # Process handles
22
- fastmcp_process = None
23
- proxy_process = None
24
-
25
-
26
- def signal_handler(sig, frame):
27
- """Handle Ctrl+C gracefully"""
28
- print("\n\nShutting down FleetMind MCP Server...")
29
-
30
- if proxy_process:
31
- print(" -> Stopping proxy...")
32
- proxy_process.terminate()
33
- try:
34
- proxy_process.wait(timeout=5)
35
- except subprocess.TimeoutExpired:
36
- proxy_process.kill()
37
-
38
- if fastmcp_process:
39
- print(" -> Stopping FastMCP server...")
40
- fastmcp_process.terminate()
41
- try:
42
- fastmcp_process.wait(timeout=5)
43
- except subprocess.TimeoutExpired:
44
- fastmcp_process.kill()
45
-
46
- print("[OK] FleetMind MCP Server stopped.")
47
- sys.exit(0)
48
-
49
-
50
- def main():
51
- global fastmcp_process, proxy_process
52
-
53
- # Register signal handler
54
- signal.signal(signal.SIGINT, signal_handler)
55
- signal.signal(signal.SIGTERM, signal_handler)
56
-
57
- print("\n" + "=" * 70)
58
- print("FleetMind MCP Server with Multi-Tenant Authentication")
59
- print("=" * 70)
60
- print("Starting components:")
61
- print(" 1. FastMCP Server (port 7861) - Internal")
62
- print(" 2. Auth Proxy (port 7860) - Public")
63
- print("=" * 70 + "\n")
64
-
65
- # Get Python executable
66
- python_exe = sys.executable
67
-
68
- # Start FastMCP server
69
- print("[1/2] Starting FastMCP server on port 7861...")
70
- try:
71
- # Don't capture output - let it show in console
72
- fastmcp_process = subprocess.Popen(
73
- [python_exe, "app.py"]
74
- )
75
- print("[OK] FastMCP server starting (output below)...")
76
- except Exception as e:
77
- print(f"[ERROR] Failed to start FastMCP server: {e}")
78
- sys.exit(1)
79
-
80
- # Wait a bit for FastMCP to initialize
81
- print(" Waiting for FastMCP to initialize...")
82
- time.sleep(3)
83
-
84
- # Check if FastMCP is still running
85
- if fastmcp_process.poll() is not None:
86
- print("[ERROR] FastMCP server failed to start")
87
- print(" Check the error messages above")
88
- sys.exit(1)
89
-
90
- print("[OK] FastMCP server running")
91
-
92
- # Start proxy
93
- print("\n[2/2] Starting authentication proxy on port 7860...")
94
- try:
95
- # Don't capture output - let it show in console
96
- proxy_process = subprocess.Popen(
97
- [python_exe, "proxy.py"]
98
- )
99
- print("[OK] Auth proxy starting (output below)...")
100
- except Exception as e:
101
- print(f"[ERROR] Failed to start proxy: {e}")
102
- if fastmcp_process:
103
- fastmcp_process.terminate()
104
- sys.exit(1)
105
-
106
- # Wait a bit for proxy to initialize
107
- time.sleep(2)
108
-
109
- # Check if proxy is still running
110
- if proxy_process.poll() is not None:
111
- print("[ERROR] Proxy failed to start")
112
- if fastmcp_process:
113
- fastmcp_process.terminate()
114
- sys.exit(1)
115
-
116
- print("[OK] Auth proxy running")
117
-
118
- print("\n" + "=" * 70)
119
- print("[OK] FleetMind MCP Server is READY!")
120
- print("=" * 70)
121
- print(f"Connect to: http://localhost:7860/sse")
122
- print(f"Claude Desktop config:")
123
- print(f' "args": ["mcp-remote", "http://localhost:7860/sse?api_key=YOUR_KEY"]')
124
- print("=" * 70)
125
- print("\n[AUTH] Multi-tenant authentication is ENABLED")
126
- print(" Each connection's API key will be captured automatically\n")
127
- print("Press Ctrl+C to stop...\n")
128
-
129
- # Monitor both processes
130
- try:
131
- while True:
132
- # Check FastMCP
133
- if fastmcp_process.poll() is not None:
134
- print("[ERROR] FastMCP server stopped unexpectedly")
135
- if proxy_process:
136
- proxy_process.terminate()
137
- sys.exit(1)
138
-
139
- # Check proxy
140
- if proxy_process.poll() is not None:
141
- print("[ERROR] Proxy stopped unexpectedly")
142
- if fastmcp_process:
143
- fastmcp_process.terminate()
144
- sys.exit(1)
145
-
146
- # Read and display logs (non-blocking)
147
- # This keeps the terminal responsive
148
- time.sleep(1)
149
-
150
- except KeyboardInterrupt:
151
- signal_handler(signal.SIGINT, None)
152
-
153
-
154
- if __name__ == "__main__":
155
- # Check if required files exist
156
- if not Path("app.py").exists():
157
- print("[ERROR] app.py not found")
158
- sys.exit(1)
159
-
160
- if not Path("proxy.py").exists():
161
- print("[ERROR] proxy.py not found")
162
- sys.exit(1)
163
-
164
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ui/app.py CHANGED
@@ -1,6 +1,6 @@
1
  """
2
  FleetMind MCP - Gradio Web Interface
3
- Enhanced 3-tab dashboard: Chat, Orders, Drivers
4
  """
5
 
6
  import sys
@@ -18,463 +18,283 @@ import json
18
  from chat.chat_engine import ChatEngine
19
  from chat.conversation import ConversationManager
20
  from chat.geocoding import GeocodingService
21
- import uuid
22
-
23
- # Global session storage
24
- SESSIONS = {}
25
-
26
- # Initialize chat engine and geocoding service
27
- chat_engine = ChatEngine()
28
- geocoding_service = GeocodingService()
29
 
30
  # ============================================
31
- # STATISTICS FUNCTIONS
32
  # ============================================
33
 
34
- def get_orders_stats():
35
- """Get order statistics by status"""
36
  try:
37
- query = """
38
- SELECT
39
- COUNT(*) as total,
40
- COUNT(CASE WHEN status = 'pending' THEN 1 END) as pending,
41
- COUNT(CASE WHEN status = 'assigned' THEN 1 END) as assigned,
42
- COUNT(CASE WHEN status = 'in_transit' THEN 1 END) as in_transit,
43
- COUNT(CASE WHEN status = 'delivered' THEN 1 END) as delivered,
44
- COUNT(CASE WHEN status = 'failed' THEN 1 END) as failed,
45
- COUNT(CASE WHEN status = 'cancelled' THEN 1 END) as cancelled
46
- FROM orders
47
- """
48
- result = execute_query(query)
49
- if result:
50
- return result[0]
51
- return {"total": 0, "pending": 0, "assigned": 0, "in_transit": 0, "delivered": 0, "failed": 0, "cancelled": 0}
52
  except Exception as e:
53
- print(f"Error getting order stats: {e}")
54
- return {"total": 0, "pending": 0, "assigned": 0, "in_transit": 0, "delivered": 0, "failed": 0, "cancelled": 0}
55
 
56
 
57
- def get_drivers_stats():
58
- """Get driver statistics by status"""
59
  try:
60
  query = """
61
  SELECT
62
- COUNT(*) as total,
63
- COUNT(CASE WHEN status = 'active' THEN 1 END) as active,
64
- COUNT(CASE WHEN status = 'busy' THEN 1 END) as busy,
65
- COUNT(CASE WHEN status = 'offline' THEN 1 END) as offline,
66
- COUNT(CASE WHEN status = 'unavailable' THEN 1 END) as unavailable
67
- FROM drivers
68
  """
69
- result = execute_query(query)
70
- if result:
71
- return result[0]
72
- return {"total": 0, "active": 0, "busy": 0, "offline": 0, "unavailable": 0}
73
- except Exception as e:
74
- print(f"Error getting driver stats: {e}")
75
- return {"total": 0, "active": 0, "busy": 0, "offline": 0, "unavailable": 0}
76
-
77
 
78
- # ============================================
79
- # ORDERS FUNCTIONS
80
- # ============================================
81
-
82
- def get_all_orders(status_filter="all", priority_filter="all", payment_filter="all", search_term=""):
83
- """Get orders with filters"""
84
- try:
85
- where_clauses = []
86
- params = []
87
-
88
- if status_filter and status_filter != "all":
89
- where_clauses.append("status = %s")
90
- params.append(status_filter)
91
-
92
- if priority_filter and priority_filter != "all":
93
- where_clauses.append("priority = %s")
94
- params.append(priority_filter)
95
 
96
- if payment_filter and payment_filter != "all":
97
- where_clauses.append("payment_status = %s")
98
- params.append(payment_filter)
99
 
100
- if search_term:
101
- where_clauses.append("(order_id ILIKE %s OR customer_name ILIKE %s OR customer_phone ILIKE %s)")
102
- search_pattern = f"%{search_term}%"
103
- params.extend([search_pattern, search_pattern, search_pattern])
104
 
105
- where_sql = " WHERE " + " AND ".join(where_clauses) if where_clauses else ""
106
 
107
- query = f"""
 
 
 
108
  SELECT
109
  order_id,
110
  customer_name,
111
  delivery_address,
112
  status,
113
  priority,
114
- assigned_driver_id,
115
- time_window_end,
116
  created_at
117
  FROM orders
118
- {where_sql}
119
  ORDER BY created_at DESC
120
- LIMIT 100
121
  """
122
-
123
- results = execute_query(query, tuple(params) if params else ())
124
 
125
  if not results:
126
- return [["-", "-", "-", "-", "-", "-", "-"]]
127
 
128
- # Format data for table
129
  data = []
130
  for row in results:
131
- # Truncate address if too long
132
- address = row['delivery_address']
133
- if len(address) > 40:
134
- address = address[:37] + "..."
135
-
136
- # Format deadline
137
- deadline = row['time_window_end'].strftime("%Y-%m-%d %H:%M") if row['time_window_end'] else "No deadline"
138
-
139
- # Driver ID or "Unassigned"
140
- driver = row['assigned_driver_id'] if row['assigned_driver_id'] else "Unassigned"
141
-
142
  data.append([
143
  row['order_id'],
144
  row['customer_name'],
145
- address,
146
  row['status'],
147
  row['priority'],
148
- driver,
149
- deadline
150
  ])
151
 
152
  return data
153
  except Exception as e:
154
- print(f"Error fetching orders: {e}")
155
- return [[f"Error: {str(e)}", "", "", "", "", "", ""]]
156
 
157
 
158
- def get_order_details(order_id):
159
- """Get complete order details"""
160
- if not order_id or order_id == "-":
161
- return "Select an order from the table to view details"
162
-
163
  try:
 
 
 
164
  query = """
165
- SELECT * FROM orders WHERE order_id = %s
 
 
 
 
 
166
  """
167
- results = execute_query(query, (order_id,))
168
 
169
- if not results:
170
- return f"Order {order_id} not found"
171
-
172
- order = results[0]
173
-
174
- # Format the details nicely
175
- details = f"""
176
- # Order Details: {order_id}
177
-
178
- ## Customer Information
179
- - **Name:** {order['customer_name']}
180
- - **Phone:** {order['customer_phone'] or 'N/A'}
181
- - **Email:** {order['customer_email'] or 'N/A'}
182
-
183
- ## Delivery Information
184
- - **Address:** {order['delivery_address']}
185
- - **Coordinates:** ({order['delivery_lat']}, {order['delivery_lng']})
186
- - **Time Window:** {order['time_window_start']} to {order['time_window_end']}
187
-
188
- ## Package Details
189
- - **Weight:** {order['weight_kg'] or 'N/A'} kg
190
- - **Volume:** {order['volume_m3'] or 'N/A'} mΒ³
191
- - **Is Fragile:** {"Yes" if order['is_fragile'] else "No"}
192
- - **Requires Signature:** {"Yes" if order['requires_signature'] else "No"}
193
- - **Requires Cold Storage:** {"Yes" if order['requires_cold_storage'] else "No"}
194
-
195
- ## Order Status
196
- - **Status:** {order['status']}
197
- - **Priority:** {order['priority']}
198
- - **Assigned Driver:** {order['assigned_driver_id'] or 'Unassigned'}
199
-
200
- ## Payment
201
- - **Order Value:** ${order['order_value'] or '0.00'}
202
- - **Payment Status:** {order['payment_status']}
203
-
204
- ## Special Instructions
205
- {order['special_instructions'] or 'None'}
206
-
207
- ## Timestamps
208
- - **Created:** {order['created_at']}
209
- - **Updated:** {order['updated_at']}
210
- - **Delivered:** {order['delivered_at'] or 'Not delivered yet'}
211
- """
212
- return details
213
 
 
 
214
  except Exception as e:
215
- return f"Error fetching order details: {str(e)}"
216
-
217
 
218
- # ============================================
219
- # DRIVERS FUNCTIONS
220
- # ============================================
221
 
222
- def get_all_drivers(status_filter="all", vehicle_filter="all", search_term=""):
223
- """Get drivers with filters"""
224
  try:
225
- where_clauses = []
226
- params = []
227
 
228
- if status_filter and status_filter != "all":
229
- where_clauses.append("status = %s")
230
- params.append(status_filter)
231
-
232
- if vehicle_filter and vehicle_filter != "all":
233
- where_clauses.append("vehicle_type = %s")
234
- params.append(vehicle_filter)
235
-
236
- if search_term:
237
- where_clauses.append("(driver_id ILIKE %s OR name ILIKE %s OR phone ILIKE %s OR vehicle_plate ILIKE %s)")
238
- search_pattern = f"%{search_term}%"
239
- params.extend([search_pattern, search_pattern, search_pattern, search_pattern])
240
-
241
- where_sql = " WHERE " + " AND ".join(where_clauses) if where_clauses else ""
242
-
243
- query = f"""
244
  SELECT
245
- driver_id,
246
- name,
247
- phone,
248
  status,
249
- vehicle_type,
250
- vehicle_plate,
251
- current_lat,
252
- current_lng,
253
- last_location_update
254
- FROM drivers
255
- {where_sql}
256
- ORDER BY name ASC
257
- LIMIT 100
258
  """
259
 
260
- results = execute_query(query, tuple(params) if params else ())
 
261
 
262
  if not results:
263
- return [["-", "-", "-", "-", "-", "-", "-"]]
264
 
265
- # Format data for table
266
  data = []
267
  for row in results:
268
- # Format location
269
- if row['current_lat'] and row['current_lng']:
270
- location = f"{row['current_lat']:.4f}, {row['current_lng']:.4f}"
271
- else:
272
- location = "No location"
273
-
274
- # Format last update
275
- last_update = row['last_location_update'].strftime("%Y-%m-%d %H:%M") if row['last_location_update'] else "Never"
276
-
277
  data.append([
278
- row['driver_id'],
279
- row['name'],
280
- row['phone'] or "N/A",
281
  row['status'],
282
- f"{row['vehicle_type']} - {row['vehicle_plate'] or 'N/A'}",
283
- location,
284
- last_update
285
  ])
286
 
287
  return data
288
  except Exception as e:
289
- print(f"Error fetching drivers: {e}")
290
- return [[f"Error: {str(e)}", "", "", "", "", "", ""]]
291
-
292
-
293
- def get_driver_details(driver_id):
294
- """Get complete driver details"""
295
- if not driver_id or driver_id == "-":
296
- return "Select a driver from the table to view details"
297
-
298
- try:
299
- query = """
300
- SELECT * FROM drivers WHERE driver_id = %s
301
- """
302
- results = execute_query(query, (driver_id,))
303
-
304
- if not results:
305
- return f"Driver {driver_id} not found"
306
-
307
- driver = results[0]
308
-
309
- # Parse skills (handle both list and JSON string)
310
- if driver['skills']:
311
- if isinstance(driver['skills'], list):
312
- skills = driver['skills']
313
- else:
314
- skills = json.loads(driver['skills'])
315
- else:
316
- skills = []
317
- skills_str = ", ".join(skills) if skills else "None"
318
-
319
- # Format the details nicely
320
- details = f"""
321
- # Driver Details: {driver_id}
322
-
323
- ## Personal Information
324
- - **Name:** {driver['name']}
325
- - **Phone:** {driver['phone'] or 'N/A'}
326
- - **Email:** {driver['email'] or 'N/A'}
327
-
328
- ## Current Location
329
- - **Coordinates:** ({driver['current_lat']}, {driver['current_lng']})
330
- - **Last Update:** {driver['last_location_update'] or 'Never updated'}
331
-
332
- ## Status
333
- - **Status:** {driver['status']}
334
-
335
- ## Vehicle Information
336
- - **Type:** {driver['vehicle_type']}
337
- - **Plate:** {driver['vehicle_plate'] or 'N/A'}
338
- - **Capacity (kg):** {driver['capacity_kg'] or 'N/A'}
339
- - **Capacity (mΒ³):** {driver['capacity_m3'] or 'N/A'}
340
-
341
- ## Skills & Certifications
342
- {skills_str}
343
-
344
- ## Timestamps
345
- - **Created:** {driver['created_at']}
346
- - **Updated:** {driver['updated_at']}
347
- """
348
- return details
349
-
350
- except Exception as e:
351
- return f"Error fetching driver details: {str(e)}"
352
-
353
-
354
- def update_order_ui(order_id, **fields):
355
- """Update order via UI"""
356
- from chat.tools import execute_tool
357
-
358
- if not order_id:
359
- return {"success": False, "message": "Order ID is required"}
360
-
361
- # Build tool input
362
- tool_input = {"order_id": order_id}
363
- tool_input.update(fields)
364
-
365
- # Call update tool
366
- result = execute_tool("update_order", tool_input)
367
-
368
- return result
369
-
370
-
371
- def delete_order_ui(order_id):
372
- """Delete order via UI"""
373
- from chat.tools import execute_tool
374
-
375
- if not order_id:
376
- return {"success": False, "message": "Order ID is required"}
377
-
378
- # Call delete tool with confirmation
379
- result = execute_tool("delete_order", {"order_id": order_id, "confirm": True})
380
-
381
- return result
382
-
383
-
384
- def update_driver_ui(driver_id, **fields):
385
- """Update driver via UI"""
386
- from chat.tools import execute_tool
387
-
388
- if not driver_id:
389
- return {"success": False, "message": "Driver ID is required"}
390
-
391
- # Build tool input
392
- tool_input = {"driver_id": driver_id}
393
- tool_input.update(fields)
394
-
395
- # Call update tool
396
- result = execute_tool("update_driver", tool_input)
397
-
398
- return result
399
-
400
-
401
- def delete_driver_ui(driver_id):
402
- """Delete driver via UI"""
403
- from chat.tools import execute_tool
404
-
405
- if not driver_id:
406
- return {"success": False, "message": "Driver ID is required"}
407
-
408
- # Call delete tool with confirmation
409
- result = execute_tool("delete_driver", {"driver_id": driver_id, "confirm": True})
410
-
411
- return result
412
 
413
 
414
  # ============================================
415
  # CHAT FUNCTIONS
416
  # ============================================
417
 
 
 
 
 
 
418
  def get_api_status():
419
  """Get API status for chat"""
 
420
  full_status = chat_engine.get_full_status()
421
  selected = full_status["selected"]
422
  claude_status = full_status["claude"]["status"]
423
  gemini_status = full_status["gemini"]["status"]
 
424
  geocoding_status = geocoding_service.get_status()
425
 
 
426
  claude_marker = "🎯 **ACTIVE** - " if selected == "anthropic" else ""
427
  gemini_marker = "🎯 **ACTIVE** - " if selected == "gemini" else ""
428
 
429
- return f"""**AI Provider:**
 
 
 
 
 
 
 
 
 
 
 
 
430
 
431
- **Claude:** {claude_marker}{claude_status}
432
- **Gemini:** {gemini_marker}{gemini_status}
433
 
434
- **Geocoding:** {geocoding_status}
 
435
  """
436
 
437
 
438
- def handle_chat_message(message, session_id):
439
- """Handle chat message from user"""
440
- if session_id not in SESSIONS:
441
- SESSIONS[session_id] = ConversationManager()
442
- welcome = chat_engine.get_welcome_message()
443
- SESSIONS[session_id].add_message("assistant", welcome)
444
 
445
- conversation = SESSIONS[session_id]
 
 
446
 
 
 
 
447
  if not message.strip():
448
- return conversation.get_formatted_history(), conversation.get_tool_calls(), session_id
449
 
450
- response, tool_calls = chat_engine.process_message(message, conversation)
451
- return conversation.get_formatted_history(), conversation.get_tool_calls(), session_id
452
 
 
 
453
 
454
- def reset_conversation(session_id):
 
455
  """Reset conversation to start fresh"""
456
- new_session_id = str(uuid.uuid4())
457
  new_conversation = ConversationManager()
 
 
458
  welcome = chat_engine.get_welcome_message()
459
  new_conversation.add_message("assistant", welcome)
460
- SESSIONS[new_session_id] = new_conversation
461
 
462
  return (
463
  new_conversation.get_formatted_history(),
464
- [],
465
- new_session_id
466
  )
467
 
468
 
469
  def get_initial_chat():
470
  """Get initial chat state with welcome message"""
471
- session_id = str(uuid.uuid4())
472
  conversation = ConversationManager()
473
  welcome = chat_engine.get_welcome_message()
474
  conversation.add_message("assistant", welcome)
475
- SESSIONS[session_id] = conversation
476
 
477
- return conversation.get_formatted_history(), [], session_id
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
478
 
479
 
480
  # ============================================
@@ -482,619 +302,212 @@ def get_initial_chat():
482
  # ============================================
483
 
484
  def create_interface():
485
- """Create the Gradio interface with 3 enhanced tabs"""
486
 
487
- with gr.Blocks(theme=gr.themes.Soft(), title="FleetMind Dispatch System") as app:
488
 
489
- gr.Markdown("# 🚚 FleetMind Dispatch System")
490
- gr.Markdown("*AI-Powered Delivery Coordination*")
491
 
492
  with gr.Tabs():
493
 
494
  # ==========================================
495
- # TAB 1: CHAT
496
  # ==========================================
497
- with gr.Tab("πŸ’¬ Chat Assistant"):
498
- provider_name = chat_engine.get_provider_name()
499
- model_name = chat_engine.get_model_name()
500
-
501
- gr.Markdown(f"### AI Dispatch Assistant")
502
- gr.Markdown(f"*Powered by {provider_name} ({model_name})*")
503
-
504
- # Quick Action Buttons
505
- gr.Markdown("**Quick Actions:**")
506
-
507
- # Row 1: Create and View
508
- with gr.Row():
509
- quick_create_order = gr.Button("πŸ“¦ Create Order", size="sm")
510
- quick_view_orders = gr.Button("πŸ“‹ View Orders", size="sm")
511
- quick_view_drivers = gr.Button("πŸ‘₯ View Drivers", size="sm")
512
- quick_check_status = gr.Button("πŸ“Š Check Status", size="sm")
513
-
514
- # Row 2: Order Management
515
  with gr.Row():
516
- quick_update_order = gr.Button("✏️ Update Order", size="sm", variant="secondary")
517
- quick_delete_order = gr.Button("πŸ—‘οΈ Delete Order", size="sm", variant="secondary")
518
- quick_update_driver = gr.Button("✏️ Update Driver", size="sm", variant="secondary")
519
- quick_delete_driver = gr.Button("πŸ—‘οΈ Delete Driver", size="sm", variant="secondary")
520
-
521
- # Chat interface
522
- chatbot = gr.Chatbot(
523
- label="Chat with AI Assistant",
524
- height=600,
525
- type="messages",
526
- show_copy_button=True,
527
- avatar_images=("πŸ‘€", "πŸ€–")
528
- )
529
-
530
- msg_input = gr.Textbox(
531
- placeholder="Type your message here... (e.g., 'Create order for John at 123 Main St' or 'Show me available drivers')",
532
- label="Your Message",
533
- lines=3
534
- )
535
-
536
- with gr.Row():
537
- send_btn = gr.Button("πŸ“€ Send Message", variant="primary", scale=3)
538
- clear_btn = gr.Button("πŸ—‘οΈ Clear Chat", scale=1)
539
-
540
- # API Status in accordion
541
- with gr.Accordion("πŸ”§ System Status", open=False):
542
- api_status = gr.Markdown(get_api_status())
543
-
544
- # Tool usage display
545
- with gr.Accordion("πŸ› οΈ Tool Usage Log", open=False):
546
- gr.Markdown("*See what tools the AI is using behind the scenes*")
547
- tool_display = gr.JSON(label="Tools Called", value=[])
548
-
549
- # Session state
550
- session_id_state = gr.State(value=None)
551
-
552
- # Event handlers
553
- def send_message(message, sess_id):
554
- if sess_id is None:
555
- sess_id = str(uuid.uuid4())
556
- SESSIONS[sess_id] = ConversationManager()
557
- welcome = chat_engine.get_welcome_message()
558
- SESSIONS[sess_id].add_message("assistant", welcome)
559
-
560
- chat_history, tools, new_sess_id = handle_chat_message(message, sess_id)
561
- return chat_history, tools, new_sess_id, ""
562
-
563
- # Quick action functions
564
- def quick_action(prompt):
565
- return prompt
566
-
567
- quick_create_order.click(
568
- fn=lambda: "Create a new order",
569
- outputs=msg_input
570
- )
571
-
572
- quick_view_orders.click(
573
- fn=lambda: "Show me all orders",
574
- outputs=msg_input
575
- )
576
-
577
- quick_view_drivers.click(
578
- fn=lambda: "Show me all available drivers",
579
- outputs=msg_input
580
- )
581
-
582
- quick_check_status.click(
583
- fn=lambda: "What is the current status of orders and drivers?",
584
- outputs=msg_input
585
- )
586
-
587
- quick_update_order.click(
588
- fn=lambda: "Update order [ORDER_ID] - change status to [STATUS]",
589
- outputs=msg_input
590
- )
591
-
592
- quick_delete_order.click(
593
- fn=lambda: "Delete order [ORDER_ID]",
594
- outputs=msg_input
595
- )
596
-
597
- quick_update_driver.click(
598
- fn=lambda: "Update driver [DRIVER_ID] - change status to [STATUS]",
599
- outputs=msg_input
600
- )
601
-
602
- quick_delete_driver.click(
603
- fn=lambda: "Delete driver [DRIVER_ID]",
604
- outputs=msg_input
605
- )
606
 
607
- send_btn.click(
608
- fn=send_message,
609
- inputs=[msg_input, session_id_state],
610
- outputs=[chatbot, tool_display, session_id_state, msg_input]
611
- )
612
 
613
- msg_input.submit(
614
- fn=send_message,
615
- inputs=[msg_input, session_id_state],
616
- outputs=[chatbot, tool_display, session_id_state, msg_input]
617
- )
618
 
619
- clear_btn.click(
620
- fn=reset_conversation,
621
- inputs=[session_id_state],
622
- outputs=[chatbot, tool_display, session_id_state]
623
  )
624
 
625
  # ==========================================
626
- # TAB 2: ORDERS
627
  # ==========================================
628
- with gr.Tab("πŸ“¦ Orders Management"):
629
- gr.Markdown("### Orders Dashboard")
630
-
631
- # Statistics Cards
632
- def update_order_stats():
633
- stats = get_orders_stats()
634
- return (
635
- f"**Total:** {stats['total']}",
636
- f"**Pending:** {stats['pending']}",
637
- f"**In Transit:** {stats['in_transit']}",
638
- f"**Delivered:** {stats['delivered']}"
639
- )
640
-
641
- with gr.Row():
642
- stat_total = gr.Markdown("**Total:** 0")
643
- stat_pending = gr.Markdown("**Pending:** 0")
644
- stat_transit = gr.Markdown("**In Transit:** 0")
645
- stat_delivered = gr.Markdown("**Delivered:** 0")
646
-
647
- gr.Markdown("---")
648
 
649
- # Filters
650
- gr.Markdown("**Filters:**")
651
  with gr.Row():
652
- status_filter = gr.Dropdown(
653
- choices=["all", "pending", "assigned", "in_transit", "delivered", "failed", "cancelled"],
654
- value="all",
655
- label="Status",
656
- scale=1
657
- )
658
- priority_filter = gr.Dropdown(
659
- choices=["all", "standard", "express", "urgent"],
660
- value="all",
661
- label="Priority",
662
- scale=1
663
- )
664
- payment_filter = gr.Dropdown(
665
- choices=["all", "pending", "paid", "cod"],
666
- value="all",
667
- label="Payment",
668
- scale=1
669
- )
670
- search_orders = gr.Textbox(
671
- placeholder="Search by Order ID, Customer, Phone...",
672
- label="Search",
673
- scale=2
674
  )
 
 
675
 
676
- with gr.Row():
677
- apply_filters_btn = gr.Button("πŸ” Apply Filters", variant="primary", scale=1)
678
- refresh_orders_btn = gr.Button("πŸ”„ Refresh", scale=1)
679
 
680
- # Orders Table
681
  orders_table = gr.Dataframe(
682
- headers=["Order ID", "Customer", "Address", "Status", "Priority", "Driver", "Deadline"],
683
- datatype=["str", "str", "str", "str", "str", "str", "str"],
684
- label="Orders List (Click row to view details)",
685
  value=get_all_orders(),
686
  interactive=False,
687
  wrap=True
688
  )
689
 
690
- # Order Details
691
- gr.Markdown("---")
692
- gr.Markdown("**Order Details:**")
693
- order_details = gr.Markdown("*Select an order from the table above to view full details*")
694
-
695
- # Edit/Delete Actions
696
- gr.Markdown("---")
697
- gr.Markdown("**Order Actions:**")
698
-
699
- with gr.Row():
700
- selected_order_id_edit = gr.Textbox(label="Order ID to Edit", placeholder="ORD-XXXXXXXX", scale=2)
701
- edit_order_btn = gr.Button("✏️ Edit Order", variant="secondary", scale=1)
702
- delete_order_btn = gr.Button("πŸ—‘οΈ Delete Order", variant="stop", scale=1)
703
-
704
- # Edit Form (in accordion)
705
- with gr.Accordion("Edit Order Form", open=False) as edit_accordion:
706
- with gr.Row():
707
- edit_customer_name = gr.Textbox(label="Customer Name")
708
- edit_customer_phone = gr.Textbox(label="Customer Phone")
709
- with gr.Row():
710
- edit_status = gr.Dropdown(
711
- choices=["pending", "assigned", "in_transit", "delivered", "failed", "cancelled"],
712
- label="Status"
713
- )
714
- edit_priority = gr.Dropdown(
715
- choices=["standard", "express", "urgent"],
716
- label="Priority"
717
- )
718
- with gr.Row():
719
- edit_payment_status = gr.Dropdown(
720
- choices=["pending", "paid", "cod"],
721
- label="Payment Status"
722
- )
723
- edit_weight_kg = gr.Number(label="Weight (kg)")
724
- edit_special_instructions = gr.Textbox(label="Special Instructions", lines=2)
725
-
726
- save_order_btn = gr.Button("πŸ’Ύ Save Changes", variant="primary")
727
-
728
- # Action Results
729
- action_result = gr.Markdown("")
730
-
731
- # Event handlers
732
- def filter_and_update_orders(status, priority, payment, search):
733
- return get_all_orders(status, priority, payment, search)
734
-
735
- def refresh_orders_and_stats(status, priority, payment, search):
736
- stats = update_order_stats()
737
- table = get_all_orders(status, priority, payment, search)
738
- return stats[0], stats[1], stats[2], stats[3], table
739
-
740
- def show_order_details(evt: gr.SelectData, table_data):
741
- try:
742
- # table_data is a pandas DataFrame from Gradio
743
- if hasattr(table_data, 'iloc'):
744
- # DataFrame - use iloc to access row and column
745
- order_id = table_data.iloc[evt.index[0], 0]
746
- else:
747
- # List of lists - use standard indexing
748
- order_id = table_data[evt.index[0]][0]
749
- return get_order_details(order_id)
750
- except Exception as e:
751
- return f"Error: {str(e)}"
752
-
753
- apply_filters_btn.click(
754
- fn=filter_and_update_orders,
755
- inputs=[status_filter, priority_filter, payment_filter, search_orders],
756
- outputs=orders_table
757
- )
758
-
759
- refresh_orders_btn.click(
760
- fn=refresh_orders_and_stats,
761
- inputs=[status_filter, priority_filter, payment_filter, search_orders],
762
- outputs=[stat_total, stat_pending, stat_transit, stat_delivered, orders_table]
763
- )
764
-
765
- orders_table.select(
766
- fn=show_order_details,
767
- inputs=[orders_table],
768
- outputs=order_details
769
  )
770
 
771
- # Edit/Delete handlers
772
- def handle_edit_order(order_id):
773
- if not order_id:
774
- return "Please enter an Order ID"
775
- # Fetch order details and populate form
776
- query = "SELECT * FROM orders WHERE order_id = %s"
777
- from database.connection import execute_query
778
- results = execute_query(query, (order_id,))
779
- if not results:
780
- return "Order not found"
781
- order = results[0]
782
- return (
783
- order.get('customer_name', ''),
784
- order.get('customer_phone', ''),
785
- order.get('status', ''),
786
- order.get('priority', ''),
787
- order.get('payment_status', ''),
788
- order.get('weight_kg', 0),
789
- order.get('special_instructions', ''),
790
- "Order loaded. Update fields and click Save Changes."
791
- )
792
-
793
- def handle_save_order(order_id, name, phone, status, priority, payment, weight, instructions, status_f, priority_f, payment_f, search):
794
- if not order_id:
795
- return "Please enter an Order ID", get_all_orders(status_f, priority_f, payment_f, search)
796
-
797
- fields = {}
798
- if name:
799
- fields['customer_name'] = name
800
- if phone:
801
- fields['customer_phone'] = phone
802
- if status:
803
- fields['status'] = status
804
- if priority:
805
- fields['priority'] = priority
806
- if payment:
807
- fields['payment_status'] = payment
808
- if weight:
809
- fields['weight_kg'] = float(weight)
810
- if instructions:
811
- fields['special_instructions'] = instructions
812
-
813
- result = update_order_ui(order_id, **fields)
814
-
815
- # Refresh table
816
- refreshed_table = get_all_orders(status_f, priority_f, payment_f, search)
817
-
818
- if result['success']:
819
- return f"βœ… {result['message']}", refreshed_table
820
- else:
821
- return f"❌ {result.get('error', 'Update failed')}", refreshed_table
822
-
823
- def handle_delete_order(order_id, status_f, priority_f, payment_f, search):
824
- if not order_id:
825
- return "Please enter an Order ID", get_all_orders(status_f, priority_f, payment_f, search)
826
-
827
- result = delete_order_ui(order_id)
828
-
829
- # Refresh table
830
- refreshed_table = get_all_orders(status_f, priority_f, payment_f, search)
831
-
832
- if result['success']:
833
- return f"βœ… {result['message']}", refreshed_table
834
- else:
835
- return f"❌ {result.get('error', 'Deletion failed')}", refreshed_table
836
-
837
- edit_order_btn.click(
838
- fn=handle_edit_order,
839
- inputs=[selected_order_id_edit],
840
- outputs=[edit_customer_name, edit_customer_phone, edit_status, edit_priority,
841
- edit_payment_status, edit_weight_kg, edit_special_instructions, action_result]
842
  )
843
 
844
- save_order_btn.click(
845
- fn=handle_save_order,
846
- inputs=[selected_order_id_edit, edit_customer_name, edit_customer_phone, edit_status,
847
- edit_priority, edit_payment_status, edit_weight_kg, edit_special_instructions,
848
- status_filter, priority_filter, payment_filter, search_orders],
849
- outputs=[action_result, orders_table]
850
  )
851
 
852
- delete_order_btn.click(
853
- fn=handle_delete_order,
854
- inputs=[selected_order_id_edit, status_filter, priority_filter, payment_filter, search_orders],
855
- outputs=[action_result, orders_table]
 
 
856
  )
857
 
858
  # ==========================================
859
- # TAB 3: DRIVERS
860
  # ==========================================
861
- with gr.Tab("πŸ‘₯ Drivers Management"):
862
- gr.Markdown("### Drivers Dashboard")
863
-
864
- # Statistics Cards
865
- def update_driver_stats():
866
- stats = get_drivers_stats()
867
- return (
868
- f"**Total:** {stats['total']}",
869
- f"**Active:** {stats['active']}",
870
- f"**Busy:** {stats['busy']}",
871
- f"**Offline:** {stats['offline']}"
872
- )
873
 
874
- with gr.Row():
875
- driver_stat_total = gr.Markdown("**Total:** 0")
876
- driver_stat_active = gr.Markdown("**Active:** 0")
877
- driver_stat_busy = gr.Markdown("**Busy:** 0")
878
- driver_stat_offline = gr.Markdown("**Offline:** 0")
879
 
880
- gr.Markdown("---")
 
881
 
882
- # Filters
883
- gr.Markdown("**Filters:**")
884
- with gr.Row():
885
- driver_status_filter = gr.Dropdown(
886
- choices=["all", "active", "busy", "offline", "unavailable"],
887
- value="all",
888
- label="Status",
889
- scale=1
890
- )
891
- vehicle_filter = gr.Dropdown(
892
- choices=["all", "van", "truck", "car", "motorcycle"],
893
- value="all",
894
- label="Vehicle Type",
895
- scale=1
896
- )
897
- search_drivers = gr.Textbox(
898
- placeholder="Search by Driver ID, Name, Phone, Plate...",
899
- label="Search",
900
- scale=2
901
- )
902
-
903
- with gr.Row():
904
- apply_driver_filters_btn = gr.Button("πŸ” Apply Filters", variant="primary", scale=1)
905
- refresh_drivers_btn = gr.Button("πŸ”„ Refresh", scale=1)
906
-
907
- # Drivers Table
908
- drivers_table = gr.Dataframe(
909
- headers=["Driver ID", "Name", "Phone", "Status", "Vehicle", "Location", "Last Update"],
910
- datatype=["str", "str", "str", "str", "str", "str", "str"],
911
- label="Drivers List (Click row to view details)",
912
- value=get_all_drivers(),
913
- interactive=False,
914
- wrap=True
915
  )
916
 
917
- # Driver Details
918
- gr.Markdown("---")
919
- gr.Markdown("**Driver Details:**")
920
- driver_details = gr.Markdown("*Select a driver from the table above to view full details*")
921
-
922
- # Edit/Delete Actions
923
- gr.Markdown("---")
924
- gr.Markdown("**Driver Actions:**")
925
 
926
  with gr.Row():
927
- selected_driver_id_edit = gr.Textbox(label="Driver ID to Edit", placeholder="DRV-XXXXXXXX", scale=2)
928
- edit_driver_btn = gr.Button("✏️ Edit Driver", variant="secondary", scale=1)
929
- delete_driver_btn = gr.Button("πŸ—‘οΈ Delete Driver", variant="stop", scale=1)
930
-
931
- # Edit Form (in accordion)
932
- with gr.Accordion("Edit Driver Form", open=False) as driver_edit_accordion:
933
- with gr.Row():
934
- edit_driver_name = gr.Textbox(label="Driver Name")
935
- edit_driver_phone = gr.Textbox(label="Phone")
936
- with gr.Row():
937
- edit_driver_email = gr.Textbox(label="Email")
938
- edit_driver_status = gr.Dropdown(
939
- choices=["active", "busy", "offline", "unavailable"],
940
- label="Status"
941
- )
942
- with gr.Row():
943
- edit_vehicle_type = gr.Textbox(label="Vehicle Type")
944
- edit_vehicle_plate = gr.Textbox(label="Vehicle Plate")
945
- with gr.Row():
946
- edit_capacity_kg = gr.Number(label="Capacity (kg)")
947
- edit_capacity_m3 = gr.Number(label="Capacity (mΒ³)")
948
 
949
- save_driver_btn = gr.Button("πŸ’Ύ Save Changes", variant="primary")
 
950
 
951
- # Action Results
952
- driver_action_result = gr.Markdown("")
953
 
954
  # Event handlers
955
- def filter_and_update_drivers(status, vehicle, search):
956
- return get_all_drivers(status, vehicle, search)
957
-
958
- def refresh_drivers_and_stats(status, vehicle, search):
959
- stats = update_driver_stats()
960
- table = get_all_drivers(status, vehicle, search)
961
- return stats[0], stats[1], stats[2], stats[3], table
962
-
963
- def show_driver_details(evt: gr.SelectData, table_data):
964
- try:
965
- # table_data is a pandas DataFrame from Gradio
966
- if hasattr(table_data, 'iloc'):
967
- # DataFrame - use iloc to access row and column
968
- driver_id = table_data.iloc[evt.index[0], 0]
969
- else:
970
- # List of lists - use standard indexing
971
- driver_id = table_data[evt.index[0]][0]
972
- return get_driver_details(driver_id)
973
- except Exception as e:
974
- return f"Error: {str(e)}"
975
-
976
- apply_driver_filters_btn.click(
977
- fn=filter_and_update_drivers,
978
- inputs=[driver_status_filter, vehicle_filter, search_drivers],
979
- outputs=drivers_table
980
- )
981
 
982
- refresh_drivers_btn.click(
983
- fn=refresh_drivers_and_stats,
984
- inputs=[driver_status_filter, vehicle_filter, search_drivers],
985
- outputs=[driver_stat_total, driver_stat_active, driver_stat_busy, driver_stat_offline, drivers_table]
986
  )
987
 
988
- drivers_table.select(
989
- fn=show_driver_details,
990
- inputs=[drivers_table],
991
- outputs=driver_details
992
  )
993
 
994
- # Edit/Delete handlers
995
- def handle_edit_driver(driver_id):
996
- if not driver_id:
997
- return "Please enter a Driver ID"
998
- # Fetch driver details and populate form
999
- query = "SELECT * FROM drivers WHERE driver_id = %s"
1000
- from database.connection import execute_query
1001
- results = execute_query(query, (driver_id,))
1002
- if not results:
1003
- return "Driver not found"
1004
- driver = results[0]
1005
- return (
1006
- driver.get('name', ''),
1007
- driver.get('phone', ''),
1008
- driver.get('email', ''),
1009
- driver.get('status', ''),
1010
- driver.get('vehicle_type', ''),
1011
- driver.get('vehicle_plate', ''),
1012
- driver.get('capacity_kg', 0),
1013
- driver.get('capacity_m3', 0),
1014
- "Driver loaded. Update fields and click Save Changes."
1015
- )
1016
-
1017
- def handle_save_driver(driver_id, name, phone, email, status, vehicle_type, vehicle_plate, capacity_kg, capacity_m3, status_f, vehicle_f, search):
1018
- if not driver_id:
1019
- return "Please enter a Driver ID", get_all_drivers(status_f, vehicle_f, search)
1020
-
1021
- fields = {}
1022
- if name:
1023
- fields['name'] = name
1024
- if phone:
1025
- fields['phone'] = phone
1026
- if email:
1027
- fields['email'] = email
1028
- if status:
1029
- fields['status'] = status
1030
- if vehicle_type:
1031
- fields['vehicle_type'] = vehicle_type
1032
- if vehicle_plate:
1033
- fields['vehicle_plate'] = vehicle_plate
1034
- if capacity_kg:
1035
- fields['capacity_kg'] = float(capacity_kg)
1036
- if capacity_m3:
1037
- fields['capacity_m3'] = float(capacity_m3)
1038
-
1039
- result = update_driver_ui(driver_id, **fields)
1040
-
1041
- # Refresh table
1042
- refreshed_table = get_all_drivers(status_f, vehicle_f, search)
1043
-
1044
- if result['success']:
1045
- return f"βœ… {result['message']}", refreshed_table
1046
- else:
1047
- return f"❌ {result.get('error', 'Update failed')}", refreshed_table
1048
-
1049
- def handle_delete_driver(driver_id, status_f, vehicle_f, search):
1050
- if not driver_id:
1051
- return "Please enter a Driver ID", get_all_drivers(status_f, vehicle_f, search)
1052
-
1053
- result = delete_driver_ui(driver_id)
1054
-
1055
- # Refresh table
1056
- refreshed_table = get_all_drivers(status_f, vehicle_f, search)
1057
-
1058
- if result['success']:
1059
- return f"βœ… {result['message']}", refreshed_table
1060
- else:
1061
- return f"❌ {result.get('error', 'Deletion failed')}", refreshed_table
1062
-
1063
- edit_driver_btn.click(
1064
- fn=handle_edit_driver,
1065
- inputs=[selected_driver_id_edit],
1066
- outputs=[edit_driver_name, edit_driver_phone, edit_driver_email, edit_driver_status,
1067
- edit_vehicle_type, edit_vehicle_plate, edit_capacity_kg, edit_capacity_m3, driver_action_result]
1068
  )
1069
 
1070
- save_driver_btn.click(
1071
- fn=handle_save_driver,
1072
- inputs=[selected_driver_id_edit, edit_driver_name, edit_driver_phone, edit_driver_email,
1073
- edit_driver_status, edit_vehicle_type, edit_vehicle_plate, edit_capacity_kg, edit_capacity_m3,
1074
- driver_status_filter, vehicle_filter, search_drivers],
1075
- outputs=[driver_action_result, drivers_table]
1076
- )
 
 
 
 
 
 
 
 
 
1077
 
1078
- delete_driver_btn.click(
1079
- fn=handle_delete_driver,
1080
- inputs=[selected_driver_id_edit, driver_status_filter, vehicle_filter, search_drivers],
1081
- outputs=[driver_action_result, drivers_table]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1082
  )
1083
 
1084
  gr.Markdown("---")
1085
- gr.Markdown("*FleetMind v1.0 - AI-Powered Dispatch Coordination*")
1086
-
1087
- # Initialize chat and stats on load
1088
- app.load(
1089
- fn=get_initial_chat,
1090
- outputs=[chatbot, tool_display, session_id_state]
1091
- ).then(
1092
- fn=update_order_stats,
1093
- outputs=[stat_total, stat_pending, stat_transit, stat_delivered]
1094
- ).then(
1095
- fn=update_driver_stats,
1096
- outputs=[driver_stat_total, driver_stat_active, driver_stat_busy, driver_stat_offline]
1097
- )
1098
 
1099
  return app
1100
 
@@ -1105,23 +518,25 @@ def create_interface():
1105
 
1106
  if __name__ == "__main__":
1107
  print("=" * 60)
1108
- print("FleetMind - Starting Enhanced UI")
1109
  print("=" * 60)
1110
 
 
1111
  print("\nChecking database connection...")
1112
  if test_connection():
1113
  print("βœ… Database connected")
1114
  else:
1115
  print("❌ Database connection failed")
 
1116
 
1117
  print("\nStarting Gradio interface...")
1118
  print("=" * 60)
1119
 
 
1120
  app = create_interface()
1121
  app.launch(
1122
- server_name="0.0.0.0",
1123
  server_port=7860,
1124
  share=False,
1125
- show_error=True,
1126
- show_api=False
1127
  )
 
1
  """
2
  FleetMind MCP - Gradio Web Interface
3
+ Simple dashboard to interact with the MCP server and database
4
  """
5
 
6
  import sys
 
18
  from chat.chat_engine import ChatEngine
19
  from chat.conversation import ConversationManager
20
  from chat.geocoding import GeocodingService
 
 
 
 
 
 
 
 
21
 
22
  # ============================================
23
+ # DATABASE FUNCTIONS
24
  # ============================================
25
 
26
+ def get_database_status():
27
+ """Check if database is connected"""
28
  try:
29
+ if test_connection():
30
+ return "βœ… Connected", "success"
31
+ else:
32
+ return "❌ Disconnected", "error"
 
 
 
 
 
 
 
 
 
 
 
33
  except Exception as e:
34
+ return f"❌ Error: {str(e)}", "error"
 
35
 
36
 
37
+ def get_orders_summary():
38
+ """Get summary of orders by status"""
39
  try:
40
  query = """
41
  SELECT
42
+ status,
43
+ COUNT(*) as count
44
+ FROM orders
45
+ GROUP BY status
46
+ ORDER BY count DESC
 
47
  """
48
+ results = execute_query(query)
 
 
 
 
 
 
 
49
 
50
+ if not results:
51
+ return "No orders in database"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
 
53
+ summary = "**Orders Summary:**\n\n"
54
+ for row in results:
55
+ summary += f"- {row['status'].upper()}: {row['count']}\n"
56
 
57
+ return summary
58
+ except Exception as e:
59
+ return f"Error: {str(e)}"
 
60
 
 
61
 
62
+ def get_all_orders():
63
+ """Get all orders from database"""
64
+ try:
65
+ query = """
66
  SELECT
67
  order_id,
68
  customer_name,
69
  delivery_address,
70
  status,
71
  priority,
 
 
72
  created_at
73
  FROM orders
 
74
  ORDER BY created_at DESC
75
+ LIMIT 50
76
  """
77
+ results = execute_query(query)
 
78
 
79
  if not results:
80
+ return [["No orders found", "", "", "", "", ""]]
81
 
82
+ # Convert to list of lists for Gradio dataframe
83
  data = []
84
  for row in results:
 
 
 
 
 
 
 
 
 
 
 
85
  data.append([
86
  row['order_id'],
87
  row['customer_name'],
88
+ row['delivery_address'][:50] + "..." if len(row['delivery_address']) > 50 else row['delivery_address'],
89
  row['status'],
90
  row['priority'],
91
+ str(row['created_at'])
 
92
  ])
93
 
94
  return data
95
  except Exception as e:
96
+ return [[f"Error: {str(e)}", "", "", "", "", ""]]
 
97
 
98
 
99
+ def create_sample_order():
100
+ """Create a sample order for testing"""
 
 
 
101
  try:
102
+ now = datetime.now()
103
+ order_id = f"ORD-{now.strftime('%Y%m%d%H%M%S')}"
104
+
105
  query = """
106
+ INSERT INTO orders (
107
+ order_id, customer_name, customer_phone, customer_email,
108
+ delivery_address, delivery_lat, delivery_lng,
109
+ time_window_start, time_window_end,
110
+ priority, weight_kg, status
111
+ ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
112
  """
 
113
 
114
+ params = (
115
+ order_id,
116
+ "Sample Customer",
117
+ "+1-555-0100",
118
+ "sample@example.com",
119
+ "456 Sample Street, San Francisco, CA 94103",
120
+ 37.7749,
121
+ -122.4194,
122
+ now + timedelta(hours=2),
123
+ now + timedelta(hours=6),
124
+ "standard",
125
+ 10.5,
126
+ "pending"
127
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
 
129
+ execute_write(query, params)
130
+ return f"βœ… Order {order_id} created successfully!", get_all_orders()
131
  except Exception as e:
132
+ return f"❌ Error: {str(e)}", get_all_orders()
 
133
 
 
 
 
134
 
135
+ def search_orders(search_term):
136
+ """Search orders by customer name or order ID"""
137
  try:
138
+ if not search_term:
139
+ return get_all_orders()
140
 
141
+ query = """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
  SELECT
143
+ order_id,
144
+ customer_name,
145
+ delivery_address,
146
  status,
147
+ priority,
148
+ created_at
149
+ FROM orders
150
+ WHERE
151
+ order_id ILIKE %s OR
152
+ customer_name ILIKE %s
153
+ ORDER BY created_at DESC
154
+ LIMIT 50
 
155
  """
156
 
157
+ search_pattern = f"%{search_term}%"
158
+ results = execute_query(query, (search_pattern, search_pattern))
159
 
160
  if not results:
161
+ return [["No matching orders found", "", "", "", "", ""]]
162
 
 
163
  data = []
164
  for row in results:
 
 
 
 
 
 
 
 
 
165
  data.append([
166
+ row['order_id'],
167
+ row['customer_name'],
168
+ row['delivery_address'][:50] + "..." if len(row['delivery_address']) > 50 else row['delivery_address'],
169
  row['status'],
170
+ row['priority'],
171
+ str(row['created_at'])
 
172
  ])
173
 
174
  return data
175
  except Exception as e:
176
+ return [[f"Error: {str(e)}", "", "", "", "", ""]]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
177
 
178
 
179
  # ============================================
180
  # CHAT FUNCTIONS
181
  # ============================================
182
 
183
+ # Initialize chat engine and geocoding service
184
+ chat_engine = ChatEngine()
185
+ geocoding_service = GeocodingService()
186
+
187
+
188
  def get_api_status():
189
  """Get API status for chat"""
190
+ # Get full status for all providers
191
  full_status = chat_engine.get_full_status()
192
  selected = full_status["selected"]
193
  claude_status = full_status["claude"]["status"]
194
  gemini_status = full_status["gemini"]["status"]
195
+
196
  geocoding_status = geocoding_service.get_status()
197
 
198
+ # Mark selected provider
199
  claude_marker = "🎯 **ACTIVE** - " if selected == "anthropic" else ""
200
  gemini_marker = "🎯 **ACTIVE** - " if selected == "gemini" else ""
201
 
202
+ return f"""### API Status
203
+
204
+ **AI Provider:**
205
+
206
+ **Claude (Anthropic):**
207
+ {claude_marker}{claude_status}
208
+
209
+ **Gemini (Google):**
210
+ {gemini_marker}{gemini_status}
211
+
212
+ *πŸ’‘ Switch provider by setting `AI_PROVIDER=anthropic` or `AI_PROVIDER=gemini` in .env*
213
+
214
+ ---
215
 
216
+ **Geocoding:**
 
217
 
218
+ **HERE Maps:**
219
+ {geocoding_status}
220
  """
221
 
222
 
223
+ def handle_chat_message(message, conversation_state):
224
+ """
225
+ Handle chat message from user
 
 
 
226
 
227
+ Args:
228
+ message: User's message
229
+ conversation_state: ConversationManager instance
230
 
231
+ Returns:
232
+ Updated chatbot history, tool display, conversation state
233
+ """
234
  if not message.strip():
235
+ return conversation_state.get_formatted_history(), conversation_state.get_tool_calls(), conversation_state
236
 
237
+ # Process message through chat engine
238
+ response, tool_calls = chat_engine.process_message(message, conversation_state)
239
 
240
+ # Return updated UI
241
+ return conversation_state.get_formatted_history(), conversation_state.get_tool_calls(), conversation_state
242
 
243
+
244
+ def reset_conversation():
245
  """Reset conversation to start fresh"""
 
246
  new_conversation = ConversationManager()
247
+
248
+ # Add welcome message
249
  welcome = chat_engine.get_welcome_message()
250
  new_conversation.add_message("assistant", welcome)
 
251
 
252
  return (
253
  new_conversation.get_formatted_history(),
254
+ [], # Clear tool calls
255
+ new_conversation
256
  )
257
 
258
 
259
  def get_initial_chat():
260
  """Get initial chat state with welcome message"""
 
261
  conversation = ConversationManager()
262
  welcome = chat_engine.get_welcome_message()
263
  conversation.add_message("assistant", welcome)
 
264
 
265
+ return conversation.get_formatted_history(), [], conversation
266
+
267
+
268
+ # ============================================
269
+ # MCP SERVER INFO
270
+ # ============================================
271
+
272
+ def get_mcp_server_info():
273
+ """Get MCP server information"""
274
+ mcp_info = {
275
+ "server_name": "dispatch-coordinator-mcp",
276
+ "version": "1.0.0",
277
+ "status": "Ready",
278
+ "tools": [
279
+ "route_optimizer",
280
+ "geocoder",
281
+ "weather_monitor",
282
+ "traffic_checker",
283
+ "distance_matrix",
284
+ "order_manager"
285
+ ]
286
+ }
287
+
288
+ return f"""
289
+ ### MCP Server Information
290
+
291
+ **Name:** {mcp_info['server_name']}
292
+ **Version:** {mcp_info['version']}
293
+ **Status:** 🟒 {mcp_info['status']}
294
+
295
+ **Available Tools ({len(mcp_info['tools'])}):**
296
+ {chr(10).join([f"- {tool}" for tool in mcp_info['tools']])}
297
+ """
298
 
299
 
300
  # ============================================
 
302
  # ============================================
303
 
304
  def create_interface():
305
+ """Create the Gradio interface"""
306
 
307
+ with gr.Blocks(theme=gr.themes.Soft(), title="FleetMind MCP Dashboard") as app:
308
 
309
+ gr.Markdown("# 🚚 FleetMind MCP Dashboard")
310
+ gr.Markdown("*Autonomous Dispatch Coordinator powered by MCP and PostgreSQL*")
311
 
312
  with gr.Tabs():
313
 
314
  # ==========================================
315
+ # TAB 1: OVERVIEW
316
  # ==========================================
317
+ with gr.Tab("πŸ“Š Overview"):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
318
  with gr.Row():
319
+ with gr.Column(scale=1):
320
+ gr.Markdown("### System Status")
321
+ db_status = gr.Textbox(
322
+ label="Database Connection",
323
+ value=get_database_status()[0],
324
+ interactive=False
325
+ )
326
+ refresh_status_btn = gr.Button("πŸ”„ Refresh Status", size="sm")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
327
 
328
+ gr.Markdown("---")
329
+ orders_summary = gr.Markdown(get_orders_summary())
 
 
 
330
 
331
+ with gr.Column(scale=2):
332
+ mcp_info = gr.Markdown(get_mcp_server_info())
 
 
 
333
 
334
+ # Refresh status button action
335
+ refresh_status_btn.click(
336
+ fn=lambda: get_database_status()[0],
337
+ outputs=db_status
338
  )
339
 
340
  # ==========================================
341
+ # TAB 2: ORDERS MANAGEMENT
342
  # ==========================================
343
+ with gr.Tab("πŸ“¦ Orders"):
344
+ gr.Markdown("### Orders Management")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
345
 
 
 
346
  with gr.Row():
347
+ search_box = gr.Textbox(
348
+ placeholder="Search by Order ID or Customer Name...",
349
+ label="Search Orders",
350
+ scale=3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
351
  )
352
+ search_btn = gr.Button("πŸ” Search", scale=1)
353
+ create_btn = gr.Button("βž• Create Sample Order", scale=1, variant="primary")
354
 
355
+ create_result = gr.Textbox(label="Result", visible=False)
 
 
356
 
 
357
  orders_table = gr.Dataframe(
358
+ headers=["Order ID", "Customer", "Delivery Address", "Status", "Priority", "Created At"],
359
+ datatype=["str", "str", "str", "str", "str", "str"],
360
+ label="Orders List",
361
  value=get_all_orders(),
362
  interactive=False,
363
  wrap=True
364
  )
365
 
366
+ refresh_orders_btn = gr.Button("πŸ”„ Refresh Orders")
367
+
368
+ # Button actions
369
+ create_btn.click(
370
+ fn=create_sample_order,
371
+ outputs=[create_result, orders_table]
372
+ ).then(
373
+ fn=lambda: gr.update(visible=True),
374
+ outputs=create_result
375
+ ).then(
376
+ fn=lambda: get_orders_summary(),
377
+ outputs=orders_summary
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
378
  )
379
 
380
+ search_btn.click(
381
+ fn=search_orders,
382
+ inputs=search_box,
383
+ outputs=orders_table
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
384
  )
385
 
386
+ search_box.submit(
387
+ fn=search_orders,
388
+ inputs=search_box,
389
+ outputs=orders_table
 
 
390
  )
391
 
392
+ refresh_orders_btn.click(
393
+ fn=get_all_orders,
394
+ outputs=orders_table
395
+ ).then(
396
+ fn=lambda: get_orders_summary(),
397
+ outputs=orders_summary
398
  )
399
 
400
  # ==========================================
401
+ # TAB 3: AI CHAT
402
  # ==========================================
403
+ with gr.Tab("πŸ’¬ Chat"):
404
+ provider_name = chat_engine.get_provider_name()
405
+ model_name = chat_engine.get_model_name()
 
 
 
 
 
 
 
 
 
406
 
407
+ gr.Markdown(f"### AI Order Assistant")
408
+ gr.Markdown(f"*Powered by: **{provider_name}** ({model_name})*")
 
 
 
409
 
410
+ # API Status
411
+ api_status = gr.Markdown(get_api_status())
412
 
413
+ # Chat interface
414
+ chatbot = gr.Chatbot(
415
+ label="Order Assistant",
416
+ height=500,
417
+ type="messages",
418
+ show_copy_button=True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
419
  )
420
 
421
+ msg_input = gr.Textbox(
422
+ placeholder="e.g., 'Create an order for John Doe at 123 Main St, deliver by 5 PM'",
423
+ label="Your Message",
424
+ lines=2
425
+ )
 
 
 
426
 
427
  with gr.Row():
428
+ send_btn = gr.Button("πŸ“€ Send", variant="primary", scale=2)
429
+ clear_btn = gr.Button("πŸ”„ Clear Chat", scale=1)
430
+
431
+ # Tool usage display (reasoning transparency)
432
+ with gr.Accordion("πŸ”§ Tool Usage (AI Reasoning)", open=False):
433
+ gr.Markdown("See what tools the AI is using behind the scenes:")
434
+ tool_display = gr.JSON(label="Tools Called")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
435
 
436
+ # Conversation state
437
+ conversation_state = gr.State(value=None)
438
 
439
+ # Initialize with welcome message
440
+ chatbot.value, tool_display.value, conversation_state.value = get_initial_chat()
441
 
442
  # Event handlers
443
+ def send_message(message, conv_state):
444
+ """Handle send button click"""
445
+ chat_history, tools, new_state = handle_chat_message(message, conv_state)
446
+ return chat_history, tools, new_state, "" # Clear input
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
447
 
448
+ send_btn.click(
449
+ fn=send_message,
450
+ inputs=[msg_input, conversation_state],
451
+ outputs=[chatbot, tool_display, conversation_state, msg_input]
452
  )
453
 
454
+ msg_input.submit(
455
+ fn=send_message,
456
+ inputs=[msg_input, conversation_state],
457
+ outputs=[chatbot, tool_display, conversation_state, msg_input]
458
  )
459
 
460
+ clear_btn.click(
461
+ fn=reset_conversation,
462
+ outputs=[chatbot, tool_display, conversation_state]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
463
  )
464
 
465
+ # ==========================================
466
+ # TAB 4: MCP TOOLS (Coming Soon)
467
+ # ==========================================
468
+ with gr.Tab("πŸ”§ MCP Tools"):
469
+ gr.Markdown("### MCP Tools")
470
+ gr.Markdown("*MCP tool integration coming soon...*")
471
+
472
+ gr.Markdown("""
473
+ Available tools:
474
+ - **route_optimizer** - Optimize delivery routes
475
+ - **geocoder** - Convert addresses to coordinates
476
+ - **weather_monitor** - Check weather conditions
477
+ - **traffic_checker** - Monitor traffic conditions
478
+ - **distance_matrix** - Calculate distances
479
+ - **order_manager** - Manage orders via MCP
480
+ """)
481
 
482
+ # ==========================================
483
+ # TAB 5: DATABASE INFO
484
+ # ==========================================
485
+ with gr.Tab("πŸ’Ύ Database"):
486
+ gr.Markdown("### Database Information")
487
+
488
+ db_info = gr.Markdown(f"""
489
+ **Database:** PostgreSQL
490
+ **Name:** fleetmind
491
+ **Host:** localhost
492
+ **Port:** 5432
493
+
494
+ **Tables:**
495
+ - orders (26 columns)
496
+ - drivers (coming soon)
497
+ - assignments (coming soon)
498
+ - exceptions (coming soon)
499
+ """)
500
+
501
+ test_db_btn = gr.Button("πŸ§ͺ Test Connection", variant="primary")
502
+ test_result = gr.Textbox(label="Test Result", interactive=False)
503
+
504
+ test_db_btn.click(
505
+ fn=lambda: "βœ… Connection successful!" if test_connection() else "❌ Connection failed",
506
+ outputs=test_result
507
  )
508
 
509
  gr.Markdown("---")
510
+ gr.Markdown("*FleetMind MCP v1.0.0 - Built with Gradio, PostgreSQL, and FastMCP*")
 
 
 
 
 
 
 
 
 
 
 
 
511
 
512
  return app
513
 
 
518
 
519
  if __name__ == "__main__":
520
  print("=" * 60)
521
+ print("FleetMind MCP - Starting Gradio Server")
522
  print("=" * 60)
523
 
524
+ # Check database connection
525
  print("\nChecking database connection...")
526
  if test_connection():
527
  print("βœ… Database connected")
528
  else:
529
  print("❌ Database connection failed")
530
+ print("Please check your .env file and PostgreSQL server")
531
 
532
  print("\nStarting Gradio interface...")
533
  print("=" * 60)
534
 
535
+ # Create and launch the interface
536
  app = create_interface()
537
  app.launch(
538
+ server_name="0.0.0.0", # Allow external connections for HF Spaces
539
  server_port=7860,
540
  share=False,
541
+ show_error=True
 
542
  )