codeBOKER commited on
Commit
cc10684
Β·
1 Parent(s): 0467de1

make trip search fields optional; update LLM prompts to search/book immediately with partial info

Browse files
app/ai/tool_schemas.py CHANGED
@@ -35,6 +35,8 @@ _SEARCH_TRIPS = {
35
  "description": (
36
  "Search active car or bus trips. "
37
  "Use when the customer asks for travel options. "
 
 
38
  "Each matching trip is sent as a separate WhatsApp message. "
39
  "The customer can reply to a trip card to select it."
40
  ),
@@ -43,11 +45,17 @@ _SEARCH_TRIPS = {
43
  "properties": {
44
  "departure": {
45
  "type": "string",
46
- "description": "Departure city or area in Arabic.",
 
 
 
47
  },
48
  "destination": {
49
  "type": "string",
50
- "description": "Destination city or area in Arabic.",
 
 
 
51
  },
52
  "travel_datetime": {
53
  "type": "string",
@@ -89,12 +97,12 @@ _SEARCH_TRIPS = {
89
  "vector_query_text": {
90
  "type": "string",
91
  "description": (
92
- "Natural-language semantic search text containing the route, date, "
93
- "time, seats, and vehicle preferences extracted from the customer."
94
  ),
95
  },
96
  },
97
- "required": ["departure", "destination", "vector_query_text"],
98
  "additionalProperties": False,
99
  },
100
  },
@@ -105,10 +113,10 @@ _CREATE_BOOKING_LEAD = {
105
  "function": {
106
  "name": "create_booking_lead",
107
  "description": (
108
- "Create a pending booking lead and notify the driver. "
109
- "This does not reserve seats or confirm payment. "
110
- "If the customer replied to a trip card, trip_id is resolved automatically β€” "
111
- "you can omit it and the system will detect which trip they meant."
112
  ),
113
  "parameters": {
114
  "type": "object",
 
35
  "description": (
36
  "Search active car or bus trips. "
37
  "Use when the customer asks for travel options. "
38
+ "You can search with only departure or only destination"
39
+ " β€” the other will match any location. "
40
  "Each matching trip is sent as a separate WhatsApp message. "
41
  "The customer can reply to a trip card to select it."
42
  ),
 
45
  "properties": {
46
  "departure": {
47
  "type": "string",
48
+ "description": (
49
+ "Departure city or area in Arabic. At least one of departure or"
50
+ " destination is required β€” the other can be omitted to match anywhere."
51
+ ),
52
  },
53
  "destination": {
54
  "type": "string",
55
+ "description": (
56
+ "Destination city or area in Arabic. At least one of departure or"
57
+ " destination is required β€” the other can be omitted to match anywhere."
58
+ ),
59
  },
60
  "travel_datetime": {
61
  "type": "string",
 
97
  "vector_query_text": {
98
  "type": "string",
99
  "description": (
100
+ "Optional natural-language semantic search text. "
101
+ "Automatically constructed from the other fields if not provided."
102
  ),
103
  },
104
  },
105
+ "required": [],
106
  "additionalProperties": False,
107
  },
108
  },
 
113
  "function": {
114
  "name": "create_booking_lead",
115
  "description": (
116
+ "Create a pending booking lead (1 seat by default) and notify the driver. "
117
+ "Call this immediately when the user replies to a trip card β€” trip_id is "
118
+ "auto-detected and requested_seats defaults to 1. "
119
+ "Does not reserve seats or confirm payment."
120
  ),
121
  "parameters": {
122
  "type": "object",
app/database/supabase.py CHANGED
@@ -289,8 +289,8 @@ class SupabaseRepository:
289
  self,
290
  *,
291
  query_embedding: list[float],
292
- departure: str,
293
- destination: str,
294
  departure_date: date | None = None,
295
  departure_time: str | None = None,
296
  requested_time: time | None = None,
 
289
  self,
290
  *,
291
  query_embedding: list[float],
292
+ departure: str | None = None,
293
+ destination: str | None = None,
294
  departure_date: date | None = None,
295
  departure_time: str | None = None,
296
  requested_time: time | None = None,
app/tools/handlers.py CHANGED
@@ -109,11 +109,11 @@ class FalsaToolHandlers:
109
  exact_time=travel_time_exact,
110
  )
111
 
112
- if not departure or not destination:
113
  return ToolResult(
114
  ok=False,
115
  data={},
116
- error="departure and destination are required before searching trips",
117
  )
118
 
119
  query = vector_query_text or _trip_vector_query_text(
@@ -186,6 +186,7 @@ class FalsaToolHandlers:
186
  ok=True,
187
  data={
188
  "count": len(top_trips),
 
189
  "alternate_alert": alternate_alert,
190
  "sent_as_messages": bool(top_trips),
191
  "note": (
@@ -949,8 +950,8 @@ def _resolve_driver_car(
949
 
950
  def _trip_vector_query_text(
951
  *,
952
- departure: str,
953
- destination: str,
954
  travel_date: str | None,
955
  travel_time: str | None,
956
  travel_time_exact: str | None,
@@ -977,8 +978,8 @@ def _trip_vector_query_text(
977
  def _is_trip_match(
978
  trip: dict[str, Any],
979
  *,
980
- departure: str,
981
- destination: str,
982
  seats: int,
983
  vehicle_type: str | None,
984
  departure_request: Any,
@@ -987,9 +988,9 @@ def _is_trip_match(
987
  return False
988
  if int(trip.get("available_seats") or 0) < seats:
989
  return False
990
- if departure.lower() not in str(trip.get("departure") or "").lower():
991
  return False
992
- if destination.lower() not in str(trip.get("destination") or "").lower():
993
  return False
994
  if vehicle_type:
995
  car = _first_or_dict(trip.get("driver_cars")) or {}
 
109
  exact_time=travel_time_exact,
110
  )
111
 
112
+ if not departure and not destination:
113
  return ToolResult(
114
  ok=False,
115
  data={},
116
+ error="At least departure or destination is required before searching trips",
117
  )
118
 
119
  query = vector_query_text or _trip_vector_query_text(
 
186
  ok=True,
187
  data={
188
  "count": len(top_trips),
189
+ "matches": top_trips,
190
  "alternate_alert": alternate_alert,
191
  "sent_as_messages": bool(top_trips),
192
  "note": (
 
950
 
951
  def _trip_vector_query_text(
952
  *,
953
+ departure: str | None,
954
+ destination: str | None,
955
  travel_date: str | None,
956
  travel_time: str | None,
957
  travel_time_exact: str | None,
 
978
  def _is_trip_match(
979
  trip: dict[str, Any],
980
  *,
981
+ departure: str | None,
982
+ destination: str | None,
983
  seats: int,
984
  vehicle_type: str | None,
985
  departure_request: Any,
 
988
  return False
989
  if int(trip.get("available_seats") or 0) < seats:
990
  return False
991
+ if departure and departure.lower() not in str(trip.get("departure") or "").lower():
992
  return False
993
+ if destination and destination.lower() not in str(trip.get("destination") or "").lower():
994
  return False
995
  if vehicle_type:
996
  car = _first_or_dict(trip.get("driver_cars")) or {}
prompts/system_passenger.md CHANGED
@@ -1,7 +1,6 @@
1
- - search_trips for travel options. Include vector_query_text (natural-language search phrase).
2
  - When search_trips returns matches, trips are sent as separate WhatsApp messages. The user can reply to a trip card to select it β€” the system will detect which trip they meant automatically, without needing a trip_id or short_id.
3
- - create_booking_lead only after trip + seat count selected. If the user replied to a trip card, trip_id is resolved automatically and you can omit it.
4
- - Ask short follow-up if details missing.
5
  - Booking leads are pending β€” seats not reserved.
6
  - After successful create_booking_lead, tell the passenger the driver's phone number from driver_phone so they can contact the driver directly.
7
  - To drive: switch_to_driver. If no account: name -> create_driver_account -> switch_to_driver. Never before account exists.
 
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 returns matches, trips are sent as separate WhatsApp messages. The user can reply to a trip card to select it β€” the system will detect which trip they meant automatically, without needing a trip_id or short_id.
3
+ - When the user replies to a trip card, immediately call create_booking_lead β€” trip_id is auto-detected and seats default to 1. Do not ask for seat count or other details.
 
4
  - Booking leads are pending β€” seats not reserved.
5
  - After successful create_booking_lead, tell the passenger the driver's phone number from driver_phone so they can contact the driver directly.
6
  - To drive: switch_to_driver. If no account: name -> create_driver_account -> switch_to_driver. Never before account exists.
tests/test_tools.py CHANGED
@@ -127,6 +127,69 @@ async def test_search_trips_reports_no_matches():
127
  assert "No active" in result.data["note"]
128
 
129
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
  @pytest.mark.asyncio
131
  async def test_search_trips_includes_alternate_alert_when_first_result_is_over_one_hour():
132
  repository = FakeRepository()
 
127
  assert "No active" in result.data["note"]
128
 
129
 
130
+ @pytest.mark.asyncio
131
+ async def test_search_trips_requires_at_least_departure_or_destination():
132
+ handlers = make_handlers()
133
+
134
+ result = await handlers.search_trips({"seats": 2})
135
+
136
+ assert result.ok is False
137
+ assert "departure or destination" in (result.error or "")
138
+
139
+
140
+ @pytest.mark.asyncio
141
+ async def test_search_trips_with_departure_only():
142
+ repository = FakeRepository()
143
+ repository.trip_vector_search_results = [
144
+ trip(trip_id="trip-1", departure="Aden", destination="Mukalla"),
145
+ trip(trip_id="trip-2", departure="Aden", destination="Taiz"),
146
+ ]
147
+ embeddings = FakeEmbeddings()
148
+ handlers = make_handlers(repository=repository, embeddings=embeddings)
149
+
150
+ result = await handlers.search_trips({"departure": "Aden"})
151
+
152
+ assert result.ok is True
153
+ assert result.data["count"] == 2
154
+ assert result.data["matches"][0]["trip_id"] == "trip-1"
155
+ assert result.data["matches"][1]["trip_id"] == "trip-2"
156
+ assert repository.vector_trip_search_calls[0]["departure"] == "Aden"
157
+ assert repository.vector_trip_search_calls[0]["destination"] is None
158
+
159
+
160
+ @pytest.mark.asyncio
161
+ async def test_search_trips_with_destination_only():
162
+ repository = FakeRepository()
163
+ repository.trip_vector_search_results = [
164
+ trip(trip_id="trip-1", departure="Aden", destination="Mukalla"),
165
+ trip(trip_id="trip-2", departure="Taiz", destination="Mukalla"),
166
+ ]
167
+ embeddings = FakeEmbeddings()
168
+ handlers = make_handlers(repository=repository, embeddings=embeddings)
169
+
170
+ result = await handlers.search_trips({"destination": "Mukalla"})
171
+
172
+ assert result.ok is True
173
+ assert result.data["count"] == 2
174
+ assert repository.vector_trip_search_calls[0]["departure"] is None
175
+ assert repository.vector_trip_search_calls[0]["destination"] == "Mukalla"
176
+
177
+
178
+ @pytest.mark.asyncio
179
+ async def test_search_trips_defaults_seats_to_one():
180
+ repository = FakeRepository()
181
+ repository.trip_vector_search_results = [
182
+ trip(trip_id="trip-1", available_seats=1),
183
+ ]
184
+ handlers = make_handlers(repository=repository)
185
+
186
+ result = await handlers.search_trips({"departure": "Aden", "destination": "Mukalla"})
187
+
188
+ assert result.ok is True
189
+ assert result.data["count"] == 1
190
+ assert repository.vector_trip_search_calls[0]["seats"] == 1
191
+
192
+
193
  @pytest.mark.asyncio
194
  async def test_search_trips_includes_alternate_alert_when_first_result_is_over_one_hour():
195
  repository = FakeRepository()