codeBOKER commited on
Commit
b5a802c
·
1 Parent(s): 1cd53ba

add driver name search for passengers

Browse files
app/ai/tool_schemas.py CHANGED
@@ -33,7 +33,7 @@ _SEARCH_TRIPS = {
33
  "function": {
34
  "name": "search_trips",
35
  "description": (
36
- "Search active trips by departure/destination (either can be omitted). "
37
  "Results sent as WhatsApp cards automatically. "
38
  "Times Asia/Aden. Buckets: صباح before 12, ظهر 12-17:59, ليل 18+."
39
  ),
@@ -74,6 +74,10 @@ _SEARCH_TRIPS = {
74
  "type": "string",
75
  "description": "Optional car type in Arabic, for example سيارة or باص.",
76
  },
 
 
 
 
77
  "vector_query_text": {
78
  "type": "string",
79
  "description": "Optional semantic text; auto-built from other fields.",
 
33
  "function": {
34
  "name": "search_trips",
35
  "description": (
36
+ "Search active trips by departure/destination/driver name (any can be omitted). "
37
  "Results sent as WhatsApp cards automatically. "
38
  "Times Asia/Aden. Buckets: صباح before 12, ظهر 12-17:59, ليل 18+."
39
  ),
 
74
  "type": "string",
75
  "description": "Optional car type in Arabic, for example سيارة or باص.",
76
  },
77
+ "driver_name": {
78
+ "type": "string",
79
+ "description": "Optional driver name to filter trips by driver.",
80
+ },
81
  "vector_query_text": {
82
  "type": "string",
83
  "description": "Optional semantic text; auto-built from other fields.",
app/database/supabase.py CHANGED
@@ -354,6 +354,7 @@ class SupabaseRepository:
354
  *,
355
  departure: str | None = None,
356
  destination: str | None = None,
 
357
  seats: int | None = None,
358
  vehicle_type: str | None = None,
359
  departure_request: DepartureRequest | None = None,
@@ -370,6 +371,11 @@ class SupabaseRepository:
370
  query = query.ilike("departure", f"%{departure}%")
371
  if destination:
372
  query = query.ilike("destination", f"%{destination}%")
 
 
 
 
 
373
  if seats:
374
  query = query.gte("available_seats", seats)
375
  if vehicle_type:
@@ -407,6 +413,7 @@ class SupabaseRepository:
407
  query_embedding: list[float],
408
  departure: str | None = None,
409
  destination: str | None = None,
 
410
  departure_date: date | None = None,
411
  departure_time: str | None = None,
412
  requested_time: time | None = None,
@@ -423,6 +430,7 @@ class SupabaseRepository:
423
  "match_threshold": 0.0,
424
  "filter_departure": departure,
425
  "filter_destination": destination,
 
426
  "filter_departure_date": (
427
  departure_date.isoformat() if departure_date else None
428
  ),
@@ -468,6 +476,24 @@ class SupabaseRepository:
468
  .execute()
469
  )
470
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
471
  def _apply_departure_request_filter(
472
  self,
473
  query: Any,
 
354
  *,
355
  departure: str | None = None,
356
  destination: str | None = None,
357
+ driver_name: str | None = None,
358
  seats: int | None = None,
359
  vehicle_type: str | None = None,
360
  departure_request: DepartureRequest | None = None,
 
371
  query = query.ilike("departure", f"%{departure}%")
372
  if destination:
373
  query = query.ilike("destination", f"%{destination}%")
374
+ if driver_name:
375
+ driver_ids = await self._resolve_driver_ids_by_name(driver_name)
376
+ if not driver_ids:
377
+ return []
378
+ query = query.in_("driver_id", driver_ids)
379
  if seats:
380
  query = query.gte("available_seats", seats)
381
  if vehicle_type:
 
413
  query_embedding: list[float],
414
  departure: str | None = None,
415
  destination: str | None = None,
416
+ driver_name: str | None = None,
417
  departure_date: date | None = None,
418
  departure_time: str | None = None,
419
  requested_time: time | None = None,
 
430
  "match_threshold": 0.0,
431
  "filter_departure": departure,
432
  "filter_destination": destination,
433
+ "filter_driver_name": driver_name,
434
  "filter_departure_date": (
435
  departure_date.isoformat() if departure_date else None
436
  ),
 
476
  .execute()
477
  )
478
 
479
+ async def _resolve_driver_ids_by_name(self, driver_name: str) -> list[str]:
480
+ customers_resp = (
481
+ await self.client.table("customers")
482
+ .select("id")
483
+ .ilike("name", f"%{driver_name}%")
484
+ .execute()
485
+ )
486
+ customer_ids = [c["id"] for c in (_response_data(customers_resp) or [])]
487
+ if not customer_ids:
488
+ return []
489
+ drivers_resp = (
490
+ await self.client.table("drivers")
491
+ .select("id")
492
+ .in_("customer_id", customer_ids)
493
+ .execute()
494
+ )
495
+ return [d["id"] for d in (_response_data(drivers_resp) or [])]
496
+
497
  def _apply_departure_request_filter(
498
  self,
499
  query: Any,
app/tools/handlers.py CHANGED
@@ -96,6 +96,7 @@ class FalsaToolHandlers:
96
  async def search_trips(self, arguments: dict[str, Any]) -> ToolResult:
97
  departure = _optional_string(arguments.get("departure"))
98
  destination = _optional_string(arguments.get("destination"))
 
99
  travel_date = _optional_string(arguments.get("travel_date"))
100
  travel_time = _optional_string(arguments.get("travel_time"))
101
  travel_time_exact = _optional_string(arguments.get("travel_time_exact"))
@@ -114,16 +115,20 @@ class FalsaToolHandlers:
114
  exact_time=travel_time_exact,
115
  )
116
 
117
- if not departure and not destination:
118
  return ToolResult(
119
  ok=False,
120
  data={},
121
- error="At least departure or destination is required before searching trips",
 
 
 
122
  )
123
 
124
  query = vector_query_text or _trip_vector_query_text(
125
  departure=departure,
126
  destination=destination,
 
127
  travel_date=travel_date,
128
  travel_time=travel_time,
129
  travel_time_exact=travel_time_exact,
@@ -136,6 +141,7 @@ class FalsaToolHandlers:
136
  query_embedding=query_embedding,
137
  departure=departure,
138
  destination=destination,
 
139
  departure_date=departure_request.departure_date,
140
  departure_time=departure_request.departure_time,
141
  requested_time=requested_time,
@@ -147,6 +153,7 @@ class FalsaToolHandlers:
147
  trips = await self.repository.search_active_trips(
148
  departure=departure,
149
  destination=destination,
 
150
  seats=seats,
151
  vehicle_type=vehicle_type,
152
  departure_request=departure_request,
@@ -160,6 +167,7 @@ class FalsaToolHandlers:
160
  trip,
161
  departure=departure,
162
  destination=destination,
 
163
  seats=seats,
164
  vehicle_type=vehicle_type,
165
  departure_request=departure_request,
@@ -911,6 +919,7 @@ def _trip_vector_query_text(
911
  *,
912
  departure: str | None,
913
  destination: str | None,
 
914
  travel_date: str | None,
915
  travel_time: str | None,
916
  travel_time_exact: str | None,
@@ -923,6 +932,7 @@ def _trip_vector_query_text(
923
  for part in [
924
  departure,
925
  destination,
 
926
  travel_date,
927
  travel_time_exact,
928
  travel_time,
@@ -939,6 +949,7 @@ def _is_trip_match(
939
  *,
940
  departure: str | None,
941
  destination: str | None,
 
942
  seats: int,
943
  vehicle_type: str | None,
944
  departure_request: Any,
@@ -951,6 +962,10 @@ def _is_trip_match(
951
  return False
952
  if destination and destination.lower() not in str(trip.get("destination") or "").lower():
953
  return False
 
 
 
 
954
  if vehicle_type:
955
  car = _first_or_dict(trip.get("driver_cars")) or {}
956
  car_type = car.get("car_type") or trip.get("car_type")
 
96
  async def search_trips(self, arguments: dict[str, Any]) -> ToolResult:
97
  departure = _optional_string(arguments.get("departure"))
98
  destination = _optional_string(arguments.get("destination"))
99
+ driver_name = _optional_string(arguments.get("driver_name"))
100
  travel_date = _optional_string(arguments.get("travel_date"))
101
  travel_time = _optional_string(arguments.get("travel_time"))
102
  travel_time_exact = _optional_string(arguments.get("travel_time_exact"))
 
115
  exact_time=travel_time_exact,
116
  )
117
 
118
+ if not departure and not destination and not driver_name:
119
  return ToolResult(
120
  ok=False,
121
  data={},
122
+ error=(
123
+ "At least departure, destination, or driver name"
124
+ " is required before searching trips"
125
+ ),
126
  )
127
 
128
  query = vector_query_text or _trip_vector_query_text(
129
  departure=departure,
130
  destination=destination,
131
+ driver_name=driver_name,
132
  travel_date=travel_date,
133
  travel_time=travel_time,
134
  travel_time_exact=travel_time_exact,
 
141
  query_embedding=query_embedding,
142
  departure=departure,
143
  destination=destination,
144
+ driver_name=driver_name,
145
  departure_date=departure_request.departure_date,
146
  departure_time=departure_request.departure_time,
147
  requested_time=requested_time,
 
153
  trips = await self.repository.search_active_trips(
154
  departure=departure,
155
  destination=destination,
156
+ driver_name=driver_name,
157
  seats=seats,
158
  vehicle_type=vehicle_type,
159
  departure_request=departure_request,
 
167
  trip,
168
  departure=departure,
169
  destination=destination,
170
+ driver_name=driver_name,
171
  seats=seats,
172
  vehicle_type=vehicle_type,
173
  departure_request=departure_request,
 
919
  *,
920
  departure: str | None,
921
  destination: str | None,
922
+ driver_name: str | None,
923
  travel_date: str | None,
924
  travel_time: str | None,
925
  travel_time_exact: str | None,
 
932
  for part in [
933
  departure,
934
  destination,
935
+ driver_name,
936
  travel_date,
937
  travel_time_exact,
938
  travel_time,
 
949
  *,
950
  departure: str | None,
951
  destination: str | None,
952
+ driver_name: str | None,
953
  seats: int,
954
  vehicle_type: str | None,
955
  departure_request: Any,
 
962
  return False
963
  if destination and destination.lower() not in str(trip.get("destination") or "").lower():
964
  return False
965
+ if driver_name:
966
+ trip_driver_name = str(trip.get("driver_name") or "").lower()
967
+ if driver_name.lower() not in trip_driver_name:
968
+ return False
969
  if vehicle_type:
970
  car = _first_or_dict(trip.get("driver_cars")) or {}
971
  car_type = car.get("car_type") or trip.get("car_type")
prompts/falsa_info.md CHANGED
@@ -15,7 +15,7 @@
15
  المستخدم الجديد: يسأله النظام هل يريد السفر (مسافر) أو العمل كسائق. المسافر: يبحث عن رحلات ويختار ويتواصل مع السائق. السائق: ينشر رحلات ويدير جدوله ويسجل مركباته ويستلم إشعارات الاهتمام بالرحلات.
16
 
17
  ## البحث عن الرحلات
18
- تقدر تبحث برسالة بسيطة حتى لو كانت المعلومات ناقصة. مثال: "من صنعاء إلى تعز بكرة". البحث ذكي — يفهم المعنى وليس الكلمات المطابقة فقط. إذا ما لقى نتائج بالضبط، يبحث في كل الرحلات النشطة. أوقات الرحلات تصنف إلى ثلاث فترات: صباح (قبل 12 ظهراً)، ظهر (من 12 ظهراً إلى 6 مساءً)، ليل (بعد 6 مساءً). أنواع المركبات: سيارة، باص، أو أي نوع يسجله السائق.
19
 
20
  ## اختيار الرحلة والتواصل مع السائق
21
  عندما تختار رحلة بالرد على بطاقة الرحلة: يتم تسجيل اهتمامك (ليس حجزاً مؤكداً)، السائق يستلم إشعار فيه اسمك ورقمك وعدد المقاعد المطلوبة، أنت تستلم رقم السائق، تتواصلون مباشرة للاتفاق على الحجز والسعر. المقاعد لا تحجز. قد يتواصل معك أكثر من شخص مهتم بنفس الرحلة.
@@ -41,6 +41,7 @@
41
  س: وش معنى المقاعد المتاحة؟ ج: عدد المقاعد الفارغة في الرحلة. تقدر تطلب أي عدد لكن لا تحجز — السائق يؤكد بعد التواصل.
42
  س: هل أقدر ألغي اختياري؟ ج: اختيار الرحلة هو إرسال اهتمام فقط ولا يوجد إلغاء من النظام. إذا تغير رأيك، تواصل مع السائق مباشرة.
43
  س: كيف أسجل كسائق؟ ج: قول "أبي أسجل كسائق" أو اختر وضع السائق. يحتاج الاسم الكامل.
 
44
  س: كيف أضيف رحلة كسائق؟ ج: قول "أبي أضيف رحلة من عدن إلى الحديدة الجمعة الصباح". فلزا تطلب التفاصيل الناقصة.
45
  س: كيف أعدل أو ألغي رحلة؟ ج: قول "أبي أعدل رحلة" أو "أبي ألغي رحلة". فلزا ترسل لك قائمة رحلاتك — ترد على اللي تبيه.
46
  س: هل فيه رحلات بين كل المدن؟ ج: فلزا تعرض الرحلات اللي ينشرها السائقين. إذا ما لقيت رحلتك، اسأل مرة ثانية أو تواصل مع الدعم.
 
15
  المستخدم الجديد: يسأله النظام هل يريد السفر (مسافر) أو العمل كسائق. المسافر: يبحث عن رحلات ويختار ويتواصل مع السائق. السائق: ينشر رحلات ويدير جدوله ويسجل مركباته ويستلم إشعارات الاهتمام بالرحلات.
16
 
17
  ## البحث عن الرحلات
18
+ تقدر تبحث برسالة بسيطة حتى لو كانت المعلومات ناقصة. مثال: "من صنعاء إلى تعز بكرة". تقدر أيضاً تبحث باسم السائق، مثال: "أبي رحلات السائق أحمد". البحث ذكي — يفهم المعنى وليس الكلمات المطابقة فقط. إذا ما لقى نتائج بالضبط، يبحث في كل الرحلات النشطة. أوقات الرحلات تصنف إلى ثلاث فترات: صباح (قبل 12 ظهراً)، ظهر (من 12 ظهراً إلى 6 مساءً)، ليل (بعد 6 مساءً). أنواع المركبات: سيارة، باص، أو أي نوع يسجله السائق.
19
 
20
  ## اختيار الرحلة والتواصل مع السائق
21
  عندما تختار رحلة بالرد على بطاقة الرحلة: يتم تسجيل اهتمامك (ليس حجزاً مؤكداً)، السائق يستلم إشعار فيه اسمك ورقمك وعدد المقاعد المطلوبة، أنت تستلم رقم السائق، تتواصلون مباشرة للاتفاق على الحجز والسعر. المقاعد لا تحجز. قد يتواصل معك أكثر من شخص مهتم بنفس الرحلة.
 
41
  س: وش معنى المقاعد المتاحة؟ ج: عدد المقاعد الفارغة في الرحلة. تقدر تطلب أي عدد لكن لا تحجز — السائق يؤكد بعد التواصل.
42
  س: هل أقدر ألغي اختياري؟ ج: اختيار الرحلة هو إرسال اهتمام فقط ولا يوجد إلغاء من النظام. إذا تغير رأيك، تواصل مع السائق مباشرة.
43
  س: كيف أسجل كسائق؟ ج: قول "أبي أسجل كسائق" أو اختر وضع السائق. يحتاج الاسم الكامل.
44
+ س: كيف أبحث عن رحلة لسائق معين؟ ج: قول "أبي رحلات السائق أحمد" أو "دور رحلة مع السائق". فلزا تبحث في الرحلات المتاحة لهالسائق وترسل لك النتائج.
45
  س: كيف أضيف رحلة كسائق؟ ج: قول "أبي أضيف رحلة من عدن إلى الحديدة الجمعة الصباح". فلزا تطلب التفاصيل الناقصة.
46
  س: كيف أعدل أو ألغي رحلة؟ ج: قول "أبي أعدل رحلة" أو "أبي ألغي رحلة". فلزا ترسل لك قائمة رحلاتك — ترد على اللي تبيه.
47
  س: هل فيه رحلات بين كل المدن؟ ج: فلزا تعرض الرحلات اللي ينشرها السائقين. إذا ما لقيت رحلتك، اسأل مرة ثانية أو تواصل مع الدعم.
prompts/system_passenger.md CHANGED
@@ -1,4 +1,4 @@
1
- - search_trips as soon as the user mentions a departure or destination. You do not need all details — the tool accepts partial info. Only ask a follow-up if neither departure nor destination was mentioned.
2
  - When search_trips finds matches, trip cards are sent to the user automatically along with a prompt to choose one. Do NOT add any text after calling search_trips — the tool will handle the response.
3
  - When the user replies to a trip card, immediately call select_trip — trip_id is auto-detected and seats default to 1. Do not ask for seat count or other details.
4
  - Selections are not reservations — seats are not held.
 
1
+ - search_trips as soon as the user mentions a departure, destination, or driver name. You do not need all details — the tool accepts partial info. Only ask a follow-up if neither departure, destination, nor driver name was mentioned.
2
  - When search_trips finds matches, trip cards are sent to the user automatically along with a prompt to choose one. Do NOT add any text after calling search_trips — the tool will handle the response.
3
  - When the user replies to a trip card, immediately call select_trip — trip_id is auto-detected and seats default to 1. Do not ask for seat count or other details.
4
  - Selections are not reservations — seats are not held.
supabase/migrations/202607130001_driver_name_search.sql ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -- Add driver name filtering to match_active_trips RPC
2
+
3
+ DROP FUNCTION IF EXISTS public.match_active_trips(
4
+ extensions.vector(1024), float, int, text, text, date, text, time, int, text
5
+ );
6
+
7
+ CREATE OR REPLACE FUNCTION public.match_active_trips(
8
+ query_embedding extensions.vector(1024),
9
+ match_threshold float DEFAULT 0.0,
10
+ match_count int DEFAULT 10,
11
+ filter_departure text DEFAULT NULL,
12
+ filter_destination text DEFAULT NULL,
13
+ filter_driver_name text DEFAULT NULL,
14
+ filter_departure_date date DEFAULT NULL,
15
+ filter_departure_time text DEFAULT NULL,
16
+ filter_requested_time time DEFAULT NULL,
17
+ filter_seats int DEFAULT 1,
18
+ filter_vehicle_type text DEFAULT NULL
19
+ )
20
+ RETURNS TABLE (
21
+ trip_id uuid,
22
+ departure text,
23
+ destination text,
24
+ departure_date date,
25
+ departure_time text,
26
+ available_seats integer,
27
+ total_seats integer,
28
+ price numeric,
29
+ status text,
30
+ driver_name text,
31
+ driver_phone_number text,
32
+ car_type text,
33
+ chunk_text text,
34
+ similarity float,
35
+ time_difference_minutes integer,
36
+ registered boolean
37
+ )
38
+ LANGUAGE sql STABLE
39
+ AS $$
40
+ WITH ranked AS (
41
+ SELECT
42
+ driver_trips.id AS trip_id,
43
+ driver_trips.departure,
44
+ driver_trips.destination,
45
+ driver_trips.departure_date,
46
+ driver_trips.departure_time,
47
+ driver_trips.available_seats,
48
+ driver_trips.total_seats,
49
+ driver_trips.price,
50
+ driver_trips.status,
51
+ customers.name AS driver_name,
52
+ customers."remoteJid" AS driver_phone_number,
53
+ driver_cars.car_type,
54
+ driver_trip_embeddings.chunk_text,
55
+ COALESCE(customers.registered, false) AS registered,
56
+ 1 - (driver_trip_embeddings.embedding <=> query_embedding) AS similarity,
57
+ driver_trip_embeddings.embedding <=> query_embedding AS vector_distance,
58
+ CASE
59
+ WHEN filter_requested_time IS NULL THEN NULL
60
+ ELSE abs(
61
+ extract(epoch FROM (
62
+ public.departure_bucket_clock_time(driver_trips.departure_time)
63
+ - filter_requested_time
64
+ )) / 60
65
+ )::integer
66
+ END AS time_difference_minutes
67
+ FROM public.driver_trip_embeddings
68
+ JOIN public.driver_trips ON driver_trips.id = driver_trip_embeddings.trip_id
69
+ LEFT JOIN public.drivers ON drivers.id = driver_trips.driver_id
70
+ LEFT JOIN public.customers ON customers.id = drivers.customer_id
71
+ LEFT JOIN public.driver_cars ON driver_cars.id = driver_trips.car_id
72
+ WHERE driver_trips.status = 'active'
73
+ AND driver_trips.available_seats >= COALESCE(filter_seats, 1)
74
+ AND (filter_departure IS NULL OR driver_trips.departure ILIKE '%' || filter_departure || '%')
75
+ AND (filter_destination IS NULL OR driver_trips.destination ILIKE '%' || filter_destination || '%')
76
+ AND (filter_driver_name IS NULL OR customers.name ILIKE '%' || filter_driver_name || '%')
77
+ AND (filter_departure_date IS NULL OR driver_trips.departure_date = filter_departure_date)
78
+ AND (filter_departure_time IS NULL OR driver_trips.departure_time = filter_departure_time)
79
+ AND (filter_vehicle_type IS NULL OR driver_cars.car_type ILIKE '%' || filter_vehicle_type || '%')
80
+ AND (
81
+ driver_trips.departure_date > (NOW() AT TIME ZONE 'Asia/Aden')::date
82
+ OR (
83
+ driver_trips.departure_date = (NOW() AT TIME ZONE 'Asia/Aden')::date
84
+ AND (
85
+ (NOW() AT TIME ZONE 'Asia/Aden')::time < TIME '12:00'
86
+ OR (
87
+ (NOW() AT TIME ZONE 'Asia/Aden')::time < TIME '18:00'
88
+ AND driver_trips.departure_time IN ('noon', 'night')
89
+ )
90
+ OR (
91
+ (NOW() AT TIME ZONE 'Asia/Aden')::time >= TIME '18:00'
92
+ AND driver_trips.departure_time = 'night'
93
+ )
94
+ )
95
+ )
96
+ )
97
+ )
98
+ SELECT
99
+ ranked.trip_id,
100
+ ranked.departure,
101
+ ranked.destination,
102
+ ranked.departure_date,
103
+ ranked.departure_time,
104
+ ranked.available_seats,
105
+ ranked.total_seats,
106
+ ranked.price,
107
+ ranked.status,
108
+ ranked.driver_name,
109
+ ranked.driver_phone_number,
110
+ ranked.car_type,
111
+ ranked.chunk_text,
112
+ ranked.similarity,
113
+ ranked.time_difference_minutes,
114
+ ranked.registered
115
+ FROM ranked
116
+ WHERE ranked.similarity >= match_threshold
117
+ ORDER BY
118
+ ranked.registered DESC,
119
+ ranked.time_difference_minutes NULLS LAST,
120
+ ranked.departure_date,
121
+ ranked.vector_distance
122
+ LIMIT match_count;
123
+ $$;
tests/test_tools.py CHANGED
@@ -145,7 +145,8 @@ async def test_search_trips_requires_at_least_departure_or_destination():
145
  result = await handlers.search_trips({"seats": 2})
146
 
147
  assert result.ok is False
148
- assert "departure or destination" in (result.error or "")
 
149
 
150
 
151
  @pytest.mark.asyncio
 
145
  result = await handlers.search_trips({"seats": 2})
146
 
147
  assert result.ok is False
148
+ assert "departure" in (result.error or "")
149
+ assert "driver name" in (result.error or "")
150
 
151
 
152
  @pytest.mark.asyncio