Groo12 commited on
Commit
7cc0170
·
1 Parent(s): 32a7f13

Add application file

Browse files
Files changed (6) hide show
  1. Dockerfile +1 -1
  2. main.py +97 -0
  3. matching.py +72 -0
  4. models.py +153 -0
  5. requirements.txt +53 -2
  6. 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", "app:app", "--host", "0.0.0.0", "--port", "7860"]
 
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
- fastapi
2
- uvicorn
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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');