|
|
""" |
|
|
Google Gemini provider for FleetMind chat |
|
|
""" |
|
|
|
|
|
import os |
|
|
import logging |
|
|
from typing import Tuple, List, Dict |
|
|
import google.generativeai as genai |
|
|
from google.generativeai.types import HarmCategory, HarmBlockThreshold |
|
|
|
|
|
from chat.providers.base_provider import AIProvider |
|
|
from chat.tools import execute_tool |
|
|
|
|
|
logger = logging.getLogger(__name__) |
|
|
|
|
|
|
|
|
class GeminiProvider(AIProvider): |
|
|
"""Google Gemini AI provider""" |
|
|
|
|
|
def __init__(self): |
|
|
self.api_key = os.getenv("GOOGLE_API_KEY", "") |
|
|
self.api_available = bool(self.api_key and not self.api_key.startswith("your_")) |
|
|
self.model_name = "gemini-2.0-flash" |
|
|
self.model = None |
|
|
self._initialized = False |
|
|
|
|
|
|
|
|
key_status = "not set" if not self.api_key else f"set ({len(self.api_key)} chars)" |
|
|
logger.info(f"GeminiProvider init: GOOGLE_API_KEY {key_status}") |
|
|
|
|
|
if not self.api_available: |
|
|
logger.warning("GeminiProvider: GOOGLE_API_KEY not configured") |
|
|
else: |
|
|
logger.info("GeminiProvider: Ready (will initialize on first use)") |
|
|
|
|
|
def _get_system_prompt(self) -> str: |
|
|
"""Get the system prompt for Gemini""" |
|
|
return """You are an AI assistant for FleetMind, a delivery dispatch system. |
|
|
|
|
|
**π¨ CRITICAL RULES - READ CAREFULLY:** |
|
|
|
|
|
1. **NEVER return text in the middle of tool calls** |
|
|
- If you need to call multiple tools, call them ALL in sequence |
|
|
- Only return text AFTER all tools are complete |
|
|
|
|
|
2. **Order Creation MUST be a single automated flow:** |
|
|
- Step 1: Call geocode_address (get coordinates) |
|
|
- Step 2: IMMEDIATELY call create_order (save to database) |
|
|
- Step 3: ONLY THEN return success message |
|
|
- DO NOT stop between Step 1 and Step 2 |
|
|
- DO NOT say "Now creating order..." - just DO it! |
|
|
|
|
|
3. **Driver Creation is a SINGLE tool call:** |
|
|
- When user wants to create a driver, call create_driver immediately |
|
|
- NO geocoding needed for drivers |
|
|
- Just call create_driver β confirm |
|
|
|
|
|
4. **If user provides required info, START IMMEDIATELY:** |
|
|
- For Orders: Customer name, address, contact (phone OR email) |
|
|
- For Drivers: Driver name (phone/email optional) |
|
|
- If all present β execute β confirm |
|
|
- If missing β ask ONCE for all missing fields |
|
|
|
|
|
**Example of CORRECT behavior:** |
|
|
|
|
|
ORDER: |
|
|
User: "Create order for John Doe, 123 Main St SF, phone 555-1234" |
|
|
You: [geocode_address] β [create_order] β "β
Order ORD-123 created!" |
|
|
(ALL in one response, no intermediate text) |
|
|
|
|
|
DRIVER: |
|
|
User: "Add new driver Mike Johnson, phone 555-0101, drives a van" |
|
|
You: [create_driver] β "β
Driver DRV-123 (Mike Johnson) added to fleet!" |
|
|
(Single tool call, immediate response) |
|
|
|
|
|
**Example of WRONG behavior (DO NOT DO THIS):** |
|
|
User: "Create order for John Doe..." |
|
|
You: [geocode_address] β "OK geocoded, now creating..." β WRONG! |
|
|
|
|
|
**Available Tools:** |
|
|
|
|
|
**Order Creation:** |
|
|
- geocode_address: Convert address to GPS coordinates |
|
|
- create_order: Create customer delivery order (REQUIRES geocoded address) |
|
|
|
|
|
**Order Querying (INTERACTIVE):** |
|
|
- count_orders: Count orders with optional filters |
|
|
- fetch_orders: Fetch N orders with pagination and filters |
|
|
- get_order_details: Get complete info about specific order by ID |
|
|
- search_orders: Search by customer name/email/phone/order ID |
|
|
- get_incomplete_orders: Get all pending/assigned/in_transit orders |
|
|
|
|
|
**Driver Creation:** |
|
|
- create_driver: Add new driver/delivery man to fleet |
|
|
|
|
|
**Driver Querying (INTERACTIVE):** |
|
|
- count_drivers: Count drivers with optional filters |
|
|
- fetch_drivers: Fetch N drivers with pagination and filters |
|
|
- get_driver_details: Get complete info about specific driver by ID |
|
|
- search_drivers: Search by name/email/phone/plate/driver ID |
|
|
- get_available_drivers: Get all active/offline drivers ready for dispatch |
|
|
|
|
|
**Order Fields:** |
|
|
Required: customer_name, delivery_address, contact |
|
|
Optional: time_window_end, priority (standard/express/urgent), special_instructions, weight_kg |
|
|
|
|
|
**Driver Fields:** |
|
|
Required: name |
|
|
Optional: phone, email, vehicle_type (van/truck/car/motorcycle), vehicle_plate, capacity_kg, capacity_m3, skills (list), status (active/busy/offline) |
|
|
|
|
|
**Order Query Workflow (INTERACTIVE - this is DIFFERENT from creation):** |
|
|
|
|
|
When user asks to "fetch orders", "show orders", or "get orders": |
|
|
1. First call count_orders (with any filters user mentioned) |
|
|
2. Tell user: "I found X orders. How many would you like to see?" |
|
|
3. Wait for user response |
|
|
4. Call fetch_orders with the limit they specify |
|
|
5. Display the results clearly |
|
|
|
|
|
When user asks "which orders are incomplete/not complete/pending": |
|
|
- Call get_incomplete_orders directly |
|
|
- Show results with priority and deadline |
|
|
|
|
|
When user asks about a specific order ID: |
|
|
- Call get_order_details with the order_id |
|
|
- Display all 26 fields clearly organized |
|
|
|
|
|
**Available Filters (use when user specifies):** |
|
|
- Status: pending, assigned, in_transit, delivered, failed, cancelled |
|
|
- Priority: standard, express, urgent |
|
|
- Payment: pending, paid, cod |
|
|
- Booleans: is_fragile, requires_signature, requires_cold_storage |
|
|
- Driver: assigned_driver_id |
|
|
|
|
|
**Example Interactions:** |
|
|
|
|
|
User: "Fetch the orders" |
|
|
You: [count_orders] β "I found 45 orders in the database. How many would you like to see? (I can also filter by status, priority, etc.)" |
|
|
|
|
|
User: "Show me 10 urgent ones" |
|
|
You: [fetch_orders with limit=10, priority=urgent] β [Display 10 urgent orders] |
|
|
|
|
|
User: "Which orders are incomplete?" |
|
|
You: [get_incomplete_orders] β [Display all pending/assigned/in_transit orders] |
|
|
|
|
|
User: "Tell me about order ORD-20251114120000" |
|
|
You: [get_order_details with order_id=ORD-20251114120000] β [Display complete order details] |
|
|
|
|
|
**Driver Query Workflow (INTERACTIVE - same as orders):** |
|
|
|
|
|
When user asks to "show drivers", "fetch drivers", or "get drivers": |
|
|
1. First call count_drivers (with any filters user mentioned) |
|
|
2. Tell user: "I found X drivers. How many would you like to see?" |
|
|
3. Wait for user response |
|
|
4. Call fetch_drivers with the limit they specify |
|
|
5. Display the results clearly |
|
|
|
|
|
When user asks "which drivers are available/free": |
|
|
- Call get_available_drivers directly |
|
|
- Show results with status and vehicle info |
|
|
|
|
|
When user asks about a specific driver ID: |
|
|
- Call get_driver_details with the driver_id |
|
|
- Display all 15 fields clearly organized |
|
|
|
|
|
**Available Driver Filters:** |
|
|
- Status: active, busy, offline, unavailable (4 values) |
|
|
- Vehicle Type: van, truck, car, motorcycle, etc. |
|
|
- Sorting: name, status, created_at, last_location_update |
|
|
|
|
|
**Example Driver Interactions:** |
|
|
|
|
|
User: "Show me drivers" |
|
|
You: [count_drivers] β "I found 15 drivers. How many would you like to see?" |
|
|
|
|
|
User: "Show 5 active ones with vans" |
|
|
You: [fetch_drivers with limit=5, status=active, vehicle_type=van] β [Display 5 drivers] |
|
|
|
|
|
User: "Which drivers are available?" |
|
|
You: [get_available_drivers] β [Display all active/offline drivers] |
|
|
|
|
|
User: "Tell me about driver DRV-20251114163800" |
|
|
You: [get_driver_details with driver_id=DRV-20251114163800] β [Display complete driver details] |
|
|
|
|
|
**Your goal:** |
|
|
- Order CREATION: Execute in ONE smooth automated flow (no stopping!) |
|
|
- Order QUERYING: Be interactive, ask user for preferences, provide helpful summaries |
|
|
- Driver CREATION: Single tool call, immediate response |
|
|
- Driver QUERYING: Be interactive, ask how many, provide helpful summaries""" |
|
|
|
|
|
def _get_gemini_tools(self) -> list: |
|
|
"""Convert tool schemas to Gemini function calling format""" |
|
|
|
|
|
return [ |
|
|
genai.protos.Tool( |
|
|
function_declarations=[ |
|
|
genai.protos.FunctionDeclaration( |
|
|
name="geocode_address", |
|
|
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.", |
|
|
parameters=genai.protos.Schema( |
|
|
type=genai.protos.Type.OBJECT, |
|
|
properties={ |
|
|
"address": genai.protos.Schema( |
|
|
type=genai.protos.Type.STRING, |
|
|
description="The full delivery address to geocode (e.g., '123 Main St, San Francisco, CA')" |
|
|
) |
|
|
}, |
|
|
required=["address"] |
|
|
) |
|
|
), |
|
|
genai.protos.FunctionDeclaration( |
|
|
name="create_order", |
|
|
description="Create a new delivery order in the database. Only call this after geocoding the address successfully.", |
|
|
parameters=genai.protos.Schema( |
|
|
type=genai.protos.Type.OBJECT, |
|
|
properties={ |
|
|
"customer_name": genai.protos.Schema( |
|
|
type=genai.protos.Type.STRING, |
|
|
description="Full name of the customer" |
|
|
), |
|
|
"customer_phone": genai.protos.Schema( |
|
|
type=genai.protos.Type.STRING, |
|
|
description="Customer phone number (optional)" |
|
|
), |
|
|
"customer_email": genai.protos.Schema( |
|
|
type=genai.protos.Type.STRING, |
|
|
description="Customer email address (optional)" |
|
|
), |
|
|
"delivery_address": genai.protos.Schema( |
|
|
type=genai.protos.Type.STRING, |
|
|
description="Full delivery address" |
|
|
), |
|
|
"delivery_lat": genai.protos.Schema( |
|
|
type=genai.protos.Type.NUMBER, |
|
|
description="Latitude from geocoding" |
|
|
), |
|
|
"delivery_lng": genai.protos.Schema( |
|
|
type=genai.protos.Type.NUMBER, |
|
|
description="Longitude from geocoding" |
|
|
), |
|
|
"time_window_end": genai.protos.Schema( |
|
|
type=genai.protos.Type.STRING, |
|
|
description="Delivery deadline in ISO format (e.g., '2025-11-13T17:00:00'). If not specified by user, default to 6 hours from now." |
|
|
), |
|
|
"priority": genai.protos.Schema( |
|
|
type=genai.protos.Type.STRING, |
|
|
description="Delivery priority. Default to 'standard' unless user specifies urgent/express." |
|
|
), |
|
|
"special_instructions": genai.protos.Schema( |
|
|
type=genai.protos.Type.STRING, |
|
|
description="Any special delivery instructions (optional)" |
|
|
), |
|
|
"weight_kg": genai.protos.Schema( |
|
|
type=genai.protos.Type.NUMBER, |
|
|
description="Package weight in kilograms (optional, default to 5.0)" |
|
|
) |
|
|
}, |
|
|
required=["customer_name", "delivery_address", "delivery_lat", "delivery_lng"] |
|
|
) |
|
|
), |
|
|
genai.protos.FunctionDeclaration( |
|
|
name="create_driver", |
|
|
description="Create a new delivery driver/delivery man in the database. Use this to onboard new drivers to the fleet.", |
|
|
parameters=genai.protos.Schema( |
|
|
type=genai.protos.Type.OBJECT, |
|
|
properties={ |
|
|
"name": genai.protos.Schema( |
|
|
type=genai.protos.Type.STRING, |
|
|
description="Full name of the driver" |
|
|
), |
|
|
"phone": genai.protos.Schema( |
|
|
type=genai.protos.Type.STRING, |
|
|
description="Driver phone number" |
|
|
), |
|
|
"email": genai.protos.Schema( |
|
|
type=genai.protos.Type.STRING, |
|
|
description="Driver email address (optional)" |
|
|
), |
|
|
"vehicle_type": genai.protos.Schema( |
|
|
type=genai.protos.Type.STRING, |
|
|
description="Type of vehicle: van, truck, car, motorcycle (default: van)" |
|
|
), |
|
|
"vehicle_plate": genai.protos.Schema( |
|
|
type=genai.protos.Type.STRING, |
|
|
description="Vehicle license plate number" |
|
|
), |
|
|
"capacity_kg": genai.protos.Schema( |
|
|
type=genai.protos.Type.NUMBER, |
|
|
description="Vehicle cargo capacity in kilograms (default: 1000.0)" |
|
|
), |
|
|
"capacity_m3": genai.protos.Schema( |
|
|
type=genai.protos.Type.NUMBER, |
|
|
description="Vehicle cargo volume in cubic meters (default: 12.0)" |
|
|
), |
|
|
"skills": genai.protos.Schema( |
|
|
type=genai.protos.Type.ARRAY, |
|
|
description="List of driver skills/certifications: refrigerated, medical_certified, fragile_handler, overnight, express_delivery", |
|
|
items=genai.protos.Schema(type=genai.protos.Type.STRING) |
|
|
), |
|
|
"status": genai.protos.Schema( |
|
|
type=genai.protos.Type.STRING, |
|
|
description="Driver status: active, busy, offline, unavailable (default: active)" |
|
|
) |
|
|
}, |
|
|
required=["name"] |
|
|
) |
|
|
), |
|
|
genai.protos.FunctionDeclaration( |
|
|
name="count_orders", |
|
|
description="Count total orders in the database with optional filters. Use this when user asks 'how many orders', 'fetch orders', or wants to know order statistics.", |
|
|
parameters=genai.protos.Schema( |
|
|
type=genai.protos.Type.OBJECT, |
|
|
properties={ |
|
|
"status": genai.protos.Schema( |
|
|
type=genai.protos.Type.STRING, |
|
|
description="Filter by order status: pending, assigned, in_transit, delivered, failed, cancelled (optional)" |
|
|
), |
|
|
"priority": genai.protos.Schema( |
|
|
type=genai.protos.Type.STRING, |
|
|
description="Filter by priority level: standard, express, urgent (optional)" |
|
|
), |
|
|
"payment_status": genai.protos.Schema( |
|
|
type=genai.protos.Type.STRING, |
|
|
description="Filter by payment status: pending, paid, cod (optional)" |
|
|
), |
|
|
"assigned_driver_id": genai.protos.Schema( |
|
|
type=genai.protos.Type.STRING, |
|
|
description="Filter by assigned driver ID (optional)" |
|
|
), |
|
|
"is_fragile": genai.protos.Schema( |
|
|
type=genai.protos.Type.BOOLEAN, |
|
|
description="Filter fragile packages only (optional)" |
|
|
), |
|
|
"requires_signature": genai.protos.Schema( |
|
|
type=genai.protos.Type.BOOLEAN, |
|
|
description="Filter orders requiring signature (optional)" |
|
|
), |
|
|
"requires_cold_storage": genai.protos.Schema( |
|
|
type=genai.protos.Type.BOOLEAN, |
|
|
description="Filter orders requiring cold storage (optional)" |
|
|
) |
|
|
}, |
|
|
required=[] |
|
|
) |
|
|
), |
|
|
genai.protos.FunctionDeclaration( |
|
|
name="fetch_orders", |
|
|
description="Fetch orders from the database with optional filters, pagination, and sorting. Use after counting to show specific number of orders.", |
|
|
parameters=genai.protos.Schema( |
|
|
type=genai.protos.Type.OBJECT, |
|
|
properties={ |
|
|
"limit": genai.protos.Schema( |
|
|
type=genai.protos.Type.INTEGER, |
|
|
description="Number of orders to fetch (default: 10, max: 100)" |
|
|
), |
|
|
"offset": genai.protos.Schema( |
|
|
type=genai.protos.Type.INTEGER, |
|
|
description="Number of orders to skip for pagination (default: 0)" |
|
|
), |
|
|
"status": genai.protos.Schema( |
|
|
type=genai.protos.Type.STRING, |
|
|
description="Filter by order status: pending, assigned, in_transit, delivered, failed, cancelled (optional)" |
|
|
), |
|
|
"priority": genai.protos.Schema( |
|
|
type=genai.protos.Type.STRING, |
|
|
description="Filter by priority level: standard, express, urgent (optional)" |
|
|
), |
|
|
"payment_status": genai.protos.Schema( |
|
|
type=genai.protos.Type.STRING, |
|
|
description="Filter by payment status: pending, paid, cod (optional)" |
|
|
), |
|
|
"assigned_driver_id": genai.protos.Schema( |
|
|
type=genai.protos.Type.STRING, |
|
|
description="Filter by assigned driver ID (optional)" |
|
|
), |
|
|
"is_fragile": genai.protos.Schema( |
|
|
type=genai.protos.Type.BOOLEAN, |
|
|
description="Filter fragile packages only (optional)" |
|
|
), |
|
|
"requires_signature": genai.protos.Schema( |
|
|
type=genai.protos.Type.BOOLEAN, |
|
|
description="Filter orders requiring signature (optional)" |
|
|
), |
|
|
"requires_cold_storage": genai.protos.Schema( |
|
|
type=genai.protos.Type.BOOLEAN, |
|
|
description="Filter orders requiring cold storage (optional)" |
|
|
), |
|
|
"sort_by": genai.protos.Schema( |
|
|
type=genai.protos.Type.STRING, |
|
|
description="Field to sort by: created_at, priority, time_window_start (default: created_at)" |
|
|
), |
|
|
"sort_order": genai.protos.Schema( |
|
|
type=genai.protos.Type.STRING, |
|
|
description="Sort order: ASC, DESC (default: DESC for newest first)" |
|
|
) |
|
|
}, |
|
|
required=[] |
|
|
) |
|
|
), |
|
|
genai.protos.FunctionDeclaration( |
|
|
name="get_order_details", |
|
|
description="Get complete details of a specific order by order ID. Use when user asks 'tell me about order X' or wants detailed information about a specific order.", |
|
|
parameters=genai.protos.Schema( |
|
|
type=genai.protos.Type.OBJECT, |
|
|
properties={ |
|
|
"order_id": genai.protos.Schema( |
|
|
type=genai.protos.Type.STRING, |
|
|
description="The order ID to fetch details for (e.g., 'ORD-20251114163800')" |
|
|
) |
|
|
}, |
|
|
required=["order_id"] |
|
|
) |
|
|
), |
|
|
genai.protos.FunctionDeclaration( |
|
|
name="search_orders", |
|
|
description="Search for orders by customer name, email, phone, or order ID pattern. Use when user provides partial information to find orders.", |
|
|
parameters=genai.protos.Schema( |
|
|
type=genai.protos.Type.OBJECT, |
|
|
properties={ |
|
|
"search_term": genai.protos.Schema( |
|
|
type=genai.protos.Type.STRING, |
|
|
description="Search term to match against customer_name, customer_email, customer_phone, or order_id" |
|
|
) |
|
|
}, |
|
|
required=["search_term"] |
|
|
) |
|
|
), |
|
|
genai.protos.FunctionDeclaration( |
|
|
name="get_incomplete_orders", |
|
|
description="Get all orders that are not yet completed (excludes delivered and cancelled orders). Shortcut for finding orders in progress (pending, assigned, in_transit).", |
|
|
parameters=genai.protos.Schema( |
|
|
type=genai.protos.Type.OBJECT, |
|
|
properties={ |
|
|
"limit": genai.protos.Schema( |
|
|
type=genai.protos.Type.INTEGER, |
|
|
description="Number of orders to fetch (default: 20)" |
|
|
) |
|
|
}, |
|
|
required=[] |
|
|
) |
|
|
), |
|
|
genai.protos.FunctionDeclaration( |
|
|
name="count_drivers", |
|
|
description="Count total drivers in the database with optional filters. Use this when user asks 'how many drivers', 'show drivers', or wants driver statistics.", |
|
|
parameters=genai.protos.Schema( |
|
|
type=genai.protos.Type.OBJECT, |
|
|
properties={ |
|
|
"status": genai.protos.Schema( |
|
|
type=genai.protos.Type.STRING, |
|
|
description="Filter by driver status: active, busy, offline, unavailable (optional)" |
|
|
), |
|
|
"vehicle_type": genai.protos.Schema( |
|
|
type=genai.protos.Type.STRING, |
|
|
description="Filter by vehicle type: van, truck, car, motorcycle, etc. (optional)" |
|
|
) |
|
|
}, |
|
|
required=[] |
|
|
) |
|
|
), |
|
|
genai.protos.FunctionDeclaration( |
|
|
name="fetch_drivers", |
|
|
description="Fetch drivers from the database with optional filters, pagination, and sorting. Use after counting to show specific number of drivers.", |
|
|
parameters=genai.protos.Schema( |
|
|
type=genai.protos.Type.OBJECT, |
|
|
properties={ |
|
|
"limit": genai.protos.Schema( |
|
|
type=genai.protos.Type.INTEGER, |
|
|
description="Number of drivers to fetch (default: 10, max: 100)" |
|
|
), |
|
|
"offset": genai.protos.Schema( |
|
|
type=genai.protos.Type.INTEGER, |
|
|
description="Number of drivers to skip for pagination (default: 0)" |
|
|
), |
|
|
"status": genai.protos.Schema( |
|
|
type=genai.protos.Type.STRING, |
|
|
description="Filter by driver status: active, busy, offline, unavailable (optional)" |
|
|
), |
|
|
"vehicle_type": genai.protos.Schema( |
|
|
type=genai.protos.Type.STRING, |
|
|
description="Filter by vehicle type: van, truck, car, motorcycle, etc. (optional)" |
|
|
), |
|
|
"sort_by": genai.protos.Schema( |
|
|
type=genai.protos.Type.STRING, |
|
|
description="Field to sort by: name, status, created_at, last_location_update (default: name)" |
|
|
), |
|
|
"sort_order": genai.protos.Schema( |
|
|
type=genai.protos.Type.STRING, |
|
|
description="Sort order: ASC, DESC (default: ASC for alphabetical)" |
|
|
) |
|
|
}, |
|
|
required=[] |
|
|
) |
|
|
), |
|
|
genai.protos.FunctionDeclaration( |
|
|
name="get_driver_details", |
|
|
description="Get complete details of a specific driver by driver ID. Use when user asks 'tell me about driver X' or wants detailed information about a specific driver.", |
|
|
parameters=genai.protos.Schema( |
|
|
type=genai.protos.Type.OBJECT, |
|
|
properties={ |
|
|
"driver_id": genai.protos.Schema( |
|
|
type=genai.protos.Type.STRING, |
|
|
description="The driver ID to fetch details for (e.g., 'DRV-20251114163800')" |
|
|
) |
|
|
}, |
|
|
required=["driver_id"] |
|
|
) |
|
|
), |
|
|
genai.protos.FunctionDeclaration( |
|
|
name="search_drivers", |
|
|
description="Search for drivers by name, email, phone, vehicle plate, or driver ID pattern. Use when user provides partial information to find drivers.", |
|
|
parameters=genai.protos.Schema( |
|
|
type=genai.protos.Type.OBJECT, |
|
|
properties={ |
|
|
"search_term": genai.protos.Schema( |
|
|
type=genai.protos.Type.STRING, |
|
|
description="Search term to match against name, email, phone, vehicle_plate, or driver_id" |
|
|
) |
|
|
}, |
|
|
required=["search_term"] |
|
|
) |
|
|
), |
|
|
genai.protos.FunctionDeclaration( |
|
|
name="get_available_drivers", |
|
|
description="Get all drivers that are available for assignment (active or offline status, excludes busy and unavailable). Shortcut for finding drivers ready for dispatch.", |
|
|
parameters=genai.protos.Schema( |
|
|
type=genai.protos.Type.OBJECT, |
|
|
properties={ |
|
|
"limit": genai.protos.Schema( |
|
|
type=genai.protos.Type.INTEGER, |
|
|
description="Number of drivers to fetch (default: 20)" |
|
|
) |
|
|
}, |
|
|
required=[] |
|
|
) |
|
|
) |
|
|
] |
|
|
) |
|
|
] |
|
|
|
|
|
def _ensure_initialized(self): |
|
|
"""Lazy initialization - only create model when first needed""" |
|
|
if self._initialized or not self.api_available: |
|
|
return |
|
|
|
|
|
try: |
|
|
genai.configure(api_key=self.api_key) |
|
|
self.model = genai.GenerativeModel( |
|
|
model_name=self.model_name, |
|
|
tools=self._get_gemini_tools(), |
|
|
system_instruction=self._get_system_prompt() |
|
|
) |
|
|
self._initialized = True |
|
|
logger.info(f"GeminiProvider: Model initialized ({self.model_name})") |
|
|
except Exception as e: |
|
|
logger.error(f"GeminiProvider: Failed to initialize: {e}") |
|
|
self.api_available = False |
|
|
self.model = None |
|
|
|
|
|
def is_available(self) -> bool: |
|
|
return self.api_available |
|
|
|
|
|
def get_status(self) -> str: |
|
|
if self.api_available: |
|
|
return f"β
Connected - Model: {self.model_name}" |
|
|
return "β οΈ Not configured (add GOOGLE_API_KEY)" |
|
|
|
|
|
def get_provider_name(self) -> str: |
|
|
return "Gemini (Google)" |
|
|
|
|
|
def get_model_name(self) -> str: |
|
|
return self.model_name if self.api_available else "gemini-2.0-flash" |
|
|
|
|
|
def process_message( |
|
|
self, |
|
|
user_message: str, |
|
|
conversation |
|
|
) -> Tuple[str, List[Dict]]: |
|
|
"""Process user message with Gemini""" |
|
|
if not self.api_available: |
|
|
return self._handle_no_api(), [] |
|
|
|
|
|
|
|
|
self._ensure_initialized() |
|
|
|
|
|
if not self._initialized: |
|
|
return "β οΈ Failed to initialize Gemini model. Please check your API key and try again.", [] |
|
|
|
|
|
try: |
|
|
|
|
|
chat = self.model.start_chat(history=self._convert_history(conversation)) |
|
|
|
|
|
|
|
|
response = chat.send_message( |
|
|
user_message, |
|
|
safety_settings={ |
|
|
HarmCategory.HARM_CATEGORY_HATE_SPEECH: HarmBlockThreshold.BLOCK_NONE, |
|
|
HarmCategory.HARM_CATEGORY_HARASSMENT: HarmBlockThreshold.BLOCK_NONE, |
|
|
HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT: HarmBlockThreshold.BLOCK_NONE, |
|
|
HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT: HarmBlockThreshold.BLOCK_NONE, |
|
|
} |
|
|
) |
|
|
|
|
|
|
|
|
conversation.add_message("user", user_message) |
|
|
|
|
|
|
|
|
return self._process_response(response, conversation, chat) |
|
|
|
|
|
except Exception as e: |
|
|
error_msg = f"β οΈ Gemini API error: {str(e)}" |
|
|
logger.error(f"Gemini provider error: {e}") |
|
|
return error_msg, [] |
|
|
|
|
|
def _convert_history(self, conversation) -> list: |
|
|
"""Convert conversation history to Gemini format""" |
|
|
history = [] |
|
|
|
|
|
for msg in conversation.get_history(): |
|
|
role = "user" if msg["role"] == "user" else "model" |
|
|
history.append({ |
|
|
"role": role, |
|
|
"parts": [{"text": str(msg["content"])}] |
|
|
}) |
|
|
return history |
|
|
|
|
|
def _process_response( |
|
|
self, |
|
|
response, |
|
|
conversation, |
|
|
chat |
|
|
) -> Tuple[str, List[Dict]]: |
|
|
"""Process Gemini's response and handle function calls""" |
|
|
tool_calls_made = [] |
|
|
|
|
|
|
|
|
try: |
|
|
|
|
|
has_function_call = False |
|
|
parts = response.candidates[0].content.parts |
|
|
logger.info(f"Processing response with {len(parts)} part(s)") |
|
|
|
|
|
for part in parts: |
|
|
if hasattr(part, 'function_call'): |
|
|
fc = part.function_call |
|
|
|
|
|
if fc is not None: |
|
|
try: |
|
|
if hasattr(fc, 'name') and fc.name: |
|
|
has_function_call = True |
|
|
logger.info(f"Detected function call: {fc.name}") |
|
|
break |
|
|
except Exception as e: |
|
|
logger.warning(f"Error checking function call: {e}") |
|
|
|
|
|
if has_function_call: |
|
|
|
|
|
current_response = response |
|
|
max_iterations = 3 |
|
|
|
|
|
for iteration in range(max_iterations): |
|
|
|
|
|
try: |
|
|
parts = current_response.candidates[0].content.parts |
|
|
logger.info(f"Iteration {iteration + 1}: Response has {len(parts)} part(s)") |
|
|
except (IndexError, AttributeError) as e: |
|
|
logger.error(f"Cannot access response parts: {e}") |
|
|
break |
|
|
|
|
|
|
|
|
has_fc = False |
|
|
fc_part = None |
|
|
|
|
|
for idx, part in enumerate(parts): |
|
|
if hasattr(part, 'function_call'): |
|
|
fc = part.function_call |
|
|
if fc and hasattr(fc, 'name') and fc.name: |
|
|
has_fc = True |
|
|
fc_part = part |
|
|
logger.info(f"Iteration {iteration + 1}: Found function_call in part {idx}: {fc.name}") |
|
|
break |
|
|
|
|
|
|
|
|
if hasattr(part, 'text') and part.text: |
|
|
logger.warning(f"Iteration {iteration + 1}: Part {idx} has text: {part.text[:100]}") |
|
|
|
|
|
if not has_fc: |
|
|
|
|
|
logger.info(f"No more function calls after iteration {iteration + 1}") |
|
|
break |
|
|
|
|
|
|
|
|
first_part = fc_part |
|
|
|
|
|
|
|
|
function_call = first_part.function_call |
|
|
function_name = function_call.name |
|
|
function_args = dict(function_call.args) if function_call.args else {} |
|
|
|
|
|
logger.info(f"Gemini executing function: {function_name} (iteration {iteration + 1})") |
|
|
|
|
|
|
|
|
tool_result = execute_tool(function_name, function_args) |
|
|
|
|
|
|
|
|
tool_calls_made.append({ |
|
|
"tool": function_name, |
|
|
"input": function_args, |
|
|
"result": tool_result |
|
|
}) |
|
|
|
|
|
conversation.add_tool_result(function_name, function_args, tool_result) |
|
|
|
|
|
|
|
|
try: |
|
|
current_response = chat.send_message( |
|
|
genai.protos.Content( |
|
|
parts=[genai.protos.Part( |
|
|
function_response=genai.protos.FunctionResponse( |
|
|
name=function_name, |
|
|
response={"result": tool_result} |
|
|
) |
|
|
)] |
|
|
) |
|
|
) |
|
|
except Exception as e: |
|
|
logger.error(f"Error sending function response: {e}") |
|
|
break |
|
|
|
|
|
|
|
|
|
|
|
final_text = "" |
|
|
try: |
|
|
parts = current_response.candidates[0].content.parts |
|
|
logger.info(f"Extracting text from {len(parts)} parts") |
|
|
|
|
|
for idx, part in enumerate(parts): |
|
|
|
|
|
if hasattr(part, 'function_call') and part.function_call: |
|
|
fc = part.function_call |
|
|
if hasattr(fc, 'name') and fc.name: |
|
|
logger.warning(f"Part {idx} still has function call: {fc.name}. Skipping.") |
|
|
continue |
|
|
|
|
|
|
|
|
if hasattr(part, 'text') and part.text: |
|
|
logger.info(f"Part {idx} has text: {part.text[:50]}...") |
|
|
final_text += part.text |
|
|
|
|
|
except (AttributeError, IndexError) as e: |
|
|
logger.error(f"Error extracting text from parts: {e}") |
|
|
|
|
|
|
|
|
if not final_text: |
|
|
logger.warning("No text extracted from response, generating fallback") |
|
|
if tool_calls_made: |
|
|
|
|
|
tool_names = [t["tool"] for t in tool_calls_made] |
|
|
if "create_order" in tool_names: |
|
|
|
|
|
create_result = next((t["result"] for t in tool_calls_made if t["tool"] == "create_order"), {}) |
|
|
if create_result.get("success"): |
|
|
order_id = create_result.get("order_id", "") |
|
|
final_text = f"β
Order {order_id} created successfully!" |
|
|
else: |
|
|
final_text = "β οΈ There was an issue creating the order." |
|
|
else: |
|
|
final_text = f"β
Executed {len(tool_calls_made)} tool(s) successfully!" |
|
|
else: |
|
|
final_text = "β
Task completed!" |
|
|
|
|
|
logger.info(f"Returning response: {final_text[:100]}") |
|
|
conversation.add_message("assistant", final_text) |
|
|
return final_text, tool_calls_made |
|
|
|
|
|
else: |
|
|
|
|
|
text_response = "" |
|
|
try: |
|
|
parts = response.candidates[0].content.parts |
|
|
logger.info(f"Extracting text from {len(parts)} parts (no function call)") |
|
|
|
|
|
for idx, part in enumerate(parts): |
|
|
|
|
|
if hasattr(part, 'function_call') and part.function_call: |
|
|
fc = part.function_call |
|
|
if hasattr(fc, 'name') and fc.name: |
|
|
logger.error(f"Part {idx} has function call {fc.name} but was not detected earlier!") |
|
|
|
|
|
logger.info("Re-processing response with function call handling") |
|
|
return self._process_response(response, conversation, chat) |
|
|
|
|
|
|
|
|
if hasattr(part, 'text') and part.text: |
|
|
logger.info(f"Part {idx} has text: {part.text[:50]}...") |
|
|
text_response += part.text |
|
|
|
|
|
except (ValueError, AttributeError, IndexError) as e: |
|
|
logger.error(f"Error extracting text from response: {e}") |
|
|
|
|
|
|
|
|
if not text_response: |
|
|
logger.warning("No text in response, using fallback") |
|
|
text_response = "I'm ready to help! What would you like me to do?" |
|
|
|
|
|
conversation.add_message("assistant", text_response) |
|
|
return text_response, tool_calls_made |
|
|
|
|
|
except Exception as e: |
|
|
logger.error(f"Error processing Gemini response: {e}") |
|
|
error_msg = f"β οΈ Error processing response: {str(e)}" |
|
|
conversation.add_message("assistant", error_msg) |
|
|
return error_msg, tool_calls_made |
|
|
|
|
|
def _handle_no_api(self) -> str: |
|
|
"""Return error message when API is not available""" |
|
|
return """β οΈ **Gemini API requires Google API key** |
|
|
|
|
|
To use Gemini: |
|
|
|
|
|
1. Get an API key from: https://aistudio.google.com/app/apikey |
|
|
- Free tier: 15 requests/min, 1500/day |
|
|
- Or use hackathon credits |
|
|
|
|
|
2. Add to your `.env` file: |
|
|
``` |
|
|
GOOGLE_API_KEY=your-gemini-key-here |
|
|
``` |
|
|
|
|
|
3. Restart the application |
|
|
|
|
|
**Alternative:** Switch to Claude by setting `AI_PROVIDER=anthropic` in .env |
|
|
""" |
|
|
|
|
|
def get_welcome_message(self) -> str: |
|
|
if not self.api_available: |
|
|
return self._handle_no_api() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return """π Hello! I'm your AI dispatch assistant powered by **Google Gemini 2.0 Flash**. |
|
|
|
|
|
I can help you manage your delivery fleet! |
|
|
|
|
|
--- |
|
|
|
|
|
π **What I Can Do:** |
|
|
|
|
|
**1. Create Delivery Orders:** |
|
|
β’ Customer Name |
|
|
β’ Delivery Address |
|
|
β’ Contact (Phone OR Email) |
|
|
β’ Optional: Deadline, Priority, Special Instructions |
|
|
|
|
|
**2. Query & Search Orders:** |
|
|
β’ Fetch orders with filters (status, priority, payment, etc.) |
|
|
β’ Get complete details of specific orders |
|
|
β’ Search by customer name, phone, email, or order ID |
|
|
β’ Find incomplete/pending orders |
|
|
|
|
|
**3. Create New Drivers:** |
|
|
β’ Driver Name (required) |
|
|
β’ Optional: Phone, Email, Vehicle Type, License Plate, Skills |
|
|
|
|
|
**4. Query & Search Drivers:** |
|
|
β’ Fetch drivers with filters (status, vehicle type) |
|
|
β’ Get complete details of specific drivers |
|
|
β’ Search by name, phone, email, plate, or driver ID |
|
|
β’ Find available/active drivers |
|
|
|
|
|
--- |
|
|
|
|
|
**Examples - Just Type Naturally:** |
|
|
|
|
|
π¦ **Create Orders:** |
|
|
π¬ "Create order for John Doe, 123 Main St San Francisco CA, phone 555-1234, deliver by 5 PM" |
|
|
π¬ "New urgent delivery to Sarah at 456 Oak Ave NYC, email [email protected]" |
|
|
|
|
|
π **Query Orders:** |
|
|
π¬ "Fetch the orders" or "Show me orders" |
|
|
π¬ "Which orders are incomplete?" |
|
|
π¬ "Tell me about order ORD-20251114120000" |
|
|
π¬ "Show me 10 urgent orders" |
|
|
π¬ "Search for orders from John" |
|
|
|
|
|
π **Create Drivers:** |
|
|
π¬ "Add new driver Tom Wilson, phone 555-0101, drives a van, plate ABC-123" |
|
|
π¬ "Create driver Sarah Martinez with refrigerated truck, phone 555-0202" |
|
|
π¬ "New driver: Mike Chen, email [email protected], motorcycle delivery" |
|
|
|
|
|
π₯ **Query Drivers:** |
|
|
π¬ "Show me drivers" or "Fetch the drivers" |
|
|
π¬ "Which drivers are available?" |
|
|
π¬ "Tell me about driver DRV-20251114163800" |
|
|
π¬ "Show 5 active drivers with vans" |
|
|
π¬ "Search for Tom" |
|
|
|
|
|
--- |
|
|
|
|
|
π **I'll automatically:** |
|
|
β’ Geocode addresses for orders |
|
|
β’ Generate unique IDs |
|
|
β’ Save everything to the database |
|
|
β’ Filter and search across all order fields |
|
|
|
|
|
What would you like to do?""" |
|
|
|