add driver name search for passengers
Browse files- app/ai/tool_schemas.py +5 -1
- app/database/supabase.py +26 -0
- app/tools/handlers.py +17 -2
- prompts/falsa_info.md +2 -1
- prompts/system_passenger.md +1 -1
- supabase/migrations/202607130001_driver_name_search.sql +123 -0
- tests/test_tools.py +2 -1
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 (
|
| 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=
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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
|
|
|
|
| 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
|