Upload folder using huggingface_hub
Browse files- app/__init__.py +9 -1
- app/__pycache__/__init__.cpython-311.pyc +0 -0
- app/__pycache__/ai_features.cpython-311.pyc +0 -0
- app/__pycache__/api.cpython-311.pyc +0 -0
- app/__pycache__/config.cpython-311.pyc +0 -0
- app/__pycache__/page_features.cpython-311.pyc +0 -0
- app/__pycache__/xero_client.cpython-311.pyc +0 -0
- app/__pycache__/xero_routes.cpython-311.pyc +0 -0
- app/__pycache__/xero_utils.cpython-311.pyc +0 -0
- app/ai_features.py +617 -0
- app/api.py +7 -395
- app/page_features.py +167 -0
- app/xero_utils.py +1 -2
- flask_session/2029240f6d1128be89ddc32729463129 +0 -0
- flask_session/aa71dde20eaf768ca7e5f90a25563ea6 +0 -0
- flask_session/d0efef4e2c8720847d7a47ac360d9d46 +0 -0
- twillio_gemini_api.py +125 -29
app/__init__.py
CHANGED
|
@@ -1,11 +1,19 @@
|
|
| 1 |
import logging
|
| 2 |
from flask import Flask, jsonify
|
|
|
|
| 3 |
from . import config
|
| 4 |
from .extensions import mongo, bcrypt, jwt, cors, session, oauth
|
| 5 |
from .xero_client import api_client, xero # <-- IMPORT XERO REMOTE APP
|
| 6 |
-
|
|
|
|
|
|
|
| 7 |
def create_app():
|
|
|
|
| 8 |
app = Flask(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
app.config.from_object(config)
|
| 10 |
|
| 11 |
logging.basicConfig(level=logging.INFO)
|
|
|
|
| 1 |
import logging
|
| 2 |
from flask import Flask, jsonify
|
| 3 |
+
|
| 4 |
from . import config
|
| 5 |
from .extensions import mongo, bcrypt, jwt, cors, session, oauth
|
| 6 |
from .xero_client import api_client, xero # <-- IMPORT XERO REMOTE APP
|
| 7 |
+
from .ai_features import ai_bp
|
| 8 |
+
from .page_features import page_bp
|
| 9 |
+
# app.register_blueprint(api_bp, url_prefix='/api')
|
| 10 |
def create_app():
|
| 11 |
+
|
| 12 |
app = Flask(__name__)
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
app.register_blueprint(ai_bp, url_prefix='/api')
|
| 16 |
+
app.register_blueprint(page_bp, url_prefix='/api/pages')
|
| 17 |
app.config.from_object(config)
|
| 18 |
|
| 19 |
logging.basicConfig(level=logging.INFO)
|
app/__pycache__/__init__.cpython-311.pyc
CHANGED
|
Binary files a/app/__pycache__/__init__.cpython-311.pyc and b/app/__pycache__/__init__.cpython-311.pyc differ
|
|
|
app/__pycache__/ai_features.cpython-311.pyc
ADDED
|
Binary file (36.4 kB). View file
|
|
|
app/__pycache__/api.cpython-311.pyc
CHANGED
|
Binary files a/app/__pycache__/api.cpython-311.pyc and b/app/__pycache__/api.cpython-311.pyc differ
|
|
|
app/__pycache__/config.cpython-311.pyc
CHANGED
|
Binary files a/app/__pycache__/config.cpython-311.pyc and b/app/__pycache__/config.cpython-311.pyc differ
|
|
|
app/__pycache__/page_features.cpython-311.pyc
ADDED
|
Binary file (11.1 kB). View file
|
|
|
app/__pycache__/xero_client.cpython-311.pyc
CHANGED
|
Binary files a/app/__pycache__/xero_client.cpython-311.pyc and b/app/__pycache__/xero_client.cpython-311.pyc differ
|
|
|
app/__pycache__/xero_routes.cpython-311.pyc
CHANGED
|
Binary files a/app/__pycache__/xero_routes.cpython-311.pyc and b/app/__pycache__/xero_routes.cpython-311.pyc differ
|
|
|
app/__pycache__/xero_utils.cpython-311.pyc
CHANGED
|
Binary files a/app/__pycache__/xero_utils.cpython-311.pyc and b/app/__pycache__/xero_utils.cpython-311.pyc differ
|
|
|
app/ai_features.py
ADDED
|
@@ -0,0 +1,617 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# your_app/ai_features.py
|
| 2 |
+
|
| 3 |
+
from flask import Blueprint, request, jsonify, current_app
|
| 4 |
+
from bson.objectid import ObjectId
|
| 5 |
+
from datetime import datetime, date, timedelta
|
| 6 |
+
from flask_jwt_extended import jwt_required, get_jwt_identity
|
| 7 |
+
from .extensions import bcrypt, mongo
|
| 8 |
+
from .Company_Info import company_info
|
| 9 |
+
|
| 10 |
+
from google import genai
|
| 11 |
+
from google.genai import types
|
| 12 |
+
import os
|
| 13 |
+
import json
|
| 14 |
+
|
| 15 |
+
# +++ START: WHATSAPP FEATURE IMPORTS +++
|
| 16 |
+
import requests
|
| 17 |
+
from twilio.twiml.messaging_response import MessagingResponse
|
| 18 |
+
# +++ END: WHATSAPP FEATURE IMPORTS +++
|
| 19 |
+
|
| 20 |
+
ai_bp = Blueprint('ai', __name__)
|
| 21 |
+
|
| 22 |
+
today = date.today()
|
| 23 |
+
weekday_number = today.weekday()
|
| 24 |
+
days_of_week = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
|
| 25 |
+
weekday_name = days_of_week[weekday_number]
|
| 26 |
+
|
| 27 |
+
# --- NEW: Define functions for the AI model ---
|
| 28 |
+
|
| 29 |
+
add_items_to_cart_function = {
|
| 30 |
+
"name": "add_items_to_cart",
|
| 31 |
+
"description": "Extracts order details from a user's message to add products to their shopping cart. Can also set a delivery date.",
|
| 32 |
+
"parameters": {
|
| 33 |
+
"type": "object", "properties": {
|
| 34 |
+
"items": { "type": "array", "description": "A list of items to add to the cart.", "items": { "type": "object", "properties": { "product_name": { "type": "string", "description": "The name of the product. This must match one of the available product names.", }, "quantity": { "type": "number", "description": "The quantity of the product.", }, "unit": { "type": "string", "description": "The unit of measure for the quantity (e.g., 'lb', 'pieces', 'box', 'bunch'). This must match one of the available units for the product.", }, }, "required": ["product_name", "quantity", "unit"], }, },
|
| 35 |
+
"delivery_date": {
|
| 36 |
+
"type": "string",
|
| 37 |
+
"description": "The desired delivery date for the order, in YYYY-MM-DD format. The user must explicitly state a date.",
|
| 38 |
+
}
|
| 39 |
+
},
|
| 40 |
+
"required": ["items"],
|
| 41 |
+
},
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
# +++ START: NEW FUNCTION DEFINITIONS FOR CART MANAGEMENT +++
|
| 45 |
+
remove_items_from_cart_function = {
|
| 46 |
+
"name": "remove_items_from_cart",
|
| 47 |
+
"description": "Removes one or more specific items from the user's shopping cart based on their name.",
|
| 48 |
+
"parameters": {
|
| 49 |
+
"type": "object",
|
| 50 |
+
"properties": {
|
| 51 |
+
"items": {
|
| 52 |
+
"type": "array",
|
| 53 |
+
"description": "A list of items to remove from the cart. Only product_name is required.",
|
| 54 |
+
"items": {
|
| 55 |
+
"type": "object",
|
| 56 |
+
"properties": {
|
| 57 |
+
"product_name": {
|
| 58 |
+
"type": "string",
|
| 59 |
+
"description": "The name of the product to remove. This must match an available product name.",
|
| 60 |
+
},
|
| 61 |
+
},
|
| 62 |
+
"required": ["product_name"],
|
| 63 |
+
},
|
| 64 |
+
}
|
| 65 |
+
},
|
| 66 |
+
"required": ["items"],
|
| 67 |
+
},
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
clear_cart_function = {
|
| 71 |
+
"name": "clear_cart",
|
| 72 |
+
"description": "Removes all items from the user's shopping cart. Use when the user wants to start over or empty their cart.",
|
| 73 |
+
"parameters": {"type": "object", "properties": {}}
|
| 74 |
+
}
|
| 75 |
+
# +++ END: NEW FUNCTION DEFINITIONS FOR CART MANAGEMENT +++
|
| 76 |
+
|
| 77 |
+
navigate_to_page_function = {
|
| 78 |
+
"name": "navigate_to_page",
|
| 79 |
+
"description": "Provides a button in the chat to navigate the user to a specific page.",
|
| 80 |
+
"parameters": {
|
| 81 |
+
"type": "object",
|
| 82 |
+
"properties": {
|
| 83 |
+
"page": {
|
| 84 |
+
"type": "string",
|
| 85 |
+
"description": "The destination page. Must be one of the allowed pages.",
|
| 86 |
+
"enum": ["orders", "cart", "catalog", "home", "about", "care", "login", "register"]
|
| 87 |
+
}
|
| 88 |
+
},
|
| 89 |
+
"required": ["page"]
|
| 90 |
+
}
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
get_my_orders_function = {
|
| 94 |
+
"name": "get_my_orders",
|
| 95 |
+
"description": "Retrieves a summary of the user's most recent orders to display in the chat.",
|
| 96 |
+
"parameters": {"type": "object", "properties": {}}
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
get_cart_items_function = {
|
| 100 |
+
"name": "get_cart_items",
|
| 101 |
+
"description": "Retrieves the items currently in the user's shopping cart and lists them.",
|
| 102 |
+
"parameters": {"type": "object", "properties": {}}
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
get_order_details_function = {
|
| 106 |
+
"name": "get_order_details",
|
| 107 |
+
"description": "Retrieves the specific items and details for a single order based on its ID.",
|
| 108 |
+
"parameters": {
|
| 109 |
+
"type": "object",
|
| 110 |
+
"properties": {
|
| 111 |
+
"order_id": {
|
| 112 |
+
"type": "string",
|
| 113 |
+
"description": "The ID of the order to fetch. Can be the full ID or the last 6 characters."
|
| 114 |
+
}
|
| 115 |
+
},
|
| 116 |
+
"required": ["order_id"]
|
| 117 |
+
}
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
cancel_order_function = {
|
| 121 |
+
"name": "cancel_order",
|
| 122 |
+
"description": "Proposes to cancel an order based on its ID. This requires user confirmation.",
|
| 123 |
+
"parameters": {
|
| 124 |
+
"type": "object",
|
| 125 |
+
"properties": {
|
| 126 |
+
"order_id": {
|
| 127 |
+
"type": "string",
|
| 128 |
+
"description": "The ID of the order to be cancelled. Can be the full ID or the last 6 characters."
|
| 129 |
+
}
|
| 130 |
+
},
|
| 131 |
+
"required": ["order_id"]
|
| 132 |
+
}
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
# +++ START: NEW FUNCTION DEFINITION FOR PLACING AN ORDER +++
|
| 136 |
+
place_order_function = {
|
| 137 |
+
"name": "place_order",
|
| 138 |
+
"description": "Places a final order using the items in the user's cart. This action is final and will clear the cart.",
|
| 139 |
+
"parameters": {
|
| 140 |
+
"type": "object",
|
| 141 |
+
"properties": {
|
| 142 |
+
"delivery_date": {
|
| 143 |
+
"type": "string",
|
| 144 |
+
"description": "The desired delivery date in YYYY-MM-DD format. Must be confirmed with the user."
|
| 145 |
+
},
|
| 146 |
+
"additional_info": {
|
| 147 |
+
"type": "string",
|
| 148 |
+
"description": "Any special instructions or notes from the user for the delivery."
|
| 149 |
+
}
|
| 150 |
+
},
|
| 151 |
+
"required": ["delivery_date"]
|
| 152 |
+
}
|
| 153 |
+
}
|
| 154 |
+
# +++ END: NEW FUNCTION DEFINITION FOR PLACING AN ORDER +++
|
| 155 |
+
|
| 156 |
+
register_new_customer_function = {
|
| 157 |
+
"name": "register_new_customer",
|
| 158 |
+
"description": "Registers a new customer by collecting their essential details. Use this when a user expresses intent to sign up or is not recognized.",
|
| 159 |
+
"parameters": {
|
| 160 |
+
"type": "object",
|
| 161 |
+
"properties": {
|
| 162 |
+
"businessName": {
|
| 163 |
+
"type": "string",
|
| 164 |
+
"description": "The name of the customer's business or company."
|
| 165 |
+
},
|
| 166 |
+
"email": {
|
| 167 |
+
"type": "string",
|
| 168 |
+
"description": "The customer's primary email address. This will be their login username."
|
| 169 |
+
},
|
| 170 |
+
"password": {
|
| 171 |
+
"type": "string",
|
| 172 |
+
"description": "A password for the user's account. The user must provide this."
|
| 173 |
+
},
|
| 174 |
+
"phoneNumber": {
|
| 175 |
+
"type": "string",
|
| 176 |
+
"description": "The user's contact phone number."
|
| 177 |
+
},
|
| 178 |
+
"businessAddress": {
|
| 179 |
+
"type": "string",
|
| 180 |
+
"description": "The full delivery address for the business."
|
| 181 |
+
},
|
| 182 |
+
"contactPerson": {
|
| 183 |
+
"type": "string",
|
| 184 |
+
"description": "The customer's full name or primary contact person's name."
|
| 185 |
+
}
|
| 186 |
+
},
|
| 187 |
+
"required": ["businessName", "email", "password", "phoneNumber", "businessAddress"]
|
| 188 |
+
}
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
@ai_bp.route('/chat', methods=['POST'])
|
| 193 |
+
@jwt_required()
|
| 194 |
+
def handle_ai_chat():
|
| 195 |
+
# ... (Web chat endpoint remains unchanged)
|
| 196 |
+
user_email = get_jwt_identity()
|
| 197 |
+
MODES = ['case', 'bag', 'piece', 'tray', 'weight']
|
| 198 |
+
all_products = list(mongo.db.products.find({}, {'name': 1, 'unit': 1, '_id': 0}))
|
| 199 |
+
product_context_list = []
|
| 200 |
+
for p in all_products:
|
| 201 |
+
if 'name' not in p: continue
|
| 202 |
+
unit_string = p.get('unit', '').lower()
|
| 203 |
+
available_modes = [mode for mode in MODES if mode in unit_string]
|
| 204 |
+
if available_modes:
|
| 205 |
+
product_context_list.append(f"{p['name']} (available units: {', '.join(available_modes)})")
|
| 206 |
+
else:
|
| 207 |
+
product_context_list.append(p['name'])
|
| 208 |
+
client = genai.Client(api_key="AIzaSyB7yKIdfW7Umv62G47BCdJjoHTJ9TeiAko")
|
| 209 |
+
|
| 210 |
+
current_date = datetime.now().strftime('%Y-%m-%d')
|
| 211 |
+
min_delivery_date = (datetime.now() + timedelta(days=2)).strftime('%Y-%m-%d')
|
| 212 |
+
|
| 213 |
+
tools = types.Tool(function_declarations=[
|
| 214 |
+
add_items_to_cart_function,
|
| 215 |
+
navigate_to_page_function,
|
| 216 |
+
get_my_orders_function,
|
| 217 |
+
cancel_order_function,
|
| 218 |
+
register_new_customer_function
|
| 219 |
+
])
|
| 220 |
+
config = types.GenerateContentConfig(tools=[tools])
|
| 221 |
+
|
| 222 |
+
instruction_prompt = f"""
|
| 223 |
+
You are an expert assistant for a food wholesale company. Your capabilities include:
|
| 224 |
+
1. Ordering & Delivery Date: Use `add_items_to_cart` to add items to the user's cart. You can also set a delivery date if the user specifies one. The current date is {current_date,weekday_name}. The earliest possible delivery date is {min_delivery_date}. If the user asks for a date earlier than this, inform them of the constraint and ask them to provide a new valid date.
|
| 225 |
+
2. Product List: {'; '.join(product_context_list)}.
|
| 226 |
+
3. Page Navigation: Use `navigate_to_page` if the user wants to GO to a page like 'my orders', 'about us', 'customer care', 'login', etc.
|
| 227 |
+
4. Order Summary: Use `get_my_orders` to LIST a summary of recent orders in the chat.
|
| 228 |
+
5. Order Cancellation: Use `cancel_order` to start the cancellation process for a SPECIFIC order. The user must provide an order ID.
|
| 229 |
+
6. New Customer Registration: Use `register_new_customer` if a user wants to sign up.
|
| 230 |
+
7. The Capability You must use AT ALL TIMES: Luring the customer to place an order and persuading them to buy more products.
|
| 231 |
+
*Always Brag about the quality of our products and our customer service and try to persuade the customer to place an order.*
|
| 232 |
+
|
| 233 |
+
|
| 234 |
+
Company Info for general questions: {company_info}
|
| 235 |
+
*** IMPORTANT: When providing a text-based answer, YOU MUST USE MARKDOWN. Use headings, bold text, bullet points (`* item`), and other formatting to make your answers clear, structured, and easy to read. ***
|
| 236 |
+
Always refer to the chat history for context. **If information is missing, ask for it.**
|
| 237 |
+
"""
|
| 238 |
+
|
| 239 |
+
user_input_part, history_data = None, []
|
| 240 |
+
if 'multipart/form-data' in request.content_type:
|
| 241 |
+
audio_file = request.files.get('audio')
|
| 242 |
+
history_json = request.form.get('history', '[]')
|
| 243 |
+
history_data = json.loads(history_json)
|
| 244 |
+
user_input_part = types.Part.from_bytes(data=audio_file.read(), mime_type=audio_file.mimetype.split(';')[0])
|
| 245 |
+
elif 'application/json' in request.content_type:
|
| 246 |
+
json_data = request.get_json()
|
| 247 |
+
history_data = json_data.get('history', [])
|
| 248 |
+
user_input_part = json_data.get('message')
|
| 249 |
+
else: return jsonify({"msg": "Unsupported format"}), 415
|
| 250 |
+
|
| 251 |
+
history_contents = [msg.get('data') for msg in history_data if msg.get('type') == 'text']
|
| 252 |
+
final_contents = [instruction_prompt] + history_contents + [user_input_part]
|
| 253 |
+
response = client.models.generate_content(model="gemini-2.5-flash", contents=final_contents, config=config)
|
| 254 |
+
response_part = response.candidates[0].content.parts[0]
|
| 255 |
+
|
| 256 |
+
if response_part.function_call:
|
| 257 |
+
function_call = response_part.function_call
|
| 258 |
+
|
| 259 |
+
if function_call.name == "add_items_to_cart":
|
| 260 |
+
proposal_data = {
|
| 261 |
+
"items": function_call.args.get('items', []),
|
| 262 |
+
"delivery_date": function_call.args.get('delivery_date')
|
| 263 |
+
}
|
| 264 |
+
return jsonify({"type": "order_proposal", "data": proposal_data}), 200
|
| 265 |
+
|
| 266 |
+
elif function_call.name == "register_new_customer":
|
| 267 |
+
return jsonify({
|
| 268 |
+
"type": "navigation_proposal",
|
| 269 |
+
"data": {
|
| 270 |
+
"path": "/register",
|
| 271 |
+
"button_text": "Go to Sign Up Page",
|
| 272 |
+
"prompt_text": "Excellent! I can help with that. Please click the button below to go to our registration page."
|
| 273 |
+
}
|
| 274 |
+
}), 200
|
| 275 |
+
|
| 276 |
+
elif function_call.name == "navigate_to_page":
|
| 277 |
+
page = function_call.args.get('page')
|
| 278 |
+
path_map = {'orders': '/order', 'cart': '/cart', 'catalog': '/catalog', 'home': '/', 'about': '/about', 'care': '/care', 'login': '/login', 'register': '/register'}
|
| 279 |
+
text_map = {'orders': 'Go to My Orders', 'cart': 'Go to Cart', 'catalog': 'Go to Catalog', 'home': 'Go to Homepage', 'about': 'Go to About Page', 'care': 'Go to Customer Care', 'login': 'Go to Login', 'register': 'Go to Sign Up'}
|
| 280 |
+
if page in path_map:
|
| 281 |
+
return jsonify({"type": "navigation_proposal", "data": {"path": path_map[page], "button_text": text_map[page], "prompt_text": f"Sure, let's go to the {page} page."}}), 200
|
| 282 |
+
|
| 283 |
+
elif function_call.name == "get_my_orders":
|
| 284 |
+
recent_orders = list(mongo.db.orders.find({'user_email': user_email}).sort('created_at', -1).limit(5))
|
| 285 |
+
if not recent_orders: return jsonify({"type": "text", "data": "You have no recent orders."}), 200
|
| 286 |
+
details_text=""
|
| 287 |
+
for order in recent_orders:
|
| 288 |
+
product_ids = [ObjectId(item['productId']) for item in order.get('items', [])]
|
| 289 |
+
products_map = {str(p['_id']): p for p in mongo.db.products.find({'_id': {'$in': product_ids}})}
|
| 290 |
+
item_lines = [f"- {item['quantity']} {item.get('mode', 'pcs')} of {products_map.get(item['productId'], {}).get('name', 'Unknown Product')}" for item in order.get('items', [])]
|
| 291 |
+
details_text += f"\n\n **Details for Order #{str(order['_id'])[-4:]}** _(Delivery Status: {order.get('status')})_:\n" + "\n".join(item_lines)
|
| 292 |
+
return jsonify({"type": "text", "data": "**Here are your recent orders**:\n" + details_text}), 200
|
| 293 |
+
|
| 294 |
+
elif function_call.name == "cancel_order":
|
| 295 |
+
order_id_frag = function_call.args.get("order_id", "").strip()
|
| 296 |
+
all_user_orders = mongo.db.orders.find({'user_email': user_email})
|
| 297 |
+
order = None
|
| 298 |
+
for o in all_user_orders:
|
| 299 |
+
if str(o['_id']).endswith(order_id_frag):
|
| 300 |
+
order = o
|
| 301 |
+
break
|
| 302 |
+
if not order: return jsonify({"type": "text", "data": f"Sorry, I couldn't find an order with ID matching '{order_id_frag}'."}), 200
|
| 303 |
+
if order.get('status') not in ['pending', 'confirmed']: return jsonify({"type": "text", "data": f"Order #{str(order['_id'])[-4:]} cannot be cancelled as its status is '{order.get('status')}'."}), 200
|
| 304 |
+
return jsonify({
|
| 305 |
+
"type": "cancellation_proposal",
|
| 306 |
+
"data": {"order_id": str(order['_id']), "prompt_text": f"Are you sure you want to cancel order #{str(order['_id'])[-4:]}?"}
|
| 307 |
+
}), 200
|
| 308 |
+
|
| 309 |
+
return jsonify({"type": "text", "data": response.text}), 200
|
| 310 |
+
|
| 311 |
+
@ai_bp.route('/whatsapp', methods=['POST'])
|
| 312 |
+
def whatsapp_reply():
|
| 313 |
+
""" Responds to incoming WhatsApp messages (text or audio) via Twilio. """
|
| 314 |
+
final_response_text = "I'm sorry, I encountered an error. Please try again."
|
| 315 |
+
twilio_resp = MessagingResponse()
|
| 316 |
+
|
| 317 |
+
try:
|
| 318 |
+
# --- 1. Get User and Message from Twilio Request ---
|
| 319 |
+
whatsapp_number = request.values.get('From')
|
| 320 |
+
user_message_text = request.values.get('Body', '').strip()
|
| 321 |
+
num_media = int(request.values.get('NumMedia', 0))
|
| 322 |
+
|
| 323 |
+
if not whatsapp_number:
|
| 324 |
+
current_app.logger.error("Request received without a 'From' number.")
|
| 325 |
+
twilio_resp.message("Could not identify your number. Please try again.")
|
| 326 |
+
return str(twilio_resp)
|
| 327 |
+
|
| 328 |
+
# --- 2. Identify User or Prepare for Registration ---
|
| 329 |
+
user = mongo.db.users.find_one({'whatsapp_number': whatsapp_number})
|
| 330 |
+
|
| 331 |
+
# --- 3. Setup AI Client and Product Context ---
|
| 332 |
+
client = genai.Client(api_key=os.getenv("GEMINI_API_KEY"))
|
| 333 |
+
MODES = ['case', 'bag', 'piece', 'tray', 'weight']
|
| 334 |
+
all_products = list(mongo.db.products.find({}, {'name': 1, 'unit': 1, '_id': 0}))
|
| 335 |
+
product_context_list = []
|
| 336 |
+
for p in all_products:
|
| 337 |
+
if 'name' not in p: continue
|
| 338 |
+
unit_string = p.get('unit', '').lower()
|
| 339 |
+
available_modes = [mode for mode in MODES if mode in unit_string]
|
| 340 |
+
if available_modes:
|
| 341 |
+
product_context_list.append(f"{p['name']} (available units: {', '.join(available_modes)})")
|
| 342 |
+
else:
|
| 343 |
+
product_context_list.append(p['name'])
|
| 344 |
+
|
| 345 |
+
# --- 4. Process User Input (Text or Audio) ---
|
| 346 |
+
user_input_part = None
|
| 347 |
+
if num_media > 0:
|
| 348 |
+
media_url = request.values.get('MediaUrl0')
|
| 349 |
+
mime_type = request.values.get('MediaContentType0')
|
| 350 |
+
if 'audio' in mime_type:
|
| 351 |
+
audio_response = requests.get(media_url)
|
| 352 |
+
if audio_response.status_code == 200:
|
| 353 |
+
user_input_part = types.Part.from_bytes(data=audio_response.content, mime_type=mime_type.split(';')[0])
|
| 354 |
+
|
| 355 |
+
if not user_input_part and user_message_text:
|
| 356 |
+
user_input_part = user_message_text
|
| 357 |
+
|
| 358 |
+
if not user_input_part:
|
| 359 |
+
twilio_resp.message("Please send a message or a voice note.")
|
| 360 |
+
return str(twilio_resp)
|
| 361 |
+
|
| 362 |
+
# --- 5. Configure AI based on User Status and Conversation History ---
|
| 363 |
+
# +++ START: RETRIEVE AND MANAGE CONVERSATION HISTORY +++
|
| 364 |
+
history_doc = mongo.db.whatsapp_history.find_one({'whatsapp_number': whatsapp_number})
|
| 365 |
+
chat_history = history_doc.get('history', []) if history_doc else []
|
| 366 |
+
# +++ END: RETRIEVE AND MANAGE CONVERSATION HISTORY +++
|
| 367 |
+
|
| 368 |
+
if user:
|
| 369 |
+
# User is known, set up for ordering, viewing orders, and placing orders
|
| 370 |
+
tools = types.Tool(function_declarations=[
|
| 371 |
+
add_items_to_cart_function,
|
| 372 |
+
remove_items_from_cart_function, # ADDED
|
| 373 |
+
clear_cart_function, # ADDED
|
| 374 |
+
get_cart_items_function,
|
| 375 |
+
place_order_function,
|
| 376 |
+
get_my_orders_function,
|
| 377 |
+
get_order_details_function,
|
| 378 |
+
])
|
| 379 |
+
instruction_prompt = f"""
|
| 380 |
+
You are a helpful WhatsApp ordering assistant for A WholeSale Grocery Company Named Matax Express. The user is {user.get('contactPerson', 'a valued customer')}.
|
| 381 |
+
Your goal is to help them manage their order.
|
| 382 |
+
- To add items, use `add_items_to_cart`.
|
| 383 |
+
- To remove specific items from the cart, use `remove_items_from_cart`.
|
| 384 |
+
- To clear the entire cart, use `clear_cart`.
|
| 385 |
+
- To list items in the cart, use `get_cart_items`.
|
| 386 |
+
- To get a summary of recent orders, use `get_my_orders`.
|
| 387 |
+
- To get details for a specific order, use `get_order_details`.
|
| 388 |
+
- To finalize the purchase, use `place_order`. You MUST confirm the delivery date and ask for any special instructions before calling this function.
|
| 389 |
+
Available products: {', '.join(product_context_list)}.
|
| 390 |
+
Always be friendly and confirm actions clearly. Refer to the chat history for context.
|
| 391 |
+
Company Info for general questions: {company_info}.
|
| 392 |
+
**Whenever the user adds items to the cart,places an order , or registers as new customer inform them that they can also view the cart, view their orders and also order on https://matax-express.vercel.app/ logging in with the account they used to register on whatsapp.**
|
| 393 |
+
Finally when the user places the order, inform them that they can view the placed order details on https://matax-express.vercel.app/ by logging in with the account they used to register on whatsapp.
|
| 394 |
+
Always respond in whatsapp formatted markdown manner.
|
| 395 |
+
"""
|
| 396 |
+
else:
|
| 397 |
+
# User is new, set up for registration
|
| 398 |
+
tools = types.Tool(function_declarations=[register_new_customer_function])
|
| 399 |
+
instruction_prompt = f"""
|
| 400 |
+
You are a helpful WhatsApp registration assistant forA WholeSale Grocery Company named matax express. The user is new.
|
| 401 |
+
Your primary goal is to register them using the `register_new_customer` function.
|
| 402 |
+
You MUST collect their business name, email, a password, phone number, and business address.
|
| 403 |
+
Ask for the information clearly. Advise the user to choose a secure password.
|
| 404 |
+
Company Info for general questions: {company_info}.
|
| 405 |
+
**Whenever the user adds items to the cart,places an order , or registers as new customer inform them that they can also view the cart, view their orders and also order on https://matax-express.vercel.app/ logging in with the account they used to register on whatsapp.**
|
| 406 |
+
Always respond in whatsapp formatted markdown manner.
|
| 407 |
+
"""
|
| 408 |
+
|
| 409 |
+
config = types.GenerateContentConfig(tools=[tools])
|
| 410 |
+
|
| 411 |
+
# +++ START: ADD HISTORY TO THE API CALL +++
|
| 412 |
+
final_contents = [instruction_prompt] + chat_history + [user_input_part]
|
| 413 |
+
response = client.models.generate_content(
|
| 414 |
+
model="gemini-2.5-flash",
|
| 415 |
+
contents=final_contents,
|
| 416 |
+
config=config
|
| 417 |
+
)
|
| 418 |
+
# +++ END: ADD HISTORY TO THE API CALL +++
|
| 419 |
+
|
| 420 |
+
response_part = response.candidates[0].content.parts[0]
|
| 421 |
+
|
| 422 |
+
# --- 6. Process AI Response (Function Call or Text) ---
|
| 423 |
+
if response_part.function_call:
|
| 424 |
+
function_call = response_part.function_call
|
| 425 |
+
|
| 426 |
+
if function_call.name == 'register_new_customer':
|
| 427 |
+
args = function_call.args
|
| 428 |
+
email = args.get('email').lower()
|
| 429 |
+
password = args.get('password')
|
| 430 |
+
|
| 431 |
+
if mongo.db.users.find_one({'email': email}):
|
| 432 |
+
final_response_text = f"An account with the email '{email}' already exists. Please try a different email or contact support."
|
| 433 |
+
elif not password:
|
| 434 |
+
final_response_text = "A password is required to create an account. Please provide a password."
|
| 435 |
+
else:
|
| 436 |
+
hashed_password = bcrypt.generate_password_hash(password).decode('utf-8')
|
| 437 |
+
new_user_doc = {
|
| 438 |
+
"businessName": args.get('businessName'),
|
| 439 |
+
"companyName": args.get('businessName'),
|
| 440 |
+
"email": email,
|
| 441 |
+
"password": hashed_password,
|
| 442 |
+
"phoneNumber": args.get('phoneNumber'),
|
| 443 |
+
"businessAddress": args.get('businessAddress'),
|
| 444 |
+
"contactPerson": args.get('contactPerson'),
|
| 445 |
+
"whatsapp_number": whatsapp_number,
|
| 446 |
+
"is_approved": True,
|
| 447 |
+
"is_admin": False,
|
| 448 |
+
"created_at": datetime.utcnow()
|
| 449 |
+
}
|
| 450 |
+
mongo.db.users.insert_one(new_user_doc)
|
| 451 |
+
final_response_text = f"Welcome, {args.get('businessName')}! You are now registered. You can start placing orders by telling me what you need."
|
| 452 |
+
|
| 453 |
+
elif function_call.name == 'add_items_to_cart' and user:
|
| 454 |
+
items_to_add = function_call.args.get('items', [])
|
| 455 |
+
added_items_messages = []
|
| 456 |
+
db_items = []
|
| 457 |
+
|
| 458 |
+
for item in items_to_add:
|
| 459 |
+
p_name = item.get('product_name')
|
| 460 |
+
product_doc = mongo.db.products.find_one({'name': {'$regex': f'^{p_name}$', '$options': 'i'}})
|
| 461 |
+
|
| 462 |
+
if product_doc:
|
| 463 |
+
db_items.append({
|
| 464 |
+
"productId": str(product_doc['_id']),
|
| 465 |
+
"quantity": item.get('quantity'),
|
| 466 |
+
"mode": item.get('unit')
|
| 467 |
+
})
|
| 468 |
+
added_items_messages.append(f"{item.get('quantity')} {item.get('unit')} of {product_doc['name']}")
|
| 469 |
+
else:
|
| 470 |
+
added_items_messages.append(f"could not find '{p_name}'")
|
| 471 |
+
|
| 472 |
+
if db_items:
|
| 473 |
+
mongo.db.carts.update_one(
|
| 474 |
+
{'user_email': user['email']},
|
| 475 |
+
{'$push': {'items': {'$each': db_items}}, '$set': {'user_email': user['email'], 'updated_at': datetime.utcnow()}},
|
| 476 |
+
upsert=True
|
| 477 |
+
)
|
| 478 |
+
final_response_text = f"OK, I've updated your cart: I added {', '.join(added_items_messages)}. You can view your cart or place the order. What's next?"
|
| 479 |
+
|
| 480 |
+
# +++ START: NEW HANDLER FOR REMOVING ITEMS FROM CART +++
|
| 481 |
+
elif function_call.name == 'remove_items_from_cart' and user:
|
| 482 |
+
items_to_remove = function_call.args.get('items', [])
|
| 483 |
+
removed_product_names = []
|
| 484 |
+
not_found_product_names = []
|
| 485 |
+
product_ids_to_pull = []
|
| 486 |
+
|
| 487 |
+
for item in items_to_remove:
|
| 488 |
+
p_name = item.get('product_name')
|
| 489 |
+
product_doc = mongo.db.products.find_one({'name': {'$regex': f'^{p_name}$', '$options': 'i'}})
|
| 490 |
+
if product_doc:
|
| 491 |
+
product_ids_to_pull.append(str(product_doc['_id']))
|
| 492 |
+
removed_product_names.append(product_doc['name'])
|
| 493 |
+
else:
|
| 494 |
+
not_found_product_names.append(p_name)
|
| 495 |
+
|
| 496 |
+
if product_ids_to_pull:
|
| 497 |
+
mongo.db.carts.update_one(
|
| 498 |
+
{'user_email': user['email']},
|
| 499 |
+
{'$pull': {'items': {'productId': {'$in': product_ids_to_pull}}}}
|
| 500 |
+
)
|
| 501 |
+
|
| 502 |
+
response_parts = []
|
| 503 |
+
if removed_product_names:
|
| 504 |
+
response_parts.append(f"I have removed {', '.join(removed_product_names)} from your cart.")
|
| 505 |
+
if not_found_product_names:
|
| 506 |
+
response_parts.append(f"I couldn't find {', '.join(not_found_product_names)} in your cart to remove.")
|
| 507 |
+
|
| 508 |
+
if not response_parts:
|
| 509 |
+
final_response_text = "I didn't find any items to remove based on your request."
|
| 510 |
+
else:
|
| 511 |
+
final_response_text = " ".join(response_parts)
|
| 512 |
+
# +++ END: NEW HANDLER FOR REMOVING ITEMS FROM CART +++
|
| 513 |
+
|
| 514 |
+
# +++ START: NEW HANDLER FOR CLEARING THE CART +++
|
| 515 |
+
elif function_call.name == 'clear_cart' and user:
|
| 516 |
+
result = mongo.db.carts.update_one(
|
| 517 |
+
{'user_email': user['email']},
|
| 518 |
+
{'$set': {'items': []}}
|
| 519 |
+
)
|
| 520 |
+
if result.matched_count > 0 and result.modified_count > 0:
|
| 521 |
+
final_response_text = "OK, I've cleared all items from your shopping cart."
|
| 522 |
+
else:
|
| 523 |
+
final_response_text = "Your cart is already empty."
|
| 524 |
+
# +++ END: NEW HANDLER FOR CLEARING THE CART +++
|
| 525 |
+
|
| 526 |
+
elif function_call.name == 'get_cart_items' and user:
|
| 527 |
+
cart = mongo.db.carts.find_one({'user_email': user['email']})
|
| 528 |
+
if not cart or not cart.get('items'):
|
| 529 |
+
final_response_text = "Your shopping cart is currently empty."
|
| 530 |
+
else:
|
| 531 |
+
cart_items = cart.get('items', [])
|
| 532 |
+
product_ids = [ObjectId(item['productId']) for item in cart_items if 'productId' in item]
|
| 533 |
+
products_map = {str(p['_id']): p for p in mongo.db.products.find({'_id': {'$in': product_ids}})}
|
| 534 |
+
|
| 535 |
+
item_lines = []
|
| 536 |
+
for item in cart_items:
|
| 537 |
+
product_details = products_map.get(item.get('productId'), {})
|
| 538 |
+
product_name = product_details.get('name', 'Unknown Product')
|
| 539 |
+
item_lines.append(f"- {item.get('quantity')} {item.get('mode', 'pcs')} of {product_name}")
|
| 540 |
+
|
| 541 |
+
if item_lines:
|
| 542 |
+
final_response_text = "*Here are the items in your cart:*\n" + "\n".join(item_lines)
|
| 543 |
+
else:
|
| 544 |
+
final_response_text = "Your shopping cart is currently empty."
|
| 545 |
+
|
| 546 |
+
elif function_call.name == 'place_order' and user:
|
| 547 |
+
cart = mongo.db.carts.find_one({'user_email': user['email']})
|
| 548 |
+
if not cart or not cart.get('items'):
|
| 549 |
+
final_response_text = "Your cart is empty. Please add some items before placing an order."
|
| 550 |
+
else:
|
| 551 |
+
args = function_call.args
|
| 552 |
+
order_doc = {
|
| 553 |
+
'user_email': user['email'],
|
| 554 |
+
'items': cart['items'],
|
| 555 |
+
'delivery_date': args.get('delivery_date'),
|
| 556 |
+
'delivery_address': user.get('businessAddress'),
|
| 557 |
+
'mobile_number': user.get('phoneNumber'),
|
| 558 |
+
'additional_info': args.get('additional_info', ''),
|
| 559 |
+
'status': 'pending',
|
| 560 |
+
'created_at': datetime.utcnow()
|
| 561 |
+
}
|
| 562 |
+
order_id = mongo.db.orders.insert_one(order_doc).inserted_id
|
| 563 |
+
mongo.db.carts.delete_one({'user_email': user['email']}) # Clear cart
|
| 564 |
+
final_response_text = f"Thank you! Your order #{str(order_id)[-6:]} has been placed for delivery on {args.get('delivery_date')}. You will receive a confirmation shortly."
|
| 565 |
+
|
| 566 |
+
elif function_call.name == 'get_my_orders' and user:
|
| 567 |
+
recent_orders = list(mongo.db.orders.find({'user_email': user['email']}).sort('created_at', -1).limit(3))
|
| 568 |
+
if not recent_orders:
|
| 569 |
+
final_response_text = "You don't have any recent orders."
|
| 570 |
+
else:
|
| 571 |
+
details_text = "Here are your last few orders:\n"
|
| 572 |
+
for order in recent_orders:
|
| 573 |
+
details_text += f"\n- Order #{str(order['_id'])[-6:]} (Status: {order.get('status')}) on {order.get('delivery_date')}"
|
| 574 |
+
final_response_text = details_text
|
| 575 |
+
|
| 576 |
+
elif function_call.name == 'get_order_details' and user:
|
| 577 |
+
order_id_frag = function_call.args.get("order_id", "").strip()
|
| 578 |
+
all_user_orders = mongo.db.orders.find({'user_email': user['email']})
|
| 579 |
+
order_doc = None
|
| 580 |
+
for o in all_user_orders:
|
| 581 |
+
if str(o['_id']).endswith(order_id_frag):
|
| 582 |
+
order_doc = o
|
| 583 |
+
break
|
| 584 |
+
|
| 585 |
+
if not order_doc:
|
| 586 |
+
final_response_text = f"Sorry, I couldn't find an order with ID matching '{order_id_frag}'."
|
| 587 |
+
else:
|
| 588 |
+
product_ids = [ObjectId(item['productId']) for item in order_doc.get('items', [])]
|
| 589 |
+
products_map = {str(p['_id']): p for p in mongo.db.products.find({'_id': {'$in': product_ids}})}
|
| 590 |
+
item_lines = [f"- {item['quantity']} {item.get('mode', 'pcs')} of {products_map.get(item['productId'], {}).get('name', 'Unknown Product')}" for item in order_doc.get('items', [])]
|
| 591 |
+
details_text = f"*Details for Order #{str(order_doc['_id'])[-6:]}*\n*Status:* {order_doc.get('status', 'N/A')}\n*Delivery Date:* {order_doc.get('delivery_date', 'N/A')}\n\n*Items:*\n" + "\n".join(item_lines)
|
| 592 |
+
final_response_text = details_text
|
| 593 |
+
else:
|
| 594 |
+
final_response_text = response.text
|
| 595 |
+
|
| 596 |
+
# +++ START: SAVE CONVERSATION TO DATABASE +++
|
| 597 |
+
user_message_to_save = user_message_text if user_message_text else "Audio message"
|
| 598 |
+
chat_history.append({"role": "user", "parts": [{"text": user_message_to_save}]})
|
| 599 |
+
chat_history.append({"role": "model", "parts": [{"text": final_response_text}]})
|
| 600 |
+
|
| 601 |
+
# Keep the history from getting too long
|
| 602 |
+
if len(chat_history) > 10:
|
| 603 |
+
chat_history = chat_history[-10:]
|
| 604 |
+
|
| 605 |
+
mongo.db.whatsapp_history.update_one(
|
| 606 |
+
{'whatsapp_number': whatsapp_number},
|
| 607 |
+
{'$set': {'history': chat_history, 'updated_at': datetime.utcnow()}},
|
| 608 |
+
upsert=True
|
| 609 |
+
)
|
| 610 |
+
# +++ END: SAVE CONVERSATION TO DATABASE +++
|
| 611 |
+
|
| 612 |
+
except Exception as e:
|
| 613 |
+
current_app.logger.error(f"WhatsApp endpoint error: {e}")
|
| 614 |
+
final_response_text = "I'm having a little trouble right now. Please try again in a moment."
|
| 615 |
+
|
| 616 |
+
twilio_resp.message(final_response_text)
|
| 617 |
+
return str(twilio_resp)
|
app/api.py
CHANGED
|
@@ -1,234 +1,22 @@
|
|
| 1 |
# your_app/api.py
|
| 2 |
|
| 3 |
-
from flask import Blueprint, request, jsonify, current_app,redirect
|
| 4 |
from bson.objectid import ObjectId
|
| 5 |
-
from datetime import datetime
|
| 6 |
from flask_jwt_extended import create_access_token, jwt_required, get_jwt_identity
|
| 7 |
from .extensions import bcrypt, mongo
|
| 8 |
from .xero_utils import trigger_po_creation, trigger_contact_creation
|
| 9 |
from .email_utils import send_order_confirmation_email, send_registration_email, send_login_notification_email, send_cart_reminder_email
|
| 10 |
-
from google import genai
|
| 11 |
-
from google.genai import types
|
| 12 |
-
from datetime import datetime, timedelta # <-- Import timedelta
|
| 13 |
-
import os
|
| 14 |
-
import json
|
| 15 |
-
from .Company_Info import company_info
|
| 16 |
-
api_bp = Blueprint('api', __name__)
|
| 17 |
|
| 18 |
|
| 19 |
-
today = date.today()
|
| 20 |
-
weekday_number = today.weekday()
|
| 21 |
-
days_of_week = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
|
| 22 |
-
weekday_name = days_of_week[weekday_number]
|
| 23 |
-
|
| 24 |
-
# --- NEW: Define functions for the AI model ---
|
| 25 |
-
|
| 26 |
-
add_items_to_cart_function = {
|
| 27 |
-
"name": "add_items_to_cart",
|
| 28 |
-
"description": "Extracts order details from a user's message to add products to their shopping cart. Can also set a delivery date.",
|
| 29 |
-
"parameters": {
|
| 30 |
-
"type": "object", "properties": {
|
| 31 |
-
"items": { "type": "array", "description": "A list of items to add to the cart.", "items": { "type": "object", "properties": { "product_name": { "type": "string", "description": "The name of the product. This must match one of the available product names.", }, "quantity": { "type": "number", "description": "The quantity of the product.", }, "unit": { "type": "string", "description": "The unit of measure for the quantity (e.g., 'lb', 'pieces', 'box', 'bunch'). This must match one of the available units for the product.", }, }, "required": ["product_name", "quantity", "unit"], }, },
|
| 32 |
-
# --- START NEW PARAMETER ---
|
| 33 |
-
"delivery_date": {
|
| 34 |
-
"type": "string",
|
| 35 |
-
"description": "The desired delivery date for the order, in YYYY-MM-DD format. The user must explicitly state a date.",
|
| 36 |
-
}
|
| 37 |
-
# --- END NEW PARAMETER ---
|
| 38 |
-
},
|
| 39 |
-
"required": ["items"],
|
| 40 |
-
},
|
| 41 |
-
}
|
| 42 |
-
|
| 43 |
-
navigate_to_page_function = {
|
| 44 |
-
"name": "navigate_to_page",
|
| 45 |
-
"description": "Provides a button in the chat to navigate the user to a specific page.",
|
| 46 |
-
"parameters": {
|
| 47 |
-
"type": "object",
|
| 48 |
-
"properties": {
|
| 49 |
-
"page": {
|
| 50 |
-
"type": "string",
|
| 51 |
-
"description": "The destination page. Must be one of the allowed pages.",
|
| 52 |
-
"enum": ["orders", "cart", "catalog", "home", "about", "care", "login", "register"]
|
| 53 |
-
}
|
| 54 |
-
},
|
| 55 |
-
"required": ["page"]
|
| 56 |
-
}
|
| 57 |
-
}
|
| 58 |
-
|
| 59 |
-
get_my_orders_function = {
|
| 60 |
-
"name": "get_my_orders",
|
| 61 |
-
"description": "Retrieves a summary of the user's most recent orders to display in the chat.",
|
| 62 |
-
"parameters": {"type": "object", "properties": {}}
|
| 63 |
-
}
|
| 64 |
-
|
| 65 |
-
# --- NEW: Function to get details of a specific order ---
|
| 66 |
-
get_order_details_function = {
|
| 67 |
-
"name": "get_order_details",
|
| 68 |
-
"description": "Retrieves the specific items and details for a single order based on its ID.",
|
| 69 |
-
"parameters": {
|
| 70 |
-
"type": "object",
|
| 71 |
-
"properties": {
|
| 72 |
-
"order_id": {
|
| 73 |
-
"type": "string",
|
| 74 |
-
"description": "The ID of the order to fetch. Can be the full ID or the last 6 characters."
|
| 75 |
-
}
|
| 76 |
-
},
|
| 77 |
-
"required": ["order_id"]
|
| 78 |
-
}
|
| 79 |
-
}
|
| 80 |
-
|
| 81 |
-
# --- NEW: Function to initiate an order cancellation ---
|
| 82 |
-
cancel_order_function = {
|
| 83 |
-
"name": "cancel_order",
|
| 84 |
-
"description": "Proposes to cancel an order based on its ID. This requires user confirmation.",
|
| 85 |
-
"parameters": {
|
| 86 |
-
"type": "object",
|
| 87 |
-
"properties": {
|
| 88 |
-
"order_id": {
|
| 89 |
-
"type": "string",
|
| 90 |
-
"description": "The ID of the order to be cancelled. Can be the full ID or the last 6 characters."
|
| 91 |
-
}
|
| 92 |
-
},
|
| 93 |
-
"required": ["order_id"]
|
| 94 |
-
}
|
| 95 |
-
}
|
| 96 |
|
|
|
|
| 97 |
|
| 98 |
@api_bp.route('/clear')
|
| 99 |
def clear_all():
|
| 100 |
-
# mongo.db.users.delete_many({})
|
| 101 |
mongo.db.orders.delete_many({})
|
| 102 |
return "✅"
|
| 103 |
|
| 104 |
-
|
| 105 |
-
@api_bp.route('/chat', methods=['POST'])
|
| 106 |
-
@jwt_required()
|
| 107 |
-
def handle_ai_chat():
|
| 108 |
-
# try:
|
| 109 |
-
# --- 1. Universal Setup ---
|
| 110 |
-
user_email = get_jwt_identity()
|
| 111 |
-
MODES = ['case', 'bag', 'piece', 'tray', 'weight']
|
| 112 |
-
all_products = list(mongo.db.products.find({}, {'name': 1, 'unit': 1, '_id': 0}))
|
| 113 |
-
product_context_list = []
|
| 114 |
-
for p in all_products:
|
| 115 |
-
if 'name' not in p: continue
|
| 116 |
-
unit_string = p.get('unit', '').lower()
|
| 117 |
-
available_modes = [mode for mode in MODES if mode in unit_string]
|
| 118 |
-
if available_modes:
|
| 119 |
-
product_context_list.append(f"{p['name']} (available units: {', '.join(available_modes)})")
|
| 120 |
-
else:
|
| 121 |
-
product_context_list.append(p['name'])
|
| 122 |
-
client = genai.Client(api_key="AIzaSyB7yKIdfW7Umv62G47BCdJjoHTJ9TeiAko")
|
| 123 |
-
|
| 124 |
-
# --- START MODIFICATION: Add all new functions to the tools list and calculate dates ---
|
| 125 |
-
current_date = datetime.now().strftime('%Y-%m-%d')
|
| 126 |
-
min_delivery_date = (datetime.now() + timedelta(days=2)).strftime('%Y-%m-%d')
|
| 127 |
-
|
| 128 |
-
tools = types.Tool(function_declarations=[
|
| 129 |
-
add_items_to_cart_function,
|
| 130 |
-
navigate_to_page_function,
|
| 131 |
-
get_my_orders_function,
|
| 132 |
-
cancel_order_function
|
| 133 |
-
])
|
| 134 |
-
config = types.GenerateContentConfig(tools=[tools])
|
| 135 |
-
|
| 136 |
-
# --- 2. UPDATED: Prepare the main instruction prompt for the AI ---
|
| 137 |
-
instruction_prompt = f"""
|
| 138 |
-
You are an expert assistant for a food wholesale company. Your capabilities include:
|
| 139 |
-
1. Ordering & Delivery Date: Use `add_items_to_cart` to add items to the user's cart. You can also set a delivery date if the user specifies one. The current date is {current_date,weekday_name}. The earliest possible delivery date is {min_delivery_date}. If the user asks for a date earlier than this, inform them of the constraint and ask them to provide a new valid date.
|
| 140 |
-
2. Product List: {'; '.join(product_context_list)}.
|
| 141 |
-
3. Page Navigation: Use `navigate_to_page` if the user wants to GO to a page like 'my orders', 'about us', 'customer care', 'login', etc.
|
| 142 |
-
4. Order Summary: Use `get_my_orders` to LIST a summary of recent orders in the chat.
|
| 143 |
-
5. Order Cancellation: Use `cancel_order` to start the cancellation process for a SPECIFIC order. The user must provide an order ID.
|
| 144 |
-
6. The Capability You must use AT ALL TIMES: Luring the customer to place an order and persuading them to buy more products.
|
| 145 |
-
*Always Brag about the quality of our products and our customer service and try to persuade the customer to place an order.*
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
Company Info for general questions: {company_info}
|
| 149 |
-
*** IMPORTANT: When providing a text-based answer, YOU MUST USE MARKDOWN. Use headings, bold text, bullet points (`* item`), and other formatting to make your answers clear, structured, and easy to read. ***
|
| 150 |
-
Always refer to the chat history for context. **If information is missing, ask for it.**
|
| 151 |
-
"""
|
| 152 |
-
# --- END MODIFICATION ---
|
| 153 |
-
|
| 154 |
-
# --- 3. Process incoming user input (This section remains the same) ---
|
| 155 |
-
user_input_part, history_data = None, []
|
| 156 |
-
if 'multipart/form-data' in request.content_type:
|
| 157 |
-
audio_file = request.files.get('audio')
|
| 158 |
-
history_json = request.form.get('history', '[]')
|
| 159 |
-
history_data = json.loads(history_json)
|
| 160 |
-
user_input_part = types.Part.from_bytes(data=audio_file.read(), mime_type=audio_file.mimetype.split(';')[0])
|
| 161 |
-
elif 'application/json' in request.content_type:
|
| 162 |
-
json_data = request.get_json()
|
| 163 |
-
history_data = json_data.get('history', [])
|
| 164 |
-
user_input_part = json_data.get('message')
|
| 165 |
-
else: return jsonify({"msg": "Unsupported format"}), 415
|
| 166 |
-
|
| 167 |
-
# --- 4. Make API call (This section remains the same) ---
|
| 168 |
-
history_contents = [msg.get('data') for msg in history_data if msg.get('type') == 'text']
|
| 169 |
-
final_contents = [instruction_prompt] + history_contents + [user_input_part]
|
| 170 |
-
response = client.models.generate_content(model="gemini-2.5-flash", contents=final_contents, config=config)
|
| 171 |
-
response_part = response.candidates[0].content.parts[0]
|
| 172 |
-
|
| 173 |
-
# --- 5. UPDATED: Process the response with new function handlers ---
|
| 174 |
-
if response_part.function_call:
|
| 175 |
-
function_call = response_part.function_call
|
| 176 |
-
|
| 177 |
-
if function_call.name == "add_items_to_cart":
|
| 178 |
-
# --- START MODIFICATION ---
|
| 179 |
-
# Create a payload with both items and the potential delivery date
|
| 180 |
-
proposal_data = {
|
| 181 |
-
"items": function_call.args.get('items', []),
|
| 182 |
-
"delivery_date": function_call.args.get('delivery_date') # This will be None if not provided by the AI
|
| 183 |
-
}
|
| 184 |
-
return jsonify({"type": "order_proposal", "data": proposal_data}), 200
|
| 185 |
-
# --- END MODIFICATION ---
|
| 186 |
-
|
| 187 |
-
elif function_call.name == "navigate_to_page":
|
| 188 |
-
page = function_call.args.get('page')
|
| 189 |
-
path_map = {'orders': '/order', 'cart': '/cart', 'catalog': '/catalog', 'home': '/', 'about': '/about', 'care': '/care', 'login': '/login', 'register': '/register'}
|
| 190 |
-
text_map = {'orders': 'Go to My Orders', 'cart': 'Go to Cart', 'catalog': 'Go to Catalog', 'home': 'Go to Homepage', 'about': 'Go to About Page', 'care': 'Go to Customer Care', 'login': 'Go to Login', 'register': 'Go to Sign Up'}
|
| 191 |
-
if page in path_map:
|
| 192 |
-
return jsonify({"type": "navigation_proposal", "data": {"path": path_map[page], "button_text": text_map[page], "prompt_text": f"Sure, let's go to the {page} page."}}), 200
|
| 193 |
-
|
| 194 |
-
elif function_call.name == "get_my_orders":
|
| 195 |
-
recent_orders = list(mongo.db.orders.find({'user_email': user_email}).sort('created_at', -1).limit(5))
|
| 196 |
-
if not recent_orders: return jsonify({"type": "text", "data": "You have no recent orders."}), 200
|
| 197 |
-
summary = "\n".join([f"- Order Details for #{str(o['_id'])[-6:]}\n{str(o)} \n\n" for o in recent_orders])
|
| 198 |
-
details_text=""
|
| 199 |
-
for order in recent_orders:
|
| 200 |
-
product_ids = [ObjectId(item['productId']) for item in order.get('items', [])]
|
| 201 |
-
products_map = {str(p['_id']): p for p in mongo.db.products.find({'_id': {'$in': product_ids}})}
|
| 202 |
-
item_lines = [f"- {item['quantity']} {item.get('mode', 'pcs')} of {products_map.get(item['productId'], {}).get('name', 'Unknown Product')}" for item in order.get('items', [])]
|
| 203 |
-
details_text += f"\n\n **Details for Order #{str(order['_id'])[-4:]}** _(Delivery Status: {order.get('status')})_:\n" + "\n".join(item_lines)
|
| 204 |
-
return jsonify({"type": "text", "data": "**Here are your recent orders**:\n" + details_text}), 200
|
| 205 |
-
|
| 206 |
-
elif function_call.name == "cancel_order":
|
| 207 |
-
order_id_frag = function_call.args.get("order_id", "").strip()
|
| 208 |
-
|
| 209 |
-
all_user_orders = mongo.db.orders.find({'user_email': user_email})
|
| 210 |
-
order = None
|
| 211 |
-
for o in all_user_orders:
|
| 212 |
-
if str(o['_id']).endswith(order_id_frag):
|
| 213 |
-
order = o
|
| 214 |
-
break
|
| 215 |
-
if not order: return jsonify({"type": "text", "data": f"Sorry, I couldn't find an order with ID matching '{order_id_frag}'."}), 200
|
| 216 |
-
if order.get('status') not in ['pending', 'confirmed']: return jsonify({"type": "text", "data": f"Order #{str(order['_id'])[-4:]} cannot be cancelled as its status is '{order.get('status')}'."}), 200
|
| 217 |
-
|
| 218 |
-
return jsonify({
|
| 219 |
-
"type": "cancellation_proposal",
|
| 220 |
-
"data": {"order_id": str(order['_id']), "prompt_text": f"Are you sure you want to cancel order #{str(order['_id'])[-4:]}?"}
|
| 221 |
-
}), 200
|
| 222 |
-
|
| 223 |
-
# Fallback to text response
|
| 224 |
-
return jsonify({"type": "text", "data": response.text}), 200
|
| 225 |
-
|
| 226 |
-
# except Exception as e:
|
| 227 |
-
# current_app.logger.error(f"Gemini API Error: {e}")
|
| 228 |
-
# return jsonify({"msg": "Sorry, I'm having trouble. Please try again."}), 500
|
| 229 |
-
|
| 230 |
-
# --- The rest of your api.py file remains unchanged ---
|
| 231 |
-
|
| 232 |
@api_bp.route('/register', methods=['POST'])
|
| 233 |
def register():
|
| 234 |
data = request.get_json()
|
|
@@ -246,7 +34,7 @@ def register():
|
|
| 246 |
user_document = data.copy()
|
| 247 |
user_document['password'] = hashed_password
|
| 248 |
user_document['company_name'] = company_name
|
| 249 |
-
user_document['is_approved'] =
|
| 250 |
user_document['is_admin'] = False
|
| 251 |
|
| 252 |
mongo.db.users.insert_one(user_document)
|
|
@@ -259,13 +47,14 @@ def register():
|
|
| 259 |
|
| 260 |
return jsonify({"msg": "Registration successful! Your application is being processed."}), 201
|
| 261 |
|
|
|
|
| 262 |
@api_bp.route('/login', methods=['POST'])
|
| 263 |
def login():
|
| 264 |
data = request.get_json()
|
| 265 |
email, password = data.get('email'), data.get('password')
|
| 266 |
user = mongo.db.users.find_one({'email': email})
|
| 267 |
|
| 268 |
-
if user and bcrypt.check_password_hash(user['password'], password):
|
| 269 |
if not user.get('is_approved', False): return jsonify({"msg": "Account pending approval"}), 403
|
| 270 |
|
| 271 |
try:
|
|
@@ -274,7 +63,7 @@ def login():
|
|
| 274 |
current_app.logger.error(f"Failed to send login notification email to {email}: {e}")
|
| 275 |
|
| 276 |
access_token = create_access_token(identity=email)
|
| 277 |
-
return jsonify(access_token=access_token, email=user['email'], companyName=user['
|
| 278 |
|
| 279 |
return jsonify({"msg": "Bad email or password"}), 401
|
| 280 |
|
|
@@ -333,7 +122,6 @@ def handle_cart():
|
|
| 333 |
})
|
| 334 |
|
| 335 |
if request.method == 'POST':
|
| 336 |
-
# --- START MODIFICATION: Handle partial updates to prevent data loss ---
|
| 337 |
data = request.get_json()
|
| 338 |
|
| 339 |
update_doc = {
|
|
@@ -341,7 +129,6 @@ def handle_cart():
|
|
| 341 |
'updated_at': datetime.utcnow()
|
| 342 |
}
|
| 343 |
|
| 344 |
-
# Only update fields that are present in the request
|
| 345 |
if 'items' in data:
|
| 346 |
update_doc['items'] = data['items']
|
| 347 |
|
|
@@ -353,7 +140,6 @@ def handle_cart():
|
|
| 353 |
{'$set': update_doc},
|
| 354 |
upsert=True
|
| 355 |
)
|
| 356 |
-
# --- END MODIFICATION ---
|
| 357 |
return jsonify({"msg": "Cart updated successfully"})
|
| 358 |
|
| 359 |
@api_bp.route('/orders', methods=['GET', 'POST'])
|
|
@@ -496,21 +282,14 @@ def cancel_order(order_id):
|
|
| 496 |
|
| 497 |
return jsonify({"msg": "Order has been cancelled."}), 200
|
| 498 |
|
| 499 |
-
# +++ START: NEW /sendmail ENDPOINT +++
|
| 500 |
@api_bp.route('/sendmail', methods=['GET'])
|
| 501 |
def send_cart_reminders():
|
| 502 |
-
"""
|
| 503 |
-
Triggers reminder emails to all users with items in their cart.
|
| 504 |
-
This can be triggered manually or by a scheduled job (e.g., cron).
|
| 505 |
-
"""
|
| 506 |
try:
|
| 507 |
-
# 1. Find all carts that have items
|
| 508 |
carts_with_items = list(mongo.db.carts.find({'items': {'$exists': True, '$ne': []}}))
|
| 509 |
|
| 510 |
if not carts_with_items:
|
| 511 |
return jsonify({"msg": "No users with pending items in cart."}), 200
|
| 512 |
|
| 513 |
-
# 2. Collect all user emails and product IDs for efficient bulk fetching
|
| 514 |
user_emails = [cart['user_email'] for cart in carts_with_items]
|
| 515 |
all_product_ids = {
|
| 516 |
ObjectId(item['productId'])
|
|
@@ -518,24 +297,20 @@ def send_cart_reminders():
|
|
| 518 |
for item in cart.get('items', [])
|
| 519 |
}
|
| 520 |
|
| 521 |
-
# 3. Fetch all required users and products from the DB
|
| 522 |
users_cursor = mongo.db.users.find({'email': {'$in': user_emails}})
|
| 523 |
products_cursor = mongo.db.products.find({'_id': {'$in': list(all_product_ids)}})
|
| 524 |
|
| 525 |
-
# 4. Create lookup maps for fast access
|
| 526 |
users_map = {user['email']: user for user in users_cursor}
|
| 527 |
products_map = {str(prod['_id']): prod for prod in products_cursor}
|
| 528 |
|
| 529 |
emails_sent_count = 0
|
| 530 |
|
| 531 |
-
# 5. Iterate through carts, prepare data, and send emails
|
| 532 |
for cart in carts_with_items:
|
| 533 |
user = users_map.get(cart['user_email'])
|
| 534 |
if not user:
|
| 535 |
current_app.logger.warning(f"Cart found for non-existent user: {cart['user_email']}")
|
| 536 |
continue
|
| 537 |
|
| 538 |
-
# Populate cart items with full product details
|
| 539 |
populated_items = []
|
| 540 |
for item in cart.get('items', []):
|
| 541 |
product_details = products_map.get(item['productId'])
|
|
@@ -560,7 +335,6 @@ def send_cart_reminders():
|
|
| 560 |
except Exception as e:
|
| 561 |
current_app.logger.error(f"Error in /sendmail endpoint: {e}")
|
| 562 |
return jsonify({"msg": "An internal error occurred while sending reminders."}), 500
|
| 563 |
-
# +++ END: NEW /sendmail ENDPOINT +++
|
| 564 |
|
| 565 |
@api_bp.route('/admin/users/approve/<user_id>', methods=['POST'])
|
| 566 |
@jwt_required()
|
|
@@ -568,168 +342,6 @@ def approve_user(user_id):
|
|
| 568 |
mongo.db.users.update_one({'_id': ObjectId(user_id)}, {'$set': {'is_approved': True}})
|
| 569 |
return jsonify({"msg": f"User {user_id} approved"})
|
| 570 |
|
| 571 |
-
|
| 572 |
-
# Default data for initialization if pages don't exist in the database
|
| 573 |
-
DEFAULT_ABOUT_DATA = {
|
| 574 |
-
"_id": "about",
|
| 575 |
-
"title": "Our Commitment to You",
|
| 576 |
-
"slogan": "It's more than produce — it's about being your daily, trusted partner.",
|
| 577 |
-
"paragraphs": [
|
| 578 |
-
"Welcome to Matax Express Ltd, your trusted wholesale produce provider serving the Greater Toronto Area for over 30 years!",
|
| 579 |
-
"At Matax Express Ltd, our commitment to service has always been at the core of what we do. From day one, we’ve focused on understanding the needs of our customers and working tirelessly to meet them. Whether delivering to bustling restaurants, local markets, or retail stores, we strive to ensure your success and satisfaction every step of the way.",
|
| 580 |
-
"We’re proud of the journey we’ve taken over the past three decades, and we also understand the importance of continuous improvement. Your feedback has been crucial in helping us grow and adapt, and we are working hard to ensure every interaction reflects the high standard of service you deserve.",
|
| 581 |
-
"While freshness remains an important priority, we are equally dedicated to creating a service experience that exceeds expectations. From reliable deliveries to personalized support, our team is here to make partnering with Matax Express Ltd seamless and efficient.",
|
| 582 |
-
"For us, it’s not just about providing produce—it’s about being a dependable partner that you can count on daily. We look forward to building stronger relationships and delivering better service for years to come."
|
| 583 |
-
]
|
| 584 |
-
}
|
| 585 |
-
|
| 586 |
-
DEFAULT_CONTACT_DATA = {
|
| 587 |
-
"_id": "contact",
|
| 588 |
-
"title": "Contact Us",
|
| 589 |
-
"intro": "We're here to help! Reach out to us through any of the channels below. We aim to respond to all inquiries within 24 business hours.",
|
| 590 |
-
"details": [
|
| 591 |
-
{"type": "Phone Support", "value": "+1 (800) 123-4567"},
|
| 592 |
-
{"type": "Email Support", "value": "support@mataxexpress.com"},
|
| 593 |
-
{"type": "Business Hours", "value": "Monday - Friday, 9:00 AM - 5:00 PM (EST)"},
|
| 594 |
-
{"type": "Mailing Address", "value": "123 Fresh Produce Lane, Farmville, ST 54321"}
|
| 595 |
-
]
|
| 596 |
-
}
|
| 597 |
-
|
| 598 |
-
@api_bp.route('/pages/<page_name>', methods=['GET'])
|
| 599 |
-
def get_page_content(page_name):
|
| 600 |
-
"""API endpoint for the frontend to fetch page content."""
|
| 601 |
-
content = mongo.db.pages.find_one({'_id': page_name})
|
| 602 |
-
if not content:
|
| 603 |
-
# If content doesn't exist, create it from default and return it
|
| 604 |
-
if page_name == 'about':
|
| 605 |
-
mongo.db.pages.insert_one(DEFAULT_ABOUT_DATA)
|
| 606 |
-
content = DEFAULT_ABOUT_DATA
|
| 607 |
-
elif page_name == 'contact':
|
| 608 |
-
mongo.db.pages.insert_one(DEFAULT_CONTACT_DATA)
|
| 609 |
-
content = DEFAULT_CONTACT_DATA
|
| 610 |
-
else:
|
| 611 |
-
return jsonify({"msg": "Page not found"}), 404
|
| 612 |
-
# Ensure _id is a string if it's an ObjectId
|
| 613 |
-
if '_id' in content and not isinstance(content['_id'], str):
|
| 614 |
-
content['_id'] = str(content['_id'])
|
| 615 |
-
return jsonify(content)
|
| 616 |
-
|
| 617 |
-
@api_bp.route('/pages/update', methods=['POST'])
|
| 618 |
-
def update_page_content():
|
| 619 |
-
"""Handles form submission from the /update UI to save changes."""
|
| 620 |
-
page_name = request.form.get('page_name')
|
| 621 |
-
if page_name == 'about':
|
| 622 |
-
paragraphs = request.form.get('paragraphs', '').strip().split('\n')
|
| 623 |
-
paragraphs = [p.strip() for p in paragraphs if p.strip()]
|
| 624 |
-
|
| 625 |
-
update_data = {
|
| 626 |
-
"title": request.form.get('title'),
|
| 627 |
-
"slogan": request.form.get('slogan'),
|
| 628 |
-
"paragraphs": paragraphs
|
| 629 |
-
}
|
| 630 |
-
elif page_name == 'contact':
|
| 631 |
-
update_data = {
|
| 632 |
-
"title": request.form.get('title'),
|
| 633 |
-
"intro": request.form.get('intro'),
|
| 634 |
-
"details": [
|
| 635 |
-
{"type": "Phone Support", "value": request.form.get('phone_value')},
|
| 636 |
-
{"type": "Email Support", "value": request.form.get('email_value')},
|
| 637 |
-
{"type": "Business Hours", "value": request.form.get('hours_value')},
|
| 638 |
-
{"type": "Mailing Address", "value": request.form.get('address_value')}
|
| 639 |
-
]
|
| 640 |
-
}
|
| 641 |
-
else:
|
| 642 |
-
return redirect('/api/update')
|
| 643 |
-
|
| 644 |
-
mongo.db.pages.update_one(
|
| 645 |
-
{'_id': page_name},
|
| 646 |
-
{'$set': update_data},
|
| 647 |
-
upsert=True
|
| 648 |
-
)
|
| 649 |
-
return redirect('/api/update')
|
| 650 |
-
|
| 651 |
-
@api_bp.route('/update', methods=['GET'])
|
| 652 |
-
def update_ui():
|
| 653 |
-
"""Serves the simple HTML UI for editing page content."""
|
| 654 |
-
about_data = mongo.db.pages.find_one({'_id': 'about'}) or DEFAULT_ABOUT_DATA
|
| 655 |
-
contact_data = mongo.db.pages.find_one({'_id': 'contact'}) or DEFAULT_CONTACT_DATA
|
| 656 |
-
|
| 657 |
-
about_paragraphs_text = "\n".join(about_data.get('paragraphs', []))
|
| 658 |
-
contact_details = {item['type']: item['value'] for item in contact_data.get('details', [])}
|
| 659 |
-
|
| 660 |
-
html = f"""
|
| 661 |
-
<!DOCTYPE html>
|
| 662 |
-
<html lang="en">
|
| 663 |
-
<head>
|
| 664 |
-
<meta charset="UTF-8">
|
| 665 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 666 |
-
<title>Update Page Content</title>
|
| 667 |
-
<style>
|
| 668 |
-
body {{ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; margin: 2em; background-color: #f8f9fa; color: #212529; }}
|
| 669 |
-
.container {{ max-width: 800px; margin: auto; }}
|
| 670 |
-
h1, h2 {{ color: #343a40; border-bottom: 2px solid #dee2e6; padding-bottom: 0.5em; }}
|
| 671 |
-
form {{ background: white; padding: 2em; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.05); margin-bottom: 2em; }}
|
| 672 |
-
label {{ display: block; margin-top: 1em; margin-bottom: 0.5em; font-weight: bold; color: #495057; }}
|
| 673 |
-
input[type="text"], textarea {{ width: 100%; padding: 0.8em; border: 1px solid #ced4da; border-radius: 4px; box-sizing: border-box; font-size: 1rem; }}
|
| 674 |
-
textarea {{ height: 250px; resize: vertical; }}
|
| 675 |
-
button {{ background-color: #007bff; color: white; padding: 0.8em 1.5em; border: none; border-radius: 4px; cursor: pointer; font-size: 1em; font-weight: bold; }}
|
| 676 |
-
button:hover {{ background-color: #0056b3; }}
|
| 677 |
-
</style>
|
| 678 |
-
</head>
|
| 679 |
-
<body>
|
| 680 |
-
<div class="container">
|
| 681 |
-
<h1>Update Website Content</h1>
|
| 682 |
-
|
| 683 |
-
<!-- About Us Page Form -->
|
| 684 |
-
<h2>About Us Page</h2>
|
| 685 |
-
<form action="/api/pages/update" method="post">
|
| 686 |
-
<input type="hidden" name="page_name" value="about">
|
| 687 |
-
<label for="about_title">Title:</label>
|
| 688 |
-
<input type="text" id="about_title" name="title" value="{about_data.get('title', '')}">
|
| 689 |
-
|
| 690 |
-
<label for="about_slogan">Slogan:</label>
|
| 691 |
-
<input type="text" id="about_slogan" name="slogan" value="{about_data.get('slogan', '')}">
|
| 692 |
-
|
| 693 |
-
<label for="about_paragraphs">Paragraphs (one paragraph per line):</label>
|
| 694 |
-
<textarea id="about_paragraphs" name="paragraphs">{about_paragraphs_text}</textarea>
|
| 695 |
-
|
| 696 |
-
<br><br>
|
| 697 |
-
<button type="submit">Update About Page</button>
|
| 698 |
-
</form>
|
| 699 |
-
|
| 700 |
-
<!-- Contact Page Form -->
|
| 701 |
-
<h2>Customer Care Page</h2>
|
| 702 |
-
<form action="/api/pages/update" method="post">
|
| 703 |
-
<input type="hidden" name="page_name" value="contact">
|
| 704 |
-
|
| 705 |
-
<label for="contact_title">Title:</label>
|
| 706 |
-
<input type="text" id="contact_title" name="title" value="{contact_data.get('title', '')}">
|
| 707 |
-
|
| 708 |
-
<label for="contact_intro">Intro Text:</label>
|
| 709 |
-
<input type="text" id="contact_intro" name="intro" value="{contact_data.get('intro', '')}">
|
| 710 |
-
|
| 711 |
-
<label for="phone_value">Phone Support:</label>
|
| 712 |
-
<input type="text" id="phone_value" name="phone_value" value="{contact_details.get('Phone Support', '')}">
|
| 713 |
-
|
| 714 |
-
<label for="email_value">Email Support:</label>
|
| 715 |
-
<input type="text" id="email_value" name="email_value" value="{contact_details.get('Email Support', '')}">
|
| 716 |
-
|
| 717 |
-
<label for="hours_value">Business Hours:</label>
|
| 718 |
-
<input type="text" id="hours_value" name="hours_value" value="{contact_details.get('Business Hours', '')}">
|
| 719 |
-
|
| 720 |
-
<label for="address_value">Mailing Address:</label>
|
| 721 |
-
<input type="text" id="address_value" name="address_value" value="{contact_details.get('Mailing Address', '')}">
|
| 722 |
-
|
| 723 |
-
<br><br>
|
| 724 |
-
<button type="submit">Update Contact Page</button>
|
| 725 |
-
</form>
|
| 726 |
-
</div>
|
| 727 |
-
</body>
|
| 728 |
-
</html>
|
| 729 |
-
"""
|
| 730 |
-
return html
|
| 731 |
-
|
| 732 |
-
|
| 733 |
# +++ START: NEW ENDPOINT FOR ITEM REQUESTS +++
|
| 734 |
@api_bp.route('/request-item', methods=['POST'])
|
| 735 |
@jwt_required()
|
|
|
|
| 1 |
# your_app/api.py
|
| 2 |
|
| 3 |
+
from flask import Blueprint, request, jsonify, current_app, redirect
|
| 4 |
from bson.objectid import ObjectId
|
| 5 |
+
from datetime import datetime
|
| 6 |
from flask_jwt_extended import create_access_token, jwt_required, get_jwt_identity
|
| 7 |
from .extensions import bcrypt, mongo
|
| 8 |
from .xero_utils import trigger_po_creation, trigger_contact_creation
|
| 9 |
from .email_utils import send_order_confirmation_email, send_registration_email, send_login_notification_email, send_cart_reminder_email
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
+
api_bp = Blueprint('api', __name__)
|
| 14 |
|
| 15 |
@api_bp.route('/clear')
|
| 16 |
def clear_all():
|
|
|
|
| 17 |
mongo.db.orders.delete_many({})
|
| 18 |
return "✅"
|
| 19 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
@api_bp.route('/register', methods=['POST'])
|
| 21 |
def register():
|
| 22 |
data = request.get_json()
|
|
|
|
| 34 |
user_document = data.copy()
|
| 35 |
user_document['password'] = hashed_password
|
| 36 |
user_document['company_name'] = company_name
|
| 37 |
+
user_document['is_approved'] = True
|
| 38 |
user_document['is_admin'] = False
|
| 39 |
|
| 40 |
mongo.db.users.insert_one(user_document)
|
|
|
|
| 47 |
|
| 48 |
return jsonify({"msg": "Registration successful! Your application is being processed."}), 201
|
| 49 |
|
| 50 |
+
# ... (the rest of your api.py file remains unchanged)
|
| 51 |
@api_bp.route('/login', methods=['POST'])
|
| 52 |
def login():
|
| 53 |
data = request.get_json()
|
| 54 |
email, password = data.get('email'), data.get('password')
|
| 55 |
user = mongo.db.users.find_one({'email': email})
|
| 56 |
|
| 57 |
+
if user and user.get('password') and bcrypt.check_password_hash(user['password'], password):
|
| 58 |
if not user.get('is_approved', False): return jsonify({"msg": "Account pending approval"}), 403
|
| 59 |
|
| 60 |
try:
|
|
|
|
| 63 |
current_app.logger.error(f"Failed to send login notification email to {email}: {e}")
|
| 64 |
|
| 65 |
access_token = create_access_token(identity=email)
|
| 66 |
+
return jsonify(access_token=access_token, email=user['email'], companyName=user['businessName'],contactPerson=user.get('contactPerson', '')) , 200
|
| 67 |
|
| 68 |
return jsonify({"msg": "Bad email or password"}), 401
|
| 69 |
|
|
|
|
| 122 |
})
|
| 123 |
|
| 124 |
if request.method == 'POST':
|
|
|
|
| 125 |
data = request.get_json()
|
| 126 |
|
| 127 |
update_doc = {
|
|
|
|
| 129 |
'updated_at': datetime.utcnow()
|
| 130 |
}
|
| 131 |
|
|
|
|
| 132 |
if 'items' in data:
|
| 133 |
update_doc['items'] = data['items']
|
| 134 |
|
|
|
|
| 140 |
{'$set': update_doc},
|
| 141 |
upsert=True
|
| 142 |
)
|
|
|
|
| 143 |
return jsonify({"msg": "Cart updated successfully"})
|
| 144 |
|
| 145 |
@api_bp.route('/orders', methods=['GET', 'POST'])
|
|
|
|
| 282 |
|
| 283 |
return jsonify({"msg": "Order has been cancelled."}), 200
|
| 284 |
|
|
|
|
| 285 |
@api_bp.route('/sendmail', methods=['GET'])
|
| 286 |
def send_cart_reminders():
|
|
|
|
|
|
|
|
|
|
|
|
|
| 287 |
try:
|
|
|
|
| 288 |
carts_with_items = list(mongo.db.carts.find({'items': {'$exists': True, '$ne': []}}))
|
| 289 |
|
| 290 |
if not carts_with_items:
|
| 291 |
return jsonify({"msg": "No users with pending items in cart."}), 200
|
| 292 |
|
|
|
|
| 293 |
user_emails = [cart['user_email'] for cart in carts_with_items]
|
| 294 |
all_product_ids = {
|
| 295 |
ObjectId(item['productId'])
|
|
|
|
| 297 |
for item in cart.get('items', [])
|
| 298 |
}
|
| 299 |
|
|
|
|
| 300 |
users_cursor = mongo.db.users.find({'email': {'$in': user_emails}})
|
| 301 |
products_cursor = mongo.db.products.find({'_id': {'$in': list(all_product_ids)}})
|
| 302 |
|
|
|
|
| 303 |
users_map = {user['email']: user for user in users_cursor}
|
| 304 |
products_map = {str(prod['_id']): prod for prod in products_cursor}
|
| 305 |
|
| 306 |
emails_sent_count = 0
|
| 307 |
|
|
|
|
| 308 |
for cart in carts_with_items:
|
| 309 |
user = users_map.get(cart['user_email'])
|
| 310 |
if not user:
|
| 311 |
current_app.logger.warning(f"Cart found for non-existent user: {cart['user_email']}")
|
| 312 |
continue
|
| 313 |
|
|
|
|
| 314 |
populated_items = []
|
| 315 |
for item in cart.get('items', []):
|
| 316 |
product_details = products_map.get(item['productId'])
|
|
|
|
| 335 |
except Exception as e:
|
| 336 |
current_app.logger.error(f"Error in /sendmail endpoint: {e}")
|
| 337 |
return jsonify({"msg": "An internal error occurred while sending reminders."}), 500
|
|
|
|
| 338 |
|
| 339 |
@api_bp.route('/admin/users/approve/<user_id>', methods=['POST'])
|
| 340 |
@jwt_required()
|
|
|
|
| 342 |
mongo.db.users.update_one({'_id': ObjectId(user_id)}, {'$set': {'is_approved': True}})
|
| 343 |
return jsonify({"msg": f"User {user_id} approved"})
|
| 344 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 345 |
# +++ START: NEW ENDPOINT FOR ITEM REQUESTS +++
|
| 346 |
@api_bp.route('/request-item', methods=['POST'])
|
| 347 |
@jwt_required()
|
app/page_features.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# your_app/page_features.py
|
| 2 |
+
|
| 3 |
+
from flask import Blueprint, request, jsonify, redirect
|
| 4 |
+
from .extensions import mongo
|
| 5 |
+
|
| 6 |
+
page_bp = Blueprint('pages', __name__)
|
| 7 |
+
|
| 8 |
+
# Default data for initialization if pages don't exist in the database
|
| 9 |
+
DEFAULT_ABOUT_DATA = {
|
| 10 |
+
"_id": "about",
|
| 11 |
+
"title": "Our Commitment to You",
|
| 12 |
+
"slogan": "It's more than produce — it's about being your daily, trusted partner.",
|
| 13 |
+
"paragraphs": [
|
| 14 |
+
"Welcome to Matax Express Ltd, your trusted wholesale produce provider serving the Greater Toronto Area for over 30 years!",
|
| 15 |
+
"At Matax Express Ltd, our commitment to service has always been at the core of what we do. From day one, we’ve focused on understanding the needs of our customers and working tirelessly to meet them. Whether delivering to bustling restaurants, local markets, or retail stores, we strive to ensure your success and satisfaction every step of the way.",
|
| 16 |
+
"We’re proud of the journey we’ve taken over the past three decades, and we also understand the importance of continuous improvement. Your feedback has been crucial in helping us grow and adapt, and we are working hard to ensure every interaction reflects the high standard of service you deserve.",
|
| 17 |
+
"While freshness remains an important priority, we are equally dedicated to creating a service experience that exceeds expectations. From reliable deliveries to personalized support, our team is here to make partnering with Matax Express Ltd seamless and efficient.",
|
| 18 |
+
"For us, it’s not just about providing produce—it’s about being a dependable partner that you can count on daily. We look forward to building stronger relationships and delivering better service for years to come."
|
| 19 |
+
]
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
DEFAULT_CONTACT_DATA = {
|
| 23 |
+
"_id": "contact",
|
| 24 |
+
"title": "Contact Us",
|
| 25 |
+
"intro": "We're here to help! Reach out to us through any of the channels below. We aim to respond to all inquiries within 24 business hours.",
|
| 26 |
+
"details": [
|
| 27 |
+
{"type": "Phone Support", "value": "+1 (800) 123-4567"},
|
| 28 |
+
{"type": "Email Support", "value": "support@mataxexpress.com"},
|
| 29 |
+
{"type": "Business Hours", "value": "Monday - Friday, 9:00 AM - 5:00 PM (EST)"},
|
| 30 |
+
{"type": "Mailing Address", "value": "123 Fresh Produce Lane, Farmville, ST 54321"}
|
| 31 |
+
]
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
@page_bp.route('/<page_name>', methods=['GET'])
|
| 35 |
+
def get_page_content(page_name):
|
| 36 |
+
"""API endpoint for the frontend to fetch page content."""
|
| 37 |
+
content = mongo.db.pages.find_one({'_id': page_name})
|
| 38 |
+
if not content:
|
| 39 |
+
# If content doesn't exist, create it from default and return it
|
| 40 |
+
if page_name == 'about':
|
| 41 |
+
mongo.db.pages.insert_one(DEFAULT_ABOUT_DATA)
|
| 42 |
+
content = DEFAULT_ABOUT_DATA
|
| 43 |
+
elif page_name == 'contact':
|
| 44 |
+
mongo.db.pages.insert_one(DEFAULT_CONTACT_DATA)
|
| 45 |
+
content = DEFAULT_CONTACT_DATA
|
| 46 |
+
else:
|
| 47 |
+
return jsonify({"msg": "Page not found"}), 404
|
| 48 |
+
# Ensure _id is a string if it's an ObjectId
|
| 49 |
+
if '_id' in content and not isinstance(content['_id'], str):
|
| 50 |
+
content['_id'] = str(content['_id'])
|
| 51 |
+
return jsonify(content)
|
| 52 |
+
|
| 53 |
+
@page_bp.route('/update', methods=['POST'])
|
| 54 |
+
def update_page_content():
|
| 55 |
+
"""Handles form submission from the /update UI to save changes."""
|
| 56 |
+
page_name = request.form.get('page_name')
|
| 57 |
+
if page_name == 'about':
|
| 58 |
+
paragraphs = request.form.get('paragraphs', '').strip().split('\n')
|
| 59 |
+
paragraphs = [p.strip() for p in paragraphs if p.strip()]
|
| 60 |
+
|
| 61 |
+
update_data = {
|
| 62 |
+
"title": request.form.get('title'),
|
| 63 |
+
"slogan": request.form.get('slogan'),
|
| 64 |
+
"paragraphs": paragraphs
|
| 65 |
+
}
|
| 66 |
+
elif page_name == 'contact':
|
| 67 |
+
update_data = {
|
| 68 |
+
"title": request.form.get('title'),
|
| 69 |
+
"intro": request.form.get('intro'),
|
| 70 |
+
"details": [
|
| 71 |
+
{"type": "Phone Support", "value": request.form.get('phone_value')},
|
| 72 |
+
{"type": "Email Support", "value": request.form.get('email_value')},
|
| 73 |
+
{"type": "Business Hours", "value": request.form.get('hours_value')},
|
| 74 |
+
{"type": "Mailing Address", "value": request.form.get('address_value')}
|
| 75 |
+
]
|
| 76 |
+
}
|
| 77 |
+
else:
|
| 78 |
+
return redirect('/api/update')
|
| 79 |
+
|
| 80 |
+
mongo.db.pages.update_one(
|
| 81 |
+
{'_id': page_name},
|
| 82 |
+
{'$set': update_data},
|
| 83 |
+
upsert=True
|
| 84 |
+
)
|
| 85 |
+
return redirect('/api/update')
|
| 86 |
+
|
| 87 |
+
@page_bp.route('/update_ui', methods=['GET'])
|
| 88 |
+
def update_ui():
|
| 89 |
+
"""Serves the simple HTML UI for editing page content."""
|
| 90 |
+
# Note: Route changed to /update_ui to avoid conflict with /pages/update
|
| 91 |
+
about_data = mongo.db.pages.find_one({'_id': 'about'}) or DEFAULT_ABOUT_DATA
|
| 92 |
+
contact_data = mongo.db.pages.find_one({'_id': 'contact'}) or DEFAULT_CONTACT_DATA
|
| 93 |
+
|
| 94 |
+
about_paragraphs_text = "\n".join(about_data.get('paragraphs', []))
|
| 95 |
+
contact_details = {item['type']: item['value'] for item in contact_data.get('details', [])}
|
| 96 |
+
|
| 97 |
+
html = f"""
|
| 98 |
+
<!DOCTYPE html>
|
| 99 |
+
<html lang="en">
|
| 100 |
+
<head>
|
| 101 |
+
<meta charset="UTF-8">
|
| 102 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 103 |
+
<title>Update Page Content</title>
|
| 104 |
+
<style>
|
| 105 |
+
body {{ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; margin: 2em; background-color: #f8f9fa; color: #212529; }}
|
| 106 |
+
.container {{ max-width: 800px; margin: auto; }}
|
| 107 |
+
h1, h2 {{ color: #343a40; border-bottom: 2px solid #dee2e6; padding-bottom: 0.5em; }}
|
| 108 |
+
form {{ background: white; padding: 2em; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.05); margin-bottom: 2em; }}
|
| 109 |
+
label {{ display: block; margin-top: 1em; margin-bottom: 0.5em; font-weight: bold; color: #495057; }}
|
| 110 |
+
input[type="text"], textarea {{ width: 100%; padding: 0.8em; border: 1px solid #ced4da; border-radius: 4px; box-sizing: border-box; font-size: 1rem; }}
|
| 111 |
+
textarea {{ height: 250px; resize: vertical; }}
|
| 112 |
+
button {{ background-color: #007bff; color: white; padding: 0.8em 1.5em; border: none; border-radius: 4px; cursor: pointer; font-size: 1em; font-weight: bold; }}
|
| 113 |
+
button:hover {{ background-color: #0056b3; }}
|
| 114 |
+
</style>
|
| 115 |
+
</head>
|
| 116 |
+
<body>
|
| 117 |
+
<div class="container">
|
| 118 |
+
<h1>Update Website Content</h1>
|
| 119 |
+
|
| 120 |
+
<!-- About Us Page Form -->
|
| 121 |
+
<h2>About Us Page</h2>
|
| 122 |
+
<form action="/api/pages/update" method="post">
|
| 123 |
+
<input type="hidden" name="page_name" value="about">
|
| 124 |
+
<label for="about_title">Title:</label>
|
| 125 |
+
<input type="text" id="about_title" name="title" value="{about_data.get('title', '')}">
|
| 126 |
+
|
| 127 |
+
<label for="about_slogan">Slogan:</label>
|
| 128 |
+
<input type="text" id="about_slogan" name="slogan" value="{about_data.get('slogan', '')}">
|
| 129 |
+
|
| 130 |
+
<label for="about_paragraphs">Paragraphs (one paragraph per line):</label>
|
| 131 |
+
<textarea id="about_paragraphs" name="paragraphs">{about_paragraphs_text}</textarea>
|
| 132 |
+
|
| 133 |
+
<br><br>
|
| 134 |
+
<button type="submit">Update About Page</button>
|
| 135 |
+
</form>
|
| 136 |
+
|
| 137 |
+
<!-- Contact Page Form -->
|
| 138 |
+
<h2>Customer Care Page</h2>
|
| 139 |
+
<form action="/api/pages/update" method="post">
|
| 140 |
+
<input type="hidden" name="page_name" value="contact">
|
| 141 |
+
|
| 142 |
+
<label for="contact_title">Title:</label>
|
| 143 |
+
<input type="text" id="contact_title" name="title" value="{contact_data.get('title', '')}">
|
| 144 |
+
|
| 145 |
+
<label for="contact_intro">Intro Text:</label>
|
| 146 |
+
<input type="text" id="contact_intro" name="intro" value="{contact_data.get('intro', '')}">
|
| 147 |
+
|
| 148 |
+
<label for="phone_value">Phone Support:</label>
|
| 149 |
+
<input type="text" id="phone_value" name="phone_value" value="{contact_details.get('Phone Support', '')}">
|
| 150 |
+
|
| 151 |
+
<label for="email_value">Email Support:</label>
|
| 152 |
+
<input type="text" id="email_value" name="email_value" value="{contact_details.get('Email Support', '')}">
|
| 153 |
+
|
| 154 |
+
<label for="hours_value">Business Hours:</label>
|
| 155 |
+
<input type="text" id="hours_value" name="hours_value" value="{contact_details.get('Business Hours', '')}">
|
| 156 |
+
|
| 157 |
+
<label for="address_value">Mailing Address:</label>
|
| 158 |
+
<input type="text" id="address_value" name="address_value" value="{contact_details.get('Mailing Address', '')}">
|
| 159 |
+
|
| 160 |
+
<br><br>
|
| 161 |
+
<button type="submit">Update Contact Page</button>
|
| 162 |
+
</form>
|
| 163 |
+
</div>
|
| 164 |
+
</body>
|
| 165 |
+
</html>
|
| 166 |
+
"""
|
| 167 |
+
return html
|
app/xero_utils.py
CHANGED
|
@@ -269,5 +269,4 @@ def trigger_po_creation(order_details):
|
|
| 269 |
thread.daemon = True
|
| 270 |
thread.start()
|
| 271 |
except Exception as e:
|
| 272 |
-
logger.error("Failed to start Xero PO creation thread for order %s. Error: %s", order_details.get('order_id'), e)
|
| 273 |
-
|
|
|
|
| 269 |
thread.daemon = True
|
| 270 |
thread.start()
|
| 271 |
except Exception as e:
|
| 272 |
+
logger.error("Failed to start Xero PO creation thread for order %s. Error: %s", order_details.get('order_id'), e)
|
|
|
flask_session/2029240f6d1128be89ddc32729463129
CHANGED
|
Binary files a/flask_session/2029240f6d1128be89ddc32729463129 and b/flask_session/2029240f6d1128be89ddc32729463129 differ
|
|
|
flask_session/aa71dde20eaf768ca7e5f90a25563ea6
ADDED
|
Binary file (179 Bytes). View file
|
|
|
flask_session/d0efef4e2c8720847d7a47ac360d9d46
ADDED
|
Binary file (153 Bytes). View file
|
|
|
twillio_gemini_api.py
CHANGED
|
@@ -1,56 +1,155 @@
|
|
| 1 |
# app.py
|
| 2 |
|
| 3 |
import os
|
|
|
|
| 4 |
from flask import Flask, request
|
| 5 |
from dotenv import load_dotenv
|
| 6 |
-
|
|
|
|
|
|
|
| 7 |
from twilio.twiml.messaging_response import MessagingResponse
|
| 8 |
|
| 9 |
-
#
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
# --- Initialize Flask App and APIs ---
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
app = Flask(__name__)
|
| 14 |
|
| 15 |
-
# Configure the Gemini API
|
| 16 |
try:
|
| 17 |
-
genai.
|
| 18 |
-
|
| 19 |
-
# Use 'gemini-1.5-pro-latest' for the most recent version
|
| 20 |
-
model = genai.GenerativeModel('gemini-1.5-pro-latest')
|
| 21 |
-
print("Gemini model initialized successfully.")
|
| 22 |
except Exception as e:
|
| 23 |
-
print(f"Error initializing Gemini
|
| 24 |
-
|
|
|
|
| 25 |
|
| 26 |
# --- Define the Webhook Endpoint ---
|
| 27 |
@app.route("/sms", methods=['POST'])
|
| 28 |
def sms_reply():
|
| 29 |
-
"""Respond to incoming messages
|
| 30 |
|
| 31 |
-
# Get the message body from the incoming Twilio request
|
| 32 |
-
incoming_msg = request.values.get('Body', '').strip()
|
| 33 |
-
print(f"Received message: '{incoming_msg}'")
|
| 34 |
-
|
| 35 |
-
# Start the TwiML response
|
| 36 |
twilio_resp = MessagingResponse()
|
| 37 |
|
| 38 |
-
if not
|
| 39 |
-
twilio_resp.message("The AI
|
| 40 |
return str(twilio_resp)
|
| 41 |
|
| 42 |
-
|
| 43 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
return str(twilio_resp)
|
| 45 |
|
| 46 |
try:
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
|
| 51 |
-
#
|
| 52 |
ai_answer = gemini_response.text
|
| 53 |
-
print(f"Gemini response: '{ai_answer}'")
|
| 54 |
|
| 55 |
# Add the Gemini response to the TwiML message
|
| 56 |
twilio_resp.message(ai_answer)
|
|
@@ -61,12 +160,9 @@ def sms_reply():
|
|
| 61 |
error_message = "Sorry, I'm having trouble connecting to my brain right now. Please try again later."
|
| 62 |
twilio_resp.message(error_message)
|
| 63 |
|
| 64 |
-
# Return the TwiML response as a string
|
| 65 |
return str(twilio_resp)
|
| 66 |
|
| 67 |
|
| 68 |
# --- Run the Flask App ---
|
| 69 |
if __name__ == "__main__":
|
| 70 |
-
# The debug=True option is for development only.
|
| 71 |
-
# In production, use a proper WSGI server like Gunicorn or Waitress.
|
| 72 |
app.run(debug=True, port=5000)
|
|
|
|
| 1 |
# app.py
|
| 2 |
|
| 3 |
import os
|
| 4 |
+
import requests # To download the audio file from Twilio's URL
|
| 5 |
from flask import Flask, request
|
| 6 |
from dotenv import load_dotenv
|
| 7 |
+
# Use the new top-level import and types
|
| 8 |
+
from google import genai
|
| 9 |
+
from google.genai import types
|
| 10 |
from twilio.twiml.messaging_response import MessagingResponse
|
| 11 |
|
| 12 |
+
# --- Define Calculator Functions for the Model to Use ---
|
| 13 |
+
# The SDK uses the function signature (name, parameters) and docstring
|
| 14 |
+
# to tell the model how and when to use these tools.
|
| 15 |
+
|
| 16 |
+
def add(a: float, b: float):
|
| 17 |
+
"""
|
| 18 |
+
Adds two numbers together.
|
| 19 |
+
|
| 20 |
+
Args:
|
| 21 |
+
a: The first number.
|
| 22 |
+
b: The second number.
|
| 23 |
+
"""
|
| 24 |
+
print(f"Tool Call: add(a={a}, b={b})")
|
| 25 |
+
return a + b
|
| 26 |
+
|
| 27 |
+
def subtract(a: float, b: float):
|
| 28 |
+
"""
|
| 29 |
+
Subtracts the second number from the first.
|
| 30 |
+
|
| 31 |
+
Args:
|
| 32 |
+
a: The number to subtract from.
|
| 33 |
+
b: The number to subtract.
|
| 34 |
+
"""
|
| 35 |
+
print(f"Tool Call: subtract(a={a}, b={b})")
|
| 36 |
+
return a - b
|
| 37 |
+
|
| 38 |
+
def multiply(a: float, b: float):
|
| 39 |
+
"""
|
| 40 |
+
Multiplies two numbers.
|
| 41 |
+
|
| 42 |
+
Args:
|
| 43 |
+
a: The first number.
|
| 44 |
+
b: The second number.
|
| 45 |
+
"""
|
| 46 |
+
print(f"Tool Call: multiply(a={a}, b={b})")
|
| 47 |
+
return a * b
|
| 48 |
+
|
| 49 |
+
def divide(a: float, b: float):
|
| 50 |
+
"""
|
| 51 |
+
Divides the first number by the second.
|
| 52 |
+
|
| 53 |
+
Args:
|
| 54 |
+
a: The numerator.
|
| 55 |
+
b: The denominator.
|
| 56 |
+
"""
|
| 57 |
+
print(f"Tool Call: divide(a={a}, b={b})")
|
| 58 |
+
if b == 0:
|
| 59 |
+
return "Error: Cannot divide by zero."
|
| 60 |
+
return a / b
|
| 61 |
+
|
| 62 |
+
# A list of all the functions the model can call
|
| 63 |
+
calculator_tools = [add, subtract, multiply, divide]
|
| 64 |
+
|
| 65 |
|
| 66 |
# --- Initialize Flask App and APIs ---
|
| 67 |
+
|
| 68 |
+
# Load environment variables from .env file
|
| 69 |
+
load_dotenv(r"C:\Users\Vaibhav Arora\Documents\MyExperimentsandCodes\APPS_WEBSITES\CANADA_WHOLESALE_PROJECT\GITHUB_REPOS\mvp-vue\wholesale-grocery-app\AIAPPS\.env")
|
| 70 |
+
|
| 71 |
app = Flask(__name__)
|
| 72 |
|
| 73 |
+
# Configure the Gemini API and initialize the client
|
| 74 |
try:
|
| 75 |
+
client = genai.Client(api_key=os.environ.get("GEMINI_API_KEY"))
|
| 76 |
+
print("Gemini client and calculator tools initialized successfully.")
|
|
|
|
|
|
|
|
|
|
| 77 |
except Exception as e:
|
| 78 |
+
print(f"Error initializing Gemini client: {e}")
|
| 79 |
+
client = None
|
| 80 |
+
|
| 81 |
|
| 82 |
# --- Define the Webhook Endpoint ---
|
| 83 |
@app.route("/sms", methods=['POST'])
|
| 84 |
def sms_reply():
|
| 85 |
+
"""Respond to incoming text or audio messages, using calculator functions if needed."""
|
| 86 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
twilio_resp = MessagingResponse()
|
| 88 |
|
| 89 |
+
if not client:
|
| 90 |
+
twilio_resp.message("The AI client is not configured correctly. Please check the server logs.")
|
| 91 |
return str(twilio_resp)
|
| 92 |
|
| 93 |
+
# Prepare the contents list for the Gemini API call
|
| 94 |
+
contents = []
|
| 95 |
+
|
| 96 |
+
# Check if the incoming message contains media
|
| 97 |
+
num_media = int(request.values.get('NumMedia', 0))
|
| 98 |
+
|
| 99 |
+
if num_media > 0:
|
| 100 |
+
media_url = request.values.get('MediaUrl0')
|
| 101 |
+
mime_type = request.values.get('MediaContentType0')
|
| 102 |
+
|
| 103 |
+
# Process only if the media is audio
|
| 104 |
+
if 'audio' in mime_type:
|
| 105 |
+
print(f"Received audio message. URL: {media_url}, MIME Type: {mime_type}")
|
| 106 |
+
|
| 107 |
+
# Download the audio file from the Twilio URL
|
| 108 |
+
audio_response = requests.get(media_url)
|
| 109 |
+
|
| 110 |
+
if audio_response.status_code == 200:
|
| 111 |
+
audio_bytes = audio_response.content
|
| 112 |
+
audio_part = types.Part.from_bytes(data=audio_bytes, mime_type=mime_type)
|
| 113 |
+
prompt = "Please transcribe this audio. If it contains a calculation or a question, please answer it."
|
| 114 |
+
contents = [prompt, audio_part]
|
| 115 |
+
else:
|
| 116 |
+
error_message = "Sorry, I couldn't download the audio file to process it. Please try again."
|
| 117 |
+
twilio_resp.message(error_message)
|
| 118 |
+
return str(twilio_resp)
|
| 119 |
+
else:
|
| 120 |
+
# Handle non-audio media like images or videos
|
| 121 |
+
twilio_resp.message("Sorry, I can only process text and audio messages.")
|
| 122 |
+
return str(twilio_resp)
|
| 123 |
+
|
| 124 |
+
else:
|
| 125 |
+
# Fallback to text message processing
|
| 126 |
+
incoming_msg = request.values.get('Body', '').strip()
|
| 127 |
+
print(f"Received text message: '{incoming_msg}'")
|
| 128 |
+
if not incoming_msg:
|
| 129 |
+
twilio_resp.message("Please send a text or audio message to get a response.")
|
| 130 |
+
return str(twilio_resp)
|
| 131 |
+
contents = [incoming_msg]
|
| 132 |
+
|
| 133 |
+
if not contents:
|
| 134 |
+
twilio_resp.message("Could not determine content to process. Please send a message.")
|
| 135 |
return str(twilio_resp)
|
| 136 |
|
| 137 |
try:
|
| 138 |
+
print("Sending content to Gemini with calculator tools...")
|
| 139 |
+
|
| 140 |
+
# Configure the request to use our calculator functions
|
| 141 |
+
config = types.GenerateContentConfig(tools=calculator_tools)
|
| 142 |
+
|
| 143 |
+
# The SDK handles the multi-turn process for function calling automatically
|
| 144 |
+
gemini_response = client.models.generate_content(
|
| 145 |
+
model="gemini-2.5-flash",
|
| 146 |
+
contents=contents, # This will contain either text or [prompt, audio_part]
|
| 147 |
+
config=config,
|
| 148 |
+
)
|
| 149 |
|
| 150 |
+
# This is the final text answer after any function calls have been resolved.
|
| 151 |
ai_answer = gemini_response.text
|
| 152 |
+
print(f"Gemini final response: '{ai_answer}'")
|
| 153 |
|
| 154 |
# Add the Gemini response to the TwiML message
|
| 155 |
twilio_resp.message(ai_answer)
|
|
|
|
| 160 |
error_message = "Sorry, I'm having trouble connecting to my brain right now. Please try again later."
|
| 161 |
twilio_resp.message(error_message)
|
| 162 |
|
|
|
|
| 163 |
return str(twilio_resp)
|
| 164 |
|
| 165 |
|
| 166 |
# --- Run the Flask App ---
|
| 167 |
if __name__ == "__main__":
|
|
|
|
|
|
|
| 168 |
app.run(debug=True, port=5000)
|