Spaces:
Runtime error
Runtime error
Add application file
Browse files- Dockerfile +1 -1
- main.py +97 -0
- matching.py +72 -0
- models.py +153 -0
- requirements.txt +53 -2
- schema.sql +62 -0
Dockerfile
CHANGED
|
@@ -10,4 +10,4 @@ COPY --chown=user ./requirements.txt requirements.txt
|
|
| 10 |
RUN pip install --no-cache-dir --upgrade -r requirements.txt
|
| 11 |
|
| 12 |
COPY --chown=user . /app
|
| 13 |
-
CMD ["uvicorn", "
|
|
|
|
| 10 |
RUN pip install --no-cache-dir --upgrade -r requirements.txt
|
| 11 |
|
| 12 |
COPY --chown=user . /app
|
| 13 |
+
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
|
main.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI, Form
|
| 2 |
+
from twilio.rest import Client
|
| 3 |
+
from dotenv import load_dotenv
|
| 4 |
+
import os
|
| 5 |
+
from matching import match_technician, NLPProcessor
|
| 6 |
+
from models import Database
|
| 7 |
+
from transformers import pipeline
|
| 8 |
+
|
| 9 |
+
nlp = NLPProcessor()
|
| 10 |
+
|
| 11 |
+
app = FastAPI()
|
| 12 |
+
load_dotenv()
|
| 13 |
+
|
| 14 |
+
HF_API_URL = "https://api-inference.huggingface.co/models/Christy123/service-classifier"
|
| 15 |
+
HF_TOKEN = os.getenv("HF_API_TOKEN")
|
| 16 |
+
|
| 17 |
+
# Twilio client
|
| 18 |
+
twilio_client = Client(os.getenv("TWILIO_ACCOUNT_SID"), os.getenv("TWILIO_AUTH_TOKEN"))
|
| 19 |
+
|
| 20 |
+
def send_message(to_number: str, body: str):
|
| 21 |
+
try:
|
| 22 |
+
if not to_number.startswith("whatsapp:"):
|
| 23 |
+
to_number = f"whatsapp:{to_number.lstrip('+')}"
|
| 24 |
+
|
| 25 |
+
message = twilio_client.messages.create(
|
| 26 |
+
from_=os.getenv("TWILIO_NUMBER"),
|
| 27 |
+
body=body,
|
| 28 |
+
to="whatsapp:+254792552491"
|
| 29 |
+
)
|
| 30 |
+
return message.sid
|
| 31 |
+
except Exception as e:
|
| 32 |
+
print(f"Twilio error: {str(e)}")
|
| 33 |
+
return None
|
| 34 |
+
|
| 35 |
+
def classify_text(text: str):
|
| 36 |
+
headers = {"Authorization": f"Bearer {HF_TOKEN}"}
|
| 37 |
+
response = requests.post(HF_API_URL, headers=headers, json={"inputs": text})
|
| 38 |
+
|
| 39 |
+
if response.status_code != 200:
|
| 40 |
+
return {"error": "Model inference failed"}
|
| 41 |
+
|
| 42 |
+
return response.json()
|
| 43 |
+
|
| 44 |
+
@app.post("/predict")
|
| 45 |
+
async def predict(text: str):
|
| 46 |
+
result = classify_text(text)
|
| 47 |
+
return {"service": result[0]["label"], "confidence": result[0]["score"]}
|
| 48 |
+
|
| 49 |
+
@app.post("/message")
|
| 50 |
+
async def handle_message(From: str = Form(...), Body: str = Form(...)):
|
| 51 |
+
db = Database()
|
| 52 |
+
user_state = db.get_user_state(From)
|
| 53 |
+
|
| 54 |
+
# Case 1: User is in menu flow (1/2/3 selection)
|
| 55 |
+
if user_state and user_state["state"] == "awaiting_service" and Body.strip() in ["1", "2", "3"]:
|
| 56 |
+
services = {"1": "plumbing", "2": "electrical", "3": "hvac"}
|
| 57 |
+
service_type = services.get(Body.strip())
|
| 58 |
+
db.update_user_state(From, "awaiting_location", service_type)
|
| 59 |
+
db.close()
|
| 60 |
+
send_message(From, "Please share your city or area.")
|
| 61 |
+
return {"status": "awaiting_location"}
|
| 62 |
+
|
| 63 |
+
# Case 2: User sends free-text request (e.g., "AC repair in Mombasa")
|
| 64 |
+
if not user_state or user_state["state"] == "idle":
|
| 65 |
+
technician, error = match_technician(Body, From)
|
| 66 |
+
if error:
|
| 67 |
+
send_message(From, error)
|
| 68 |
+
return {"status": "error"}
|
| 69 |
+
|
| 70 |
+
response = (
|
| 71 |
+
f"Found {technician['name']} (Rating: {technician['rating']}/5) "
|
| 72 |
+
f"for your {nlp.extract_service(Body)} request. "
|
| 73 |
+
f"Contact: {technician['contact']}. Confirm? (Yes/No)"
|
| 74 |
+
)
|
| 75 |
+
send_message(From, response)
|
| 76 |
+
db.update_user_state(From, "awaiting_confirmation", response)
|
| 77 |
+
db.close()
|
| 78 |
+
return {"status": "success"}
|
| 79 |
+
|
| 80 |
+
# Case 3: Handle confirmation/feedback
|
| 81 |
+
db.close()
|
| 82 |
+
return {"status": "processed"}
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
# main.py
|
| 87 |
+
@app.on_event("startup")
|
| 88 |
+
async def startup_db():
|
| 89 |
+
db = Database()
|
| 90 |
+
try:
|
| 91 |
+
# Simple query to verify tables
|
| 92 |
+
db.cursor.execute("SELECT 1 FROM users LIMIT 1")
|
| 93 |
+
except psycopg2.Error as e:
|
| 94 |
+
print(f"CRITICAL: Database not initialized. Run schema.sql first")
|
| 95 |
+
raise
|
| 96 |
+
finally:
|
| 97 |
+
db.close()
|
matching.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from models import Database
|
| 2 |
+
from geopy.geocoders import Nominatim
|
| 3 |
+
import os
|
| 4 |
+
from dotenv import load_dotenv
|
| 5 |
+
from models import NLPProcessor
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
nlp = NLPProcessor()
|
| 9 |
+
load_dotenv()
|
| 10 |
+
|
| 11 |
+
def match_technician(text: str, user_number: str):
|
| 12 |
+
db = Database()
|
| 13 |
+
try:
|
| 14 |
+
service_type = nlp.extract_service(text)
|
| 15 |
+
location = nlp.extract_location(text)
|
| 16 |
+
|
| 17 |
+
if service_type == "unknown":
|
| 18 |
+
return None, "Could not determine service. Please specify (e.g., 'AC repair', 'plumber')"
|
| 19 |
+
|
| 20 |
+
if not location:
|
| 21 |
+
return None, "Could not detect location. Try: 'AC repair in Nairobi'"
|
| 22 |
+
|
| 23 |
+
longitude, latitude = geocode_location(location)
|
| 24 |
+
technician = db.find_technician(service_type, longitude, latitude)
|
| 25 |
+
|
| 26 |
+
if not technician:
|
| 27 |
+
return None, f"No {service_type} technicians near {location}"
|
| 28 |
+
|
| 29 |
+
db.save_request(user_number, technician["id"], service_type)
|
| 30 |
+
return technician, None
|
| 31 |
+
|
| 32 |
+
except Exception as e:
|
| 33 |
+
return None, f"System error: {str(e)}"
|
| 34 |
+
finally:
|
| 35 |
+
db.close()
|
| 36 |
+
|
| 37 |
+
# def match_technician(text: str, user_number: str):
|
| 38 |
+
# technician = None
|
| 39 |
+
# db = Database()
|
| 40 |
+
# try:
|
| 41 |
+
|
| 42 |
+
# # Step 1: Extract service type and location using DistilBERT
|
| 43 |
+
# service_type = nlp.extract_service(text)
|
| 44 |
+
# location = nlp.extract_location(text)
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
# if not location:
|
| 48 |
+
# return None, "Could not detect your location. Please specify (e.g., 'AC repair in Nairobi')."
|
| 49 |
+
|
| 50 |
+
# # Step 2: Get coordinates (mock function - replace with real geocoding)
|
| 51 |
+
# longitude, latitude = geocode_location(location)
|
| 52 |
+
|
| 53 |
+
# # Step 3: Find nearest technician
|
| 54 |
+
# technician = db.find_technician(service_type, longitude, latitude)
|
| 55 |
+
# if not technician:
|
| 56 |
+
# db.close()
|
| 57 |
+
# return None, f"No available {service_type} technicians near {location}."
|
| 58 |
+
|
| 59 |
+
# # Step 4: Save request to DB
|
| 60 |
+
# request_id = db.save_request(user_number, technician["id"], service_type)
|
| 61 |
+
# return technician, None
|
| 62 |
+
# except Exception as e:
|
| 63 |
+
# return None, f"System error: {str(e)}"
|
| 64 |
+
# finally:
|
| 65 |
+
# db.close()
|
| 66 |
+
|
| 67 |
+
def geocode_location(location: str) -> tuple[float, float]:
|
| 68 |
+
geolocator = Nominatim(user_agent="technician_matcher")
|
| 69 |
+
location = geolocator.geocode(location + ", Kenya") # Adjust country as needed
|
| 70 |
+
if location:
|
| 71 |
+
return (location.longitude, location.latitude)
|
| 72 |
+
return (-1.2921, 36.8219) # Fallback to Nairobi
|
models.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import psycopg2
|
| 2 |
+
from dotenv import load_dotenv
|
| 3 |
+
import os
|
| 4 |
+
from functools import lru_cache
|
| 5 |
+
from transformers import pipeline
|
| 6 |
+
load_dotenv()
|
| 7 |
+
|
| 8 |
+
class Database:
|
| 9 |
+
def __init__(self):
|
| 10 |
+
try:
|
| 11 |
+
self.conn = psycopg2.connect(os.getenv("POSTGRES_URL"))
|
| 12 |
+
self.cursor = self.conn.cursor()
|
| 13 |
+
# Verify tables exist on startup
|
| 14 |
+
self.cursor.execute("""
|
| 15 |
+
SELECT EXISTS (
|
| 16 |
+
SELECT FROM information_schema.tables
|
| 17 |
+
WHERE table_name = 'users'
|
| 18 |
+
)
|
| 19 |
+
""")
|
| 20 |
+
if not self.cursor.fetchone()[0]:
|
| 21 |
+
raise RuntimeError("Database tables not initialized")
|
| 22 |
+
except Exception as e:
|
| 23 |
+
print(f"Database connection error: {str(e)}")
|
| 24 |
+
raise
|
| 25 |
+
|
| 26 |
+
def find_technician(self, service_type: str, longitude: float, latitude: float):
|
| 27 |
+
query = """
|
| 28 |
+
SELECT id, name, contact, rating
|
| 29 |
+
FROM technicians
|
| 30 |
+
WHERE %s = ANY(qualifications)
|
| 31 |
+
AND availability = 'available'
|
| 32 |
+
ORDER BY location <-> ST_SetSRID(ST_MakePoint(%s, %s), 4326)
|
| 33 |
+
LIMIT 1
|
| 34 |
+
"""
|
| 35 |
+
self.cursor.execute(query, (service_type, longitude, latitude))
|
| 36 |
+
result = self.cursor.fetchone()
|
| 37 |
+
if result:
|
| 38 |
+
return {
|
| 39 |
+
"id": result[0],
|
| 40 |
+
"name": result[1],
|
| 41 |
+
"contact": result[2],
|
| 42 |
+
"rating": result[3]
|
| 43 |
+
}
|
| 44 |
+
return None
|
| 45 |
+
|
| 46 |
+
def get_user_state(self, user_number: str):
|
| 47 |
+
query = "SELECT state, last_message FROM users WHERE number = %s"
|
| 48 |
+
self.cursor.execute(query, (user_number,))
|
| 49 |
+
result = self.cursor.fetchone()
|
| 50 |
+
return {"state": result[0], "last_message": result[1]} if result else None
|
| 51 |
+
|
| 52 |
+
def update_user_state(self, user_number: str, state: str, last_message: str):
|
| 53 |
+
query = """
|
| 54 |
+
INSERT INTO users (number, state, last_message)
|
| 55 |
+
VALUES (%s, %s, %s)
|
| 56 |
+
ON CONFLICT (number) DO UPDATE
|
| 57 |
+
SET state = %s, last_message = %s, updated_at = CURRENT_TIMESTAMP
|
| 58 |
+
"""
|
| 59 |
+
self.cursor.execute(query, (user_number, state, last_message, state, last_message))
|
| 60 |
+
self.conn.commit()
|
| 61 |
+
|
| 62 |
+
def save_request(self, user_number: str, technician_id: int, service_type: str):
|
| 63 |
+
query = """
|
| 64 |
+
INSERT INTO requests (user_number, technician_id, service_type, status)
|
| 65 |
+
VALUES (%s, %s, %s, %s)
|
| 66 |
+
RETURNING id
|
| 67 |
+
"""
|
| 68 |
+
self.cursor.execute(query, (user_number, technician_id, service_type, "pending"))
|
| 69 |
+
self.conn.commit()
|
| 70 |
+
return self.cursor.fetchone()[0]
|
| 71 |
+
|
| 72 |
+
def close(self):
|
| 73 |
+
self.cursor.close()
|
| 74 |
+
self.conn.close()
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
class NLPProcessor:
|
| 78 |
+
def __init__(self):
|
| 79 |
+
# Initialize all required attributes
|
| 80 |
+
self.api_url = "https://api-inference.huggingface.co/models/Christy123/service-classifier"
|
| 81 |
+
self.api_token = os.getenv("HF_API_TOKEN") # Make sure this is in your .env
|
| 82 |
+
self.ner_url = "https://api-inference.huggingface.co/models/dbmdz/bert-large-cased-finetuned-conll03-english"
|
| 83 |
+
self.service_mappings = {
|
| 84 |
+
"ac": "hvac",
|
| 85 |
+
"air conditioner": "hvac",
|
| 86 |
+
"plumb": "plumbing",
|
| 87 |
+
"pipe": "plumbing",
|
| 88 |
+
"electr": "electrical",
|
| 89 |
+
"wiring": "electrical"
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
def extract_service(self, text: str) -> str:
|
| 93 |
+
"""Enhanced service classification with fallback"""
|
| 94 |
+
try:
|
| 95 |
+
# Try Hugging Face API first
|
| 96 |
+
headers = {"Authorization": f"Bearer {os.getenv('HF_API_TOKEN')}"}
|
| 97 |
+
response = requests.post(
|
| 98 |
+
"https://api-inference.huggingface.co/models/Christy123/service-classifier",
|
| 99 |
+
headers=headers,
|
| 100 |
+
json={"inputs": text},
|
| 101 |
+
timeout=5
|
| 102 |
+
)
|
| 103 |
+
|
| 104 |
+
if response.status_code == 200:
|
| 105 |
+
result = response.json()[0]
|
| 106 |
+
if result["score"] > 0.7: # Only accept confident predictions
|
| 107 |
+
return result["label"]
|
| 108 |
+
|
| 109 |
+
# Fallback to keyword matching
|
| 110 |
+
text_lower = text.lower()
|
| 111 |
+
for keyword, service in self.service_mappings.items():
|
| 112 |
+
if keyword in text_lower:
|
| 113 |
+
return service
|
| 114 |
+
|
| 115 |
+
return "unknown"
|
| 116 |
+
|
| 117 |
+
except Exception:
|
| 118 |
+
return "unknown"
|
| 119 |
+
|
| 120 |
+
def extract_location(self, text: str) -> str:
|
| 121 |
+
"""Enhanced location detection with fallback methods"""
|
| 122 |
+
headers = {"Authorization": f"Bearer {self.api_token}"}
|
| 123 |
+
|
| 124 |
+
# Try Hugging Face NER first
|
| 125 |
+
try:
|
| 126 |
+
response = requests.post(
|
| 127 |
+
"https://api-inference.huggingface.co/models/dbmdz/bert-large-cased-finetuned-conll03-english",
|
| 128 |
+
headers=headers,
|
| 129 |
+
json={"inputs": text},
|
| 130 |
+
timeout=5
|
| 131 |
+
)
|
| 132 |
+
if response.status_code == 200:
|
| 133 |
+
entities = response.json()
|
| 134 |
+
locations = [e["word"] for e in entities if e["entity_group"] == "LOC"]
|
| 135 |
+
if locations:
|
| 136 |
+
return locations[0]
|
| 137 |
+
except Exception:
|
| 138 |
+
pass
|
| 139 |
+
|
| 140 |
+
# Fallback 1: Simple keyword matching
|
| 141 |
+
kenyan_towns = ["Nairobi", "Mombasa", "Kisumu", "Nakuru", "Eldoret",
|
| 142 |
+
"Westlands", "Karen", "Runda", "Thika", "Naivasha"]
|
| 143 |
+
for town in kenyan_towns:
|
| 144 |
+
if town.lower() in text.lower():
|
| 145 |
+
return town
|
| 146 |
+
|
| 147 |
+
# Fallback 2: Look for "in <location>" pattern
|
| 148 |
+
import re
|
| 149 |
+
match = re.search(r"\bin\s+([A-Za-z]+)", text, re.IGNORECASE)
|
| 150 |
+
if match:
|
| 151 |
+
return match.group(1)
|
| 152 |
+
|
| 153 |
+
return None
|
requirements.txt
CHANGED
|
@@ -1,2 +1,53 @@
|
|
| 1 |
-
|
| 2 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
aiohappyeyeballs==2.6.1
|
| 2 |
+
aiohttp==3.12.14
|
| 3 |
+
aiohttp-retry==2.9.1
|
| 4 |
+
aiosignal==1.4.0
|
| 5 |
+
annotated-types==0.7.0
|
| 6 |
+
anyio==4.9.0
|
| 7 |
+
asyncpg==0.30.0
|
| 8 |
+
attrs==25.3.0
|
| 9 |
+
certifi==2025.7.9
|
| 10 |
+
charset-normalizer==3.4.2
|
| 11 |
+
click==8.2.1
|
| 12 |
+
distro==1.9.0
|
| 13 |
+
dnspython==2.7.0
|
| 14 |
+
fastapi==0.116.0
|
| 15 |
+
filelock==3.18.0
|
| 16 |
+
frozenlist==1.7.0
|
| 17 |
+
fsspec==2025.5.1
|
| 18 |
+
geographiclib==2.0
|
| 19 |
+
geopy==2.4.1
|
| 20 |
+
h11==0.16.0
|
| 21 |
+
hf-xet==1.1.5
|
| 22 |
+
httpcore==1.0.9
|
| 23 |
+
httpx==0.28.1
|
| 24 |
+
huggingface-hub==0.33.4
|
| 25 |
+
idna==3.10
|
| 26 |
+
jiter==0.10.0
|
| 27 |
+
motor==3.7.1
|
| 28 |
+
multidict==6.6.3
|
| 29 |
+
numpy==2.3.1
|
| 30 |
+
openai==1.95.1
|
| 31 |
+
packaging==25.0
|
| 32 |
+
propcache==0.3.2
|
| 33 |
+
psycopg2-binary==2.9.10
|
| 34 |
+
pydantic==2.11.7
|
| 35 |
+
pydantic_core==2.33.2
|
| 36 |
+
PyJWT==2.10.1
|
| 37 |
+
pymongo==4.13.2
|
| 38 |
+
python-dotenv==1.1.1
|
| 39 |
+
PyYAML==6.0.2
|
| 40 |
+
regex==2024.11.6
|
| 41 |
+
requests==2.32.4
|
| 42 |
+
safetensors==0.5.3
|
| 43 |
+
sniffio==1.3.1
|
| 44 |
+
starlette==0.46.2
|
| 45 |
+
tokenizers==0.21.2
|
| 46 |
+
tqdm==4.67.1
|
| 47 |
+
transformers==4.53.2
|
| 48 |
+
twilio==9.6.5
|
| 49 |
+
typing-inspection==0.4.1
|
| 50 |
+
typing_extensions==4.14.1
|
| 51 |
+
urllib3==2.5.0
|
| 52 |
+
uvicorn==0.35.0
|
| 53 |
+
yarl==1.20.1
|
schema.sql
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
-- Enable PostGIS (run once)
|
| 2 |
+
CREATE EXTENSION IF NOT EXISTS postgis;
|
| 3 |
+
|
| 4 |
+
-- Create technicians table
|
| 5 |
+
CREATE TABLE IF NOT EXISTS technicians (
|
| 6 |
+
id SERIAL PRIMARY KEY,
|
| 7 |
+
name VARCHAR(100) NOT NULL,
|
| 8 |
+
qualifications TEXT[] NOT NULL,
|
| 9 |
+
location GEOGRAPHY(POINT) NOT NULL,
|
| 10 |
+
availability VARCHAR(20) DEFAULT 'available',
|
| 11 |
+
rating FLOAT DEFAULT 5.0,
|
| 12 |
+
contact VARCHAR(50) NOT NULL
|
| 13 |
+
);
|
| 14 |
+
|
| 15 |
+
-- Create index for geospatial queries
|
| 16 |
+
CREATE INDEX IF NOT EXISTS technicians_location_idx ON technicians USING GIST (location);
|
| 17 |
+
|
| 18 |
+
-- Insert sample technician
|
| 19 |
+
INSERT INTO technicians (name, qualifications, location, availability, rating, contact)
|
| 20 |
+
VALUES (
|
| 21 |
+
'John Doe',
|
| 22 |
+
ARRAY['plumbing', 'electrical'],
|
| 23 |
+
ST_SetSRID(ST_MakePoint(36.8219, -1.2921), 4326),
|
| 24 |
+
'available',
|
| 25 |
+
4.8,
|
| 26 |
+
'whatsapp:+254123456789'
|
| 27 |
+
);
|
| 28 |
+
|
| 29 |
+
-- Create users table for session tracking
|
| 30 |
+
CREATE TABLE IF NOT EXISTS users (
|
| 31 |
+
number VARCHAR(50) PRIMARY KEY,
|
| 32 |
+
last_message TEXT,
|
| 33 |
+
state VARCHAR(50),
|
| 34 |
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
| 35 |
+
);
|
| 36 |
+
|
| 37 |
+
-- Create requests table for feedback collection
|
| 38 |
+
CREATE TABLE IF NOT EXISTS requests (
|
| 39 |
+
id SERIAL PRIMARY KEY,
|
| 40 |
+
user_number VARCHAR(50),
|
| 41 |
+
technician_id INT,
|
| 42 |
+
service_type VARCHAR(50),
|
| 43 |
+
status VARCHAR(20),
|
| 44 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 45 |
+
FOREIGN KEY (technician_id) REFERENCES technicians(id)
|
| 46 |
+
);
|
| 47 |
+
|
| 48 |
+
-- Check if technicians exist
|
| 49 |
+
SELECT * FROM technicians;
|
| 50 |
+
|
| 51 |
+
-- If empty, insert sample data:
|
| 52 |
+
INSERT INTO technicians (name, qualifications, location, contact)
|
| 53 |
+
VALUES (
|
| 54 |
+
'John Doe',
|
| 55 |
+
ARRAY['hvac', 'electrical'],
|
| 56 |
+
ST_SetSRID(ST_MakePoint(36.8219, -1.2921), 4326), -- Nairobi coords
|
| 57 |
+
'+254712345678'
|
| 58 |
+
);
|
| 59 |
+
|
| 60 |
+
INSERT INTO technicians (name, qualifications, location, contact) VALUES
|
| 61 |
+
('AC Specialist', ARRAY['hvac'], ST_MakePoint(36.81, -1.29), '+254700000001'),
|
| 62 |
+
('Plumber Ltd', ARRAY['plumbing'], ST_MakePoint(36.82, -1.30), '+254700000002');
|