replace numbered trip list with trip cards for driver delete/modify/list
Browse files- initiate_trip_action and check_driver_trips now send format_trip_card
per trip (like passenger search) instead of a numbered text list
- Driver replies to a trip card to delete (bypass LLM) or modify
(set session, route to LLM for update_trip_field)
- Remove delete_trip_by_number / modify_trip_by_number handlers
and their tool schemas (replaced by card reply flow)
- Remove dead interactive list code (_handle_trip_interactive_reply)
- Clean up trip_selection.py: remove unused builders/parsers
- Update list_driver_trips query to include drivers(*)
- app/ai/tool_schemas.py +2 -65
- app/database/supabase.py +1 -1
- app/services/conversation_service.py +64 -100
- app/tools/handlers.py +74 -112
- app/whatsapp/trip_selection.py +0 -81
- tests/conftest.py +33 -7
- tests/test_conversation_service.py +8 -104
- tests/test_tool_schemas.py +1 -1
- tests/test_tools.py +16 -41
- tests/test_trip_selection.py +25 -26
- tests/test_webhooks.py +1 -1
app/ai/tool_schemas.py
CHANGED
|
@@ -151,7 +151,7 @@ _CHECK_DRIVER_TRIPS = {
|
|
| 151 |
"type": "function",
|
| 152 |
"function": {
|
| 153 |
"name": "check_driver_trips",
|
| 154 |
-
"description": "List upcoming active trips
|
| 155 |
"parameters": {
|
| 156 |
"type": "object",
|
| 157 |
"properties": {},
|
|
@@ -246,7 +246,7 @@ _INITIATE_TRIP_ACTION = {
|
|
| 246 |
"type": "function",
|
| 247 |
"function": {
|
| 248 |
"name": "initiate_trip_action",
|
| 249 |
-
"description": "Start delete/modify flow. Sends
|
| 250 |
"parameters": {
|
| 251 |
"type": "object",
|
| 252 |
"properties": {
|
|
@@ -305,65 +305,6 @@ _UPDATE_TRIP_FIELD = {
|
|
| 305 |
},
|
| 306 |
}
|
| 307 |
|
| 308 |
-
_DELETE_TRIP_BY_NUMBER = {
|
| 309 |
-
"type": "function",
|
| 310 |
-
"function": {
|
| 311 |
-
"name": "delete_trip_by_number",
|
| 312 |
-
"description": "Cancel a trip by driver-visible number (oldest=1).",
|
| 313 |
-
"parameters": {
|
| 314 |
-
"type": "object",
|
| 315 |
-
"properties": {
|
| 316 |
-
"trip_number": {
|
| 317 |
-
"type": "integer",
|
| 318 |
-
"minimum": 1,
|
| 319 |
-
"description": "The one-based number of the trip from the driver trip list.",
|
| 320 |
-
},
|
| 321 |
-
},
|
| 322 |
-
"required": ["trip_number"],
|
| 323 |
-
"additionalProperties": False,
|
| 324 |
-
},
|
| 325 |
-
},
|
| 326 |
-
}
|
| 327 |
-
|
| 328 |
-
_MODIFY_TRIP_BY_NUMBER = {
|
| 329 |
-
"type": "function",
|
| 330 |
-
"function": {
|
| 331 |
-
"name": "modify_trip_by_number",
|
| 332 |
-
"description": "Modify a trip field by driver-visible number (oldest=1).",
|
| 333 |
-
"parameters": {
|
| 334 |
-
"type": "object",
|
| 335 |
-
"properties": {
|
| 336 |
-
"trip_number": {
|
| 337 |
-
"type": "integer",
|
| 338 |
-
"minimum": 1,
|
| 339 |
-
"description": "The one-based number of the trip from the driver trip list.",
|
| 340 |
-
},
|
| 341 |
-
"field": {
|
| 342 |
-
"type": "string",
|
| 343 |
-
"enum": [
|
| 344 |
-
"departure",
|
| 345 |
-
"destination",
|
| 346 |
-
"departure_date",
|
| 347 |
-
"departure_time",
|
| 348 |
-
"pickup_time",
|
| 349 |
-
"vehicle_type",
|
| 350 |
-
"available_seats",
|
| 351 |
-
"total_seats",
|
| 352 |
-
"price",
|
| 353 |
-
],
|
| 354 |
-
"description": "The trip field to update.",
|
| 355 |
-
},
|
| 356 |
-
"value": {
|
| 357 |
-
"type": "string",
|
| 358 |
-
"description": "New value. Dates YYYY-MM-DD, time HH:MM, routes Arabic, seats/price digits.",
|
| 359 |
-
},
|
| 360 |
-
},
|
| 361 |
-
"required": ["trip_number", "field", "value"],
|
| 362 |
-
"additionalProperties": False,
|
| 363 |
-
},
|
| 364 |
-
},
|
| 365 |
-
}
|
| 366 |
-
|
| 367 |
_SWITCH_TO_DRIVER = {
|
| 368 |
"type": "function",
|
| 369 |
"function": {
|
|
@@ -408,8 +349,6 @@ _TOOL_SCHEMAS: dict[str, dict[str, Any]] = {
|
|
| 408 |
"add_trip_by_driver": _ADD_TRIP_BY_DRIVER,
|
| 409 |
"initiate_trip_action": _INITIATE_TRIP_ACTION,
|
| 410 |
"update_trip_field": _UPDATE_TRIP_FIELD,
|
| 411 |
-
"delete_trip_by_number": _DELETE_TRIP_BY_NUMBER,
|
| 412 |
-
"modify_trip_by_number": _MODIFY_TRIP_BY_NUMBER,
|
| 413 |
"switch_to_driver": _SWITCH_TO_DRIVER,
|
| 414 |
"switch_to_passenger": _SWITCH_TO_PASSENGER,
|
| 415 |
}
|
|
@@ -427,8 +366,6 @@ _TOOLS_BY_MODE: dict[UserMode, list[str]] = {
|
|
| 427 |
"check_driver_trips",
|
| 428 |
"add_driver_car",
|
| 429 |
"add_trip_by_driver",
|
| 430 |
-
"delete_trip_by_number",
|
| 431 |
-
"modify_trip_by_number",
|
| 432 |
"initiate_trip_action",
|
| 433 |
"update_trip_field",
|
| 434 |
"switch_to_passenger",
|
|
|
|
| 151 |
"type": "function",
|
| 152 |
"function": {
|
| 153 |
"name": "check_driver_trips",
|
| 154 |
+
"description": "List upcoming active trips. Sends trip cards directly.",
|
| 155 |
"parameters": {
|
| 156 |
"type": "object",
|
| 157 |
"properties": {},
|
|
|
|
| 246 |
"type": "function",
|
| 247 |
"function": {
|
| 248 |
"name": "initiate_trip_action",
|
| 249 |
+
"description": "Start delete/modify flow. Sends trip cards — driver replies to a card to pick the trip.",
|
| 250 |
"parameters": {
|
| 251 |
"type": "object",
|
| 252 |
"properties": {
|
|
|
|
| 305 |
},
|
| 306 |
}
|
| 307 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 308 |
_SWITCH_TO_DRIVER = {
|
| 309 |
"type": "function",
|
| 310 |
"function": {
|
|
|
|
| 349 |
"add_trip_by_driver": _ADD_TRIP_BY_DRIVER,
|
| 350 |
"initiate_trip_action": _INITIATE_TRIP_ACTION,
|
| 351 |
"update_trip_field": _UPDATE_TRIP_FIELD,
|
|
|
|
|
|
|
| 352 |
"switch_to_driver": _SWITCH_TO_DRIVER,
|
| 353 |
"switch_to_passenger": _SWITCH_TO_PASSENGER,
|
| 354 |
}
|
|
|
|
| 366 |
"check_driver_trips",
|
| 367 |
"add_driver_car",
|
| 368 |
"add_trip_by_driver",
|
|
|
|
|
|
|
| 369 |
"initiate_trip_action",
|
| 370 |
"update_trip_field",
|
| 371 |
"switch_to_passenger",
|
app/database/supabase.py
CHANGED
|
@@ -468,7 +468,7 @@ class SupabaseRepository:
|
|
| 468 |
async def list_driver_trips(self, driver_id: str) -> list[dict[str, Any]]:
|
| 469 |
query = (
|
| 470 |
self.client.table("driver_trips")
|
| 471 |
-
.select("*, driver_cars(*)")
|
| 472 |
.eq("driver_id", driver_id)
|
| 473 |
.eq("status", "active")
|
| 474 |
)
|
|
|
|
| 468 |
async def list_driver_trips(self, driver_id: str) -> list[dict[str, Any]]:
|
| 469 |
query = (
|
| 470 |
self.client.table("driver_trips")
|
| 471 |
+
.select("*, driver_cars(*), drivers(*)")
|
| 472 |
.eq("driver_id", driver_id)
|
| 473 |
.eq("status", "active")
|
| 474 |
)
|
app/services/conversation_service.py
CHANGED
|
@@ -13,7 +13,7 @@ from app.tools.handlers import FalsaToolHandlers, _trip_summary
|
|
| 13 |
from app.tools.registry import ToolRegistry
|
| 14 |
from app.utils.time import now_in_timezone
|
| 15 |
from app.whatsapp.client import WhatsAppClient
|
| 16 |
-
|
| 17 |
|
| 18 |
logger = logging.getLogger(__name__)
|
| 19 |
|
|
@@ -121,6 +121,69 @@ class ConversationService:
|
|
| 121 |
)
|
| 122 |
return reply
|
| 123 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
reply = await self.ai.generate_reply(
|
| 125 |
messages=self._ai_messages(context, user_mode=user_mode),
|
| 126 |
tools=get_tool_schemas(user_mode),
|
|
@@ -141,105 +204,6 @@ class ConversationService:
|
|
| 141 |
|
| 142 |
return reply
|
| 143 |
|
| 144 |
-
async def _handle_trip_interactive_reply(
|
| 145 |
-
self,
|
| 146 |
-
inbound: WhatsAppInboundMessage,
|
| 147 |
-
customer: dict[str, Any],
|
| 148 |
-
*,
|
| 149 |
-
action: str,
|
| 150 |
-
trip_id: str,
|
| 151 |
-
) -> str | None:
|
| 152 |
-
current_message = await self.repository.create_message(
|
| 153 |
-
customer_id=str(customer["id"]),
|
| 154 |
-
sender_type="customer",
|
| 155 |
-
message=inbound.text,
|
| 156 |
-
whatsapp_message_id=inbound.message_id,
|
| 157 |
-
metadata={
|
| 158 |
-
"whatsapp": inbound.raw,
|
| 159 |
-
"timestamp": inbound.timestamp,
|
| 160 |
-
"interactive_reply_id": inbound.interactive_reply_id,
|
| 161 |
-
},
|
| 162 |
-
)
|
| 163 |
-
|
| 164 |
-
driver = await self.repository.get_driver_by_remoteJid(inbound.remoteJid)
|
| 165 |
-
if not driver:
|
| 166 |
-
reply = (
|
| 167 |
-
"لا يوجد حساب سائق مرتبط بهذا الرفم"
|
| 168 |
-
"من فضللك, سجل كحساب سائق اولا"
|
| 169 |
-
)
|
| 170 |
-
await self._store_and_send_assistant_reply(
|
| 171 |
-
customer,
|
| 172 |
-
inbound.remoteJid,
|
| 173 |
-
reply,
|
| 174 |
-
user_mode=_resolve_user_mode(customer),
|
| 175 |
-
)
|
| 176 |
-
return reply
|
| 177 |
-
|
| 178 |
-
trip = await self.repository.get_trip_by_id(trip_id)
|
| 179 |
-
if not trip or str(trip.get("driver_id")) != str(driver["id"]):
|
| 180 |
-
reply = "That trip could not be found for your account."
|
| 181 |
-
await self._store_and_send_assistant_reply(
|
| 182 |
-
customer,
|
| 183 |
-
inbound.remoteJid,
|
| 184 |
-
reply,
|
| 185 |
-
user_mode=_resolve_user_mode(customer),
|
| 186 |
-
)
|
| 187 |
-
return reply
|
| 188 |
-
|
| 189 |
-
if action == "DELETE":
|
| 190 |
-
await self.repository.cancel_driver_trip(trip_id)
|
| 191 |
-
await unindex_trip(repository=self.repository, trip_id=trip_id)
|
| 192 |
-
reply = "Success! Your trip has been canceled."
|
| 193 |
-
await self._store_and_send_assistant_reply(
|
| 194 |
-
customer,
|
| 195 |
-
inbound.remoteJid,
|
| 196 |
-
reply,
|
| 197 |
-
user_mode="driver",
|
| 198 |
-
)
|
| 199 |
-
return reply
|
| 200 |
-
|
| 201 |
-
if action == "MODIFY":
|
| 202 |
-
await self.repository.set_customer_session_field(
|
| 203 |
-
customer_id=str(customer["id"]),
|
| 204 |
-
key="active_edit_trip_id",
|
| 205 |
-
value=trip_id,
|
| 206 |
-
)
|
| 207 |
-
summary = _trip_summary(trip)
|
| 208 |
-
route = f"{summary.get('departure')} -> {summary.get('destination')}"
|
| 209 |
-
time_label = summary.get("departure_time") or summary.get("departure_time_type")
|
| 210 |
-
system_note = (
|
| 211 |
-
f"SYSTEM: Driver selected trip {trip_id} ({route}, {time_label}) to modify. "
|
| 212 |
-
"Ask them what details they want to change."
|
| 213 |
-
)
|
| 214 |
-
user_mode = "driver"
|
| 215 |
-
registry = self._tool_registry(
|
| 216 |
-
customer,
|
| 217 |
-
remoteJid=inbound.remoteJid,
|
| 218 |
-
user_mode=user_mode,
|
| 219 |
-
current_message=current_message,
|
| 220 |
-
)
|
| 221 |
-
context = await self.repository.get_recent_context_messages(
|
| 222 |
-
customer_id=str(customer["id"]),
|
| 223 |
-
current_message_id=str(current_message["id"]),
|
| 224 |
-
limit=8,
|
| 225 |
-
)
|
| 226 |
-
messages = self._ai_messages(context, user_mode=user_mode)
|
| 227 |
-
messages.append({"role": "system", "content": system_note})
|
| 228 |
-
reply = await self.ai.generate_reply(
|
| 229 |
-
messages=messages,
|
| 230 |
-
tools=get_tool_schemas(user_mode),
|
| 231 |
-
registry=registry,
|
| 232 |
-
)
|
| 233 |
-
await self._store_and_send_assistant_reply(
|
| 234 |
-
customer,
|
| 235 |
-
inbound.remoteJid,
|
| 236 |
-
reply,
|
| 237 |
-
user_mode=user_mode,
|
| 238 |
-
)
|
| 239 |
-
return reply
|
| 240 |
-
|
| 241 |
-
return None
|
| 242 |
-
|
| 243 |
async def _store_and_send_assistant_reply(
|
| 244 |
self,
|
| 245 |
customer: dict[str, Any],
|
|
|
|
| 13 |
from app.tools.registry import ToolRegistry
|
| 14 |
from app.utils.time import now_in_timezone
|
| 15 |
from app.whatsapp.client import WhatsAppClient
|
| 16 |
+
|
| 17 |
|
| 18 |
logger = logging.getLogger(__name__)
|
| 19 |
|
|
|
|
| 121 |
)
|
| 122 |
return reply
|
| 123 |
|
| 124 |
+
if user_mode == "driver" and inbound.context_message_id:
|
| 125 |
+
original = await self.repository.get_message_by_whatsapp_id(inbound.context_message_id)
|
| 126 |
+
if original:
|
| 127 |
+
orig_meta = original.get("metadata") or {}
|
| 128 |
+
if orig_meta.get("type") == "driver_trip_card":
|
| 129 |
+
trip_id = orig_meta.get("trip_id")
|
| 130 |
+
action = orig_meta.get("action")
|
| 131 |
+
if trip_id and action:
|
| 132 |
+
driver = await self.repository.get_driver_by_remoteJid(inbound.remoteJid)
|
| 133 |
+
trip = await self.repository.get_trip_by_id(trip_id)
|
| 134 |
+
if driver and trip and str(trip.get("driver_id")) == str(driver["id"]):
|
| 135 |
+
if action == "DELETE":
|
| 136 |
+
await self.repository.cancel_driver_trip(trip_id)
|
| 137 |
+
await unindex_trip(repository=self.repository, trip_id=trip_id)
|
| 138 |
+
reply = "تم حذف الرحلة بنجاح"
|
| 139 |
+
await self._store_and_send_assistant_reply(
|
| 140 |
+
customer,
|
| 141 |
+
inbound.remoteJid,
|
| 142 |
+
reply,
|
| 143 |
+
user_mode="driver",
|
| 144 |
+
)
|
| 145 |
+
return reply
|
| 146 |
+
|
| 147 |
+
if action == "MODIFY":
|
| 148 |
+
await self.repository.set_customer_session_field(
|
| 149 |
+
customer_id=str(customer["id"]),
|
| 150 |
+
key="active_edit_trip_id",
|
| 151 |
+
value=trip_id,
|
| 152 |
+
)
|
| 153 |
+
summary = _trip_summary(trip)
|
| 154 |
+
route = f"{summary.get('departure')} -> {summary.get('destination')}"
|
| 155 |
+
time_label = summary.get("departure_time") or summary.get("departure_time_type")
|
| 156 |
+
system_note = (
|
| 157 |
+
f"SYSTEM: Driver selected trip {trip_id} ({route}, {time_label}) to modify. "
|
| 158 |
+
"Ask them what details they want to change. if there are no details sent"
|
| 159 |
+
)
|
| 160 |
+
registry = self._tool_registry(
|
| 161 |
+
customer,
|
| 162 |
+
remoteJid=inbound.remoteJid,
|
| 163 |
+
user_mode="driver",
|
| 164 |
+
current_message=current_message,
|
| 165 |
+
)
|
| 166 |
+
context = await self.repository.get_recent_context_messages(
|
| 167 |
+
customer_id=str(customer["id"]),
|
| 168 |
+
current_message_id=str(current_message["id"]),
|
| 169 |
+
limit=8,
|
| 170 |
+
)
|
| 171 |
+
messages = self._ai_messages(context, user_mode="driver")
|
| 172 |
+
messages.append({"role": "system", "content": system_note})
|
| 173 |
+
reply = await self.ai.generate_reply(
|
| 174 |
+
messages=messages,
|
| 175 |
+
tools=get_tool_schemas("driver"),
|
| 176 |
+
registry=registry,
|
| 177 |
+
)
|
| 178 |
+
if reply:
|
| 179 |
+
await self._store_and_send_assistant_reply(
|
| 180 |
+
customer,
|
| 181 |
+
inbound.remoteJid,
|
| 182 |
+
reply,
|
| 183 |
+
user_mode="driver",
|
| 184 |
+
)
|
| 185 |
+
return reply
|
| 186 |
+
|
| 187 |
reply = await self.ai.generate_reply(
|
| 188 |
messages=self._ai_messages(context, user_mode=user_mode),
|
| 189 |
tools=get_tool_schemas(user_mode),
|
|
|
|
| 204 |
|
| 205 |
return reply
|
| 206 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 207 |
async def _store_and_send_assistant_reply(
|
| 208 |
self,
|
| 209 |
customer: dict[str, Any],
|
app/tools/handlers.py
CHANGED
|
@@ -12,7 +12,7 @@ _NO_DRIVER_ERROR = (
|
|
| 12 |
from app.database.supabase import SupabaseRepository
|
| 13 |
from app.models.domain import ToolResult
|
| 14 |
from app.services.embedding_service import JinaEmbeddingService
|
| 15 |
-
from app.services.trip_indexing import index_trip
|
| 16 |
from app.utils.departure import (
|
| 17 |
_parse_date_value,
|
| 18 |
normalize_departure_bucket,
|
|
@@ -23,7 +23,7 @@ from app.utils.departure import (
|
|
| 23 |
trip_satisfies_departure_request,
|
| 24 |
)
|
| 25 |
from app.whatsapp.client import WhatsAppClient, WhatsAppClientError
|
| 26 |
-
from app.whatsapp.trip_selection import
|
| 27 |
|
| 28 |
|
| 29 |
class FalsaToolHandlers:
|
|
@@ -370,18 +370,52 @@ class FalsaToolHandlers:
|
|
| 370 |
)
|
| 371 |
|
| 372 |
trips = await self.repository.list_driver_trips(str(driver["id"]))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 373 |
return ToolResult(
|
| 374 |
ok=True,
|
| 375 |
data={
|
| 376 |
-
"driver_id": driver["id"],
|
| 377 |
-
"upcoming_trips": await self._summarize_trips(trips),
|
| 378 |
"count": len(trips),
|
| 379 |
-
"
|
| 380 |
-
|
| 381 |
-
if not trips
|
| 382 |
-
else "Upcoming active trips retrieved successfully."
|
| 383 |
-
),
|
| 384 |
},
|
|
|
|
| 385 |
)
|
| 386 |
|
| 387 |
async def add_driver_car(self, arguments: dict[str, Any]) -> ToolResult:
|
|
@@ -623,116 +657,44 @@ class FalsaToolHandlers:
|
|
| 623 |
},
|
| 624 |
)
|
| 625 |
|
| 626 |
-
|
| 627 |
-
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
|
| 633 |
-
|
| 634 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 635 |
|
| 636 |
return ToolResult(
|
| 637 |
ok=True,
|
| 638 |
data={
|
| 639 |
"count": len(trips),
|
| 640 |
"action_type": action_type,
|
| 641 |
-
"
|
| 642 |
-
|
| 643 |
-
"Ask them to reply with the trip number."
|
| 644 |
-
),
|
| 645 |
-
},
|
| 646 |
-
)
|
| 647 |
-
|
| 648 |
-
async def delete_trip_by_number(self, arguments: dict[str, Any]) -> ToolResult:
|
| 649 |
-
driver = await self.repository.get_driver_by_remoteJid(self.remoteJid)
|
| 650 |
-
if not driver:
|
| 651 |
-
return ToolResult(
|
| 652 |
-
ok=False,
|
| 653 |
-
data={"action": "create_driver_account"},
|
| 654 |
-
error=_NO_DRIVER_ERROR,
|
| 655 |
-
)
|
| 656 |
-
|
| 657 |
-
trip_number = _optional_int(arguments.get("trip_number"))
|
| 658 |
-
if trip_number is None or trip_number < 1:
|
| 659 |
-
return ToolResult(ok=False, data={}, error="trip_number must be an integer >= 1")
|
| 660 |
-
|
| 661 |
-
trips = await self.repository.list_driver_trips(str(driver["id"]))
|
| 662 |
-
if trip_number > len(trips):
|
| 663 |
-
return ToolResult(
|
| 664 |
-
ok=False,
|
| 665 |
-
data={"count": len(trips)},
|
| 666 |
-
error=f"trip_number must be between 1 and {len(trips)}",
|
| 667 |
-
)
|
| 668 |
-
|
| 669 |
-
trip = trips[trip_number - 1]
|
| 670 |
-
await self.repository.cancel_driver_trip(str(trip["id"]))
|
| 671 |
-
await unindex_trip(repository=self.repository, trip_id=str(trip["id"]))
|
| 672 |
-
|
| 673 |
-
return ToolResult(
|
| 674 |
-
ok=True,
|
| 675 |
-
data={
|
| 676 |
-
"trip_number": trip_number,
|
| 677 |
-
"trip_id": trip.get("id"),
|
| 678 |
-
"trip": _trip_summary(trip, trip_number=trip_number),
|
| 679 |
-
"message": "Success! Your trip has been canceled.",
|
| 680 |
-
},
|
| 681 |
-
)
|
| 682 |
-
|
| 683 |
-
async def modify_trip_by_number(self, arguments: dict[str, Any]) -> ToolResult:
|
| 684 |
-
print("\n modefing our new \n")
|
| 685 |
-
driver = await self.repository.get_driver_by_remoteJid(self.remoteJid)
|
| 686 |
-
if not driver:
|
| 687 |
-
return ToolResult(
|
| 688 |
-
ok=False,
|
| 689 |
-
data={"action": "create_driver_account"},
|
| 690 |
-
error=_NO_DRIVER_ERROR,
|
| 691 |
-
)
|
| 692 |
-
|
| 693 |
-
trip_number = _optional_int(arguments.get("trip_number"))
|
| 694 |
-
field = _optional_string(arguments.get("field"))
|
| 695 |
-
value = _optional_string(arguments.get("value"))
|
| 696 |
-
if trip_number is None or trip_number < 1:
|
| 697 |
-
return ToolResult(ok=False, data={}, error="trip_number must be an integer >= 1")
|
| 698 |
-
if not field or value is None:
|
| 699 |
-
return ToolResult(ok=False, data={}, error="field and value are required")
|
| 700 |
-
|
| 701 |
-
trips = await self.repository.list_driver_trips(str(driver["id"]))
|
| 702 |
-
if trip_number > len(trips):
|
| 703 |
-
return ToolResult(
|
| 704 |
-
ok=False,
|
| 705 |
-
data={"count": len(trips)},
|
| 706 |
-
error=f"trip_number must be between 1 and {len(trips)}",
|
| 707 |
-
)
|
| 708 |
-
|
| 709 |
-
trip = trips[trip_number - 1]
|
| 710 |
-
updates, error = await self._build_trip_field_update(
|
| 711 |
-
driver_id=str(driver["id"]),
|
| 712 |
-
field=field,
|
| 713 |
-
value=value,
|
| 714 |
-
)
|
| 715 |
-
if error:
|
| 716 |
-
return ToolResult(ok=False, data={}, error=error)
|
| 717 |
-
|
| 718 |
-
updated_trip = await self.repository.update_driver_trip(str(trip["id"]), updates)
|
| 719 |
-
await index_trip(
|
| 720 |
-
repository=self.repository,
|
| 721 |
-
embeddings=self.embeddings,
|
| 722 |
-
embedding_model=self.embedding_model,
|
| 723 |
-
trip=updated_trip,
|
| 724 |
-
)
|
| 725 |
-
|
| 726 |
-
return ToolResult(
|
| 727 |
-
ok=True,
|
| 728 |
-
data={
|
| 729 |
-
"trip_number": trip_number,
|
| 730 |
-
"trip_id": updated_trip.get("id"),
|
| 731 |
-
"field": field,
|
| 732 |
-
"value": value,
|
| 733 |
-
"trip": _trip_summary(updated_trip, trip_number=trip_number),
|
| 734 |
-
"message": "Trip updated successfully.",
|
| 735 |
},
|
|
|
|
| 736 |
)
|
| 737 |
|
| 738 |
async def update_trip_field(self, arguments: dict[str, Any]) -> ToolResult:
|
|
|
|
| 12 |
from app.database.supabase import SupabaseRepository
|
| 13 |
from app.models.domain import ToolResult
|
| 14 |
from app.services.embedding_service import JinaEmbeddingService
|
| 15 |
+
from app.services.trip_indexing import index_trip
|
| 16 |
from app.utils.departure import (
|
| 17 |
_parse_date_value,
|
| 18 |
normalize_departure_bucket,
|
|
|
|
| 23 |
trip_satisfies_departure_request,
|
| 24 |
)
|
| 25 |
from app.whatsapp.client import WhatsAppClient, WhatsAppClientError
|
| 26 |
+
from app.whatsapp.trip_selection import format_trip_card
|
| 27 |
|
| 28 |
|
| 29 |
class FalsaToolHandlers:
|
|
|
|
| 370 |
)
|
| 371 |
|
| 372 |
trips = await self.repository.list_driver_trips(str(driver["id"]))
|
| 373 |
+
|
| 374 |
+
if not trips:
|
| 375 |
+
return ToolResult(
|
| 376 |
+
ok=True,
|
| 377 |
+
data={
|
| 378 |
+
"count": 0,
|
| 379 |
+
"message": "No upcoming active trips found.",
|
| 380 |
+
},
|
| 381 |
+
)
|
| 382 |
+
|
| 383 |
+
for trip in trips[:5]:
|
| 384 |
+
trip_id = trip.get("trip_id") or trip.get("id")
|
| 385 |
+
selection_count = await self.repository.count_trip_selections(trip_id)
|
| 386 |
+
trip["selection_count"] = selection_count
|
| 387 |
+
card = format_trip_card(trip)
|
| 388 |
+
try:
|
| 389 |
+
resp = await self.whatsapp.send_text(self.remoteJid, card)
|
| 390 |
+
wam_id = resp.get("messages", [{}])[0].get("id")
|
| 391 |
+
if wam_id:
|
| 392 |
+
await self.repository.create_message(
|
| 393 |
+
customer_id=str(self.customer["id"]),
|
| 394 |
+
sender_type="assistant",
|
| 395 |
+
message=card,
|
| 396 |
+
whatsapp_message_id=wam_id,
|
| 397 |
+
metadata={"trip_id": trip_id, "type": "driver_trip_card"},
|
| 398 |
+
)
|
| 399 |
+
except WhatsAppClientError:
|
| 400 |
+
logger.warning("Failed to send trip card for trip %s", trip_id)
|
| 401 |
+
|
| 402 |
+
prompt = "هذه قائمة رحلاتك المسجلة"
|
| 403 |
+
await self.whatsapp.send_text(self.remoteJid, prompt)
|
| 404 |
+
await self.repository.create_message(
|
| 405 |
+
customer_id=str(self.customer["id"]),
|
| 406 |
+
sender_type="assistant",
|
| 407 |
+
message=prompt,
|
| 408 |
+
metadata={"type": "driver_trip_list_prompt"},
|
| 409 |
+
)
|
| 410 |
+
|
| 411 |
return ToolResult(
|
| 412 |
ok=True,
|
| 413 |
data={
|
|
|
|
|
|
|
| 414 |
"count": len(trips),
|
| 415 |
+
"sent_as_messages": True,
|
| 416 |
+
"note": "Trip cards sent. No text reply needed.",
|
|
|
|
|
|
|
|
|
|
| 417 |
},
|
| 418 |
+
suppress_llm_reply=True,
|
| 419 |
)
|
| 420 |
|
| 421 |
async def add_driver_car(self, arguments: dict[str, Any]) -> ToolResult:
|
|
|
|
| 657 |
},
|
| 658 |
)
|
| 659 |
|
| 660 |
+
action_label = "حذفها" if action_type == "DELETE" else "تعديلها"
|
| 661 |
+
for trip in trips[:5]:
|
| 662 |
+
trip_id = trip.get("trip_id") or trip.get("id")
|
| 663 |
+
selection_count = await self.repository.count_trip_selections(trip_id)
|
| 664 |
+
trip["selection_count"] = selection_count
|
| 665 |
+
card = format_trip_card(trip)
|
| 666 |
+
try:
|
| 667 |
+
resp = await self.whatsapp.send_text(self.remoteJid, card)
|
| 668 |
+
wam_id = resp.get("messages", [{}])[0].get("id")
|
| 669 |
+
if wam_id:
|
| 670 |
+
await self.repository.create_message(
|
| 671 |
+
customer_id=str(self.customer["id"]),
|
| 672 |
+
sender_type="assistant",
|
| 673 |
+
message=card,
|
| 674 |
+
whatsapp_message_id=wam_id,
|
| 675 |
+
metadata={"trip_id": trip_id, "type": "driver_trip_card", "action": action_type},
|
| 676 |
+
)
|
| 677 |
+
except WhatsAppClientError:
|
| 678 |
+
logger.warning("Failed to send trip card for trip %s", trip_id)
|
| 679 |
+
|
| 680 |
+
prompt = f"قم بالرد على بطاقة الرحلة التي تريد {action_label}"
|
| 681 |
+
await self.whatsapp.send_text(self.remoteJid, prompt)
|
| 682 |
+
await self.repository.create_message(
|
| 683 |
+
customer_id=str(self.customer["id"]),
|
| 684 |
+
sender_type="assistant",
|
| 685 |
+
message=prompt,
|
| 686 |
+
metadata={"type": "driver_trip_selection_prompt"},
|
| 687 |
+
)
|
| 688 |
|
| 689 |
return ToolResult(
|
| 690 |
ok=True,
|
| 691 |
data={
|
| 692 |
"count": len(trips),
|
| 693 |
"action_type": action_type,
|
| 694 |
+
"sent_as_messages": True,
|
| 695 |
+
"note": "Trip cards sent. No text reply needed.",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 696 |
},
|
| 697 |
+
suppress_llm_reply=True,
|
| 698 |
)
|
| 699 |
|
| 700 |
async def update_trip_field(self, arguments: dict[str, Any]) -> ToolResult:
|
app/whatsapp/trip_selection.py
CHANGED
|
@@ -8,74 +8,6 @@ _BUCKET_LABELS = {
|
|
| 8 |
"night": "مساء",
|
| 9 |
}
|
| 10 |
|
| 11 |
-
_ACTION_DESCRIPTIONS = {
|
| 12 |
-
"DELETE": "🗑️ اختر رقم الرحله التي تريد حذفها من فضلك",
|
| 13 |
-
"MODIFY": "✏️ يمكنك ارسال رقم الرحلة التي تريد تعديلها",
|
| 14 |
-
}
|
| 15 |
-
_PERFFIX= "قائمة الرحلات المسجله في حسابك لدينا\n\n"+("-"*7)
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
def parse_trip_action_reply(reply_id: str) -> tuple[str, str] | None:
|
| 19 |
-
if "_" not in reply_id:
|
| 20 |
-
return None
|
| 21 |
-
action, trip_id = reply_id.split("_", 1)
|
| 22 |
-
if action not in {"DELETE", "MODIFY"} or not trip_id:
|
| 23 |
-
return None
|
| 24 |
-
return action, trip_id
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
def build_trip_selection_list(
|
| 28 |
-
*,
|
| 29 |
-
trips: list[dict[str, Any]],
|
| 30 |
-
action_type: str,
|
| 31 |
-
) -> dict[str, Any]:
|
| 32 |
-
action = action_type.upper()
|
| 33 |
-
suffix = _ACTION_DESCRIPTIONS.get(action, "")
|
| 34 |
-
rows = [
|
| 35 |
-
f"""
|
| 36 |
-
[{str(trips.index(trip))}] :الرحلة رقم
|
| 37 |
-
\n
|
| 38 |
-
{_trip_row_description(trip)}
|
| 39 |
-
"""
|
| 40 |
-
for trip in trips[:10]
|
| 41 |
-
]
|
| 42 |
-
rows_text = "\n".join(rows)
|
| 43 |
-
return {
|
| 44 |
-
"type": "text",
|
| 45 |
-
"text": {
|
| 46 |
-
"body": f"{_PERFFIX} {rows_text} {suffix}"
|
| 47 |
-
|
| 48 |
-
}
|
| 49 |
-
}
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
def build_trip_selection_text(
|
| 53 |
-
*,
|
| 54 |
-
trips: list[dict[str, Any]],
|
| 55 |
-
action_type: str,
|
| 56 |
-
) -> str:
|
| 57 |
-
action = action_type.upper()
|
| 58 |
-
suffix = {
|
| 59 |
-
"DELETE": "ارسل رقم الرحلة التي تريد حذفها.",
|
| 60 |
-
"MODIFY": "ارسل رقم الرحلة التي تريد تعديلها.",
|
| 61 |
-
}.get(action, "")
|
| 62 |
-
|
| 63 |
-
lines = [
|
| 64 |
-
f"قائمة الرحلات المسجلة لك:\n"
|
| 65 |
-
]
|
| 66 |
-
for index, trip in enumerate(trips, start=1):
|
| 67 |
-
parsed_date = trip_departure_date(trip)
|
| 68 |
-
bucket = trip_departure_bucket(trip)
|
| 69 |
-
bucket_text = _BUCKET_LABELS.get(bucket, "") if bucket else ""
|
| 70 |
-
date_text = parsed_date.isoformat() if parsed_date else str(trip.get("departure_date") or "")
|
| 71 |
-
lines.append(
|
| 72 |
-
f"{index}. {trip.get('departure', '')} → {trip.get('destination', '')} | {date_text} | {bucket_text} | seats: {trip.get('available_seats')} / {trip.get('total_seats')} | price: {trip.get('price')}"
|
| 73 |
-
)
|
| 74 |
-
lines.append("")
|
| 75 |
-
lines.append(suffix)
|
| 76 |
-
return "\n".join(lines)
|
| 77 |
-
|
| 78 |
-
|
| 79 |
def format_trip_card(trip: dict[str, Any]) -> str:
|
| 80 |
departure = str(trip.get("departure") or "")
|
| 81 |
destination = str(trip.get("destination") or "")
|
|
@@ -116,16 +48,3 @@ def _first_dict_value(value: Any) -> dict[str, Any] | None:
|
|
| 116 |
return None
|
| 117 |
|
| 118 |
|
| 119 |
-
def _trip_row_description(trip: dict[str, Any]) -> str:
|
| 120 |
-
departure = str(trip.get("departure") or "")
|
| 121 |
-
destination = str(trip.get("destination") or "")
|
| 122 |
-
parsed_date = trip_departure_date(trip)
|
| 123 |
-
date_text = parsed_date.isoformat() if parsed_date else ""
|
| 124 |
-
bucket = trip_departure_bucket(trip)
|
| 125 |
-
bucket_text = _BUCKET_LABELS.get(bucket, "") if bucket else ""
|
| 126 |
-
|
| 127 |
-
return (
|
| 128 |
-
f"🚌 {departure} → {destination}\n"
|
| 129 |
-
f"📅 التاريخ: {date_text}\n"
|
| 130 |
-
f"🕰️ الوقت: {bucket_text}"
|
| 131 |
-
)
|
|
|
|
| 8 |
"night": "مساء",
|
| 9 |
}
|
| 10 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
def format_trip_card(trip: dict[str, Any]) -> str:
|
| 12 |
departure = str(trip.get("departure") or "")
|
| 13 |
destination = str(trip.get("destination") or "")
|
|
|
|
| 48 |
return None
|
| 49 |
|
| 50 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/conftest.py
CHANGED
|
@@ -167,10 +167,16 @@ class FakeRepository:
|
|
| 167 |
return customer
|
| 168 |
raise KeyError(customer_id)
|
| 169 |
|
| 170 |
-
|
| 171 |
-
for customer in self.
|
| 172 |
if customer["id"] == customer_id:
|
| 173 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 174 |
return {}
|
| 175 |
|
| 176 |
async def update_customer_session(
|
|
@@ -179,10 +185,10 @@ class FakeRepository:
|
|
| 179 |
customer_id: str,
|
| 180 |
session_data: dict[str, Any],
|
| 181 |
) -> dict[str, Any]:
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
raise KeyError(customer_id)
|
| 187 |
|
| 188 |
async def set_customer_session_field(
|
|
@@ -312,6 +318,9 @@ class FakeRepository:
|
|
| 312 |
async def get_driver_by_phone(self, remote_jid: str) -> dict[str, Any] | None:
|
| 313 |
return self.drivers_by_remote_jid.get(remote_jid)
|
| 314 |
|
|
|
|
|
|
|
|
|
|
| 315 |
async def create_driver(self, *, customer_id: str) -> dict[str, Any]:
|
| 316 |
customer = next(
|
| 317 |
(customer for customer in self.customers_by_remote_jid.values() if customer["id"] == customer_id),
|
|
@@ -322,6 +331,7 @@ class FakeRepository:
|
|
| 322 |
driver = {
|
| 323 |
"id": f"driver-{len(self.drivers_by_remote_jid) + 1}",
|
| 324 |
"customer_id": customer_id,
|
|
|
|
| 325 |
"status": "active",
|
| 326 |
"customers": customer,
|
| 327 |
}
|
|
@@ -405,6 +415,22 @@ class FakeRepository:
|
|
| 405 |
trip.update(updates)
|
| 406 |
return trip
|
| 407 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 408 |
async def cancel_driver_trip(self, trip_id: str) -> dict[str, Any]:
|
| 409 |
trip = self.trips_by_id[trip_id]
|
| 410 |
trip["status"] = "cancelled"
|
|
|
|
| 167 |
return customer
|
| 168 |
raise KeyError(customer_id)
|
| 169 |
|
| 170 |
+
def _find_customer_by_id(self, customer_id: str) -> dict[str, Any] | None:
|
| 171 |
+
for customer in self.customers_by_remote_jid.values():
|
| 172 |
if customer["id"] == customer_id:
|
| 173 |
+
return customer
|
| 174 |
+
return None
|
| 175 |
+
|
| 176 |
+
async def get_customer_session(self, customer_id: str) -> dict[str, Any]:
|
| 177 |
+
customer = self._find_customer_by_id(customer_id)
|
| 178 |
+
if customer:
|
| 179 |
+
return dict(customer.get("session_data") or {})
|
| 180 |
return {}
|
| 181 |
|
| 182 |
async def update_customer_session(
|
|
|
|
| 185 |
customer_id: str,
|
| 186 |
session_data: dict[str, Any],
|
| 187 |
) -> dict[str, Any]:
|
| 188 |
+
customer = self._find_customer_by_id(customer_id)
|
| 189 |
+
if customer:
|
| 190 |
+
customer["session_data"] = session_data
|
| 191 |
+
return customer
|
| 192 |
raise KeyError(customer_id)
|
| 193 |
|
| 194 |
async def set_customer_session_field(
|
|
|
|
| 318 |
async def get_driver_by_phone(self, remote_jid: str) -> dict[str, Any] | None:
|
| 319 |
return self.drivers_by_remote_jid.get(remote_jid)
|
| 320 |
|
| 321 |
+
async def get_driver_by_remoteJid(self, remote_jid: str) -> dict[str, Any] | None:
|
| 322 |
+
return self.drivers_by_remote_jid.get(remote_jid)
|
| 323 |
+
|
| 324 |
async def create_driver(self, *, customer_id: str) -> dict[str, Any]:
|
| 325 |
customer = next(
|
| 326 |
(customer for customer in self.customers_by_remote_jid.values() if customer["id"] == customer_id),
|
|
|
|
| 331 |
driver = {
|
| 332 |
"id": f"driver-{len(self.drivers_by_remote_jid) + 1}",
|
| 333 |
"customer_id": customer_id,
|
| 334 |
+
"name": customer.get("name"),
|
| 335 |
"status": "active",
|
| 336 |
"customers": customer,
|
| 337 |
}
|
|
|
|
| 415 |
trip.update(updates)
|
| 416 |
return trip
|
| 417 |
|
| 418 |
+
async def get_driver_trip_by_datetime(
|
| 419 |
+
self,
|
| 420 |
+
*,
|
| 421 |
+
driver_id: str,
|
| 422 |
+
departure_date: Any,
|
| 423 |
+
departure_time: str,
|
| 424 |
+
) -> dict[str, Any] | None:
|
| 425 |
+
for trip in self.trips_by_id.values():
|
| 426 |
+
if (
|
| 427 |
+
str(trip.get("driver_id")) == str(driver_id)
|
| 428 |
+
and str(trip.get("departure_date")) == str(departure_date)
|
| 429 |
+
and str(trip.get("departure_time")) == departure_time
|
| 430 |
+
):
|
| 431 |
+
return trip
|
| 432 |
+
return None
|
| 433 |
+
|
| 434 |
async def cancel_driver_trip(self, trip_id: str) -> dict[str, Any]:
|
| 435 |
trip = self.trips_by_id[trip_id]
|
| 436 |
trip["status"] = "cancelled"
|
tests/test_conversation_service.py
CHANGED
|
@@ -28,7 +28,7 @@ async def test_conversation_stores_messages_uses_last_four_context_and_sends_rep
|
|
| 28 |
reply = await service.handle_inbound_message(
|
| 29 |
WhatsAppInboundMessage(
|
| 30 |
message_id="wamid.new",
|
| 31 |
-
|
| 32 |
text="Aden to Mukalla tomorrow",
|
| 33 |
profile_name="Customer",
|
| 34 |
)
|
|
@@ -40,14 +40,10 @@ async def test_conversation_stores_messages_uses_last_four_context_and_sends_rep
|
|
| 40 |
|
| 41 |
ai_messages = ai.calls[0]["messages"]
|
| 42 |
assert ai_messages[0]["role"] == "system"
|
| 43 |
-
assert "
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
"prior-4",
|
| 48 |
-
"prior-5",
|
| 49 |
-
"Aden to Mukalla tomorrow",
|
| 50 |
-
]
|
| 51 |
tool_names = {tool["function"]["name"] for tool in ai.calls[0]["tools"]}
|
| 52 |
assert tool_names == {
|
| 53 |
"about_falsa",
|
|
@@ -80,7 +76,7 @@ async def test_conversation_skips_duplicate_whatsapp_message(settings):
|
|
| 80 |
result = await service.handle_inbound_message(
|
| 81 |
WhatsAppInboundMessage(
|
| 82 |
message_id="wamid.duplicate",
|
| 83 |
-
|
| 84 |
text="same message",
|
| 85 |
)
|
| 86 |
)
|
|
@@ -107,7 +103,7 @@ async def test_conversation_uses_passenger_tools_when_user_mode_is_passenger(set
|
|
| 107 |
await service.handle_inbound_message(
|
| 108 |
WhatsAppInboundMessage(
|
| 109 |
message_id="wamid.passenger",
|
| 110 |
-
|
| 111 |
text="Aden to Mukalla tomorrow",
|
| 112 |
profile_name="Customer",
|
| 113 |
)
|
|
@@ -142,7 +138,7 @@ async def test_conversation_uses_driver_tools_when_user_mode_is_driver(settings)
|
|
| 142 |
await service.handle_inbound_message(
|
| 143 |
WhatsAppInboundMessage(
|
| 144 |
message_id="wamid.driver",
|
| 145 |
-
|
| 146 |
text="Check my trips",
|
| 147 |
profile_name="Ali",
|
| 148 |
)
|
|
@@ -155,102 +151,10 @@ async def test_conversation_uses_driver_tools_when_user_mode_is_driver(settings)
|
|
| 155 |
"check_driver_trips",
|
| 156 |
"add_driver_car",
|
| 157 |
"add_trip_by_driver",
|
| 158 |
-
"delete_trip_by_number",
|
| 159 |
-
"modify_trip_by_number",
|
| 160 |
"initiate_trip_action",
|
| 161 |
"update_trip_field",
|
| 162 |
"switch_to_passenger",
|
| 163 |
}
|
| 164 |
|
| 165 |
|
| 166 |
-
@pytest.mark.asyncio
|
| 167 |
-
async def test_conversation_handles_delete_interactive_reply(settings):
|
| 168 |
-
repository = FakeRepository()
|
| 169 |
-
customer = await repository.upsert_customer(phone_number="967700000010", name="Ali")
|
| 170 |
-
customer["user_mode"] = "driver"
|
| 171 |
-
repository.drivers_by_phone["967700000010"] = {
|
| 172 |
-
"id": "driver-1",
|
| 173 |
-
"name": "Ali",
|
| 174 |
-
"phone_number": "967700000010",
|
| 175 |
-
}
|
| 176 |
-
repository.trips_by_id["trip-1"] = {
|
| 177 |
-
"id": "trip-1",
|
| 178 |
-
"driver_id": "driver-1",
|
| 179 |
-
"departure": "Aden",
|
| 180 |
-
"destination": "Mukalla",
|
| 181 |
-
"departure_date": "2026-12-01",
|
| 182 |
-
"departure_time": "morning",
|
| 183 |
-
"status": "active",
|
| 184 |
-
"driver_cars": {"car_type": "SUV"},
|
| 185 |
-
"drivers": {"name": "Ali"},
|
| 186 |
-
}
|
| 187 |
-
repository.trip_embeddings.append({"trip_id": "trip-1"})
|
| 188 |
-
ai = FakeAI()
|
| 189 |
-
service = ConversationService(
|
| 190 |
-
repository=repository,
|
| 191 |
-
embeddings=FakeEmbeddings(),
|
| 192 |
-
whatsapp=FakeWhatsApp(),
|
| 193 |
-
ai=ai,
|
| 194 |
-
settings=settings,
|
| 195 |
-
)
|
| 196 |
-
|
| 197 |
-
reply = await service.handle_inbound_message(
|
| 198 |
-
WhatsAppInboundMessage(
|
| 199 |
-
message_id="wamid.delete",
|
| 200 |
-
from_phone="967700000010",
|
| 201 |
-
text="Noon - Mukalla",
|
| 202 |
-
message_type="interactive",
|
| 203 |
-
interactive_reply_id="DELETE_trip-1",
|
| 204 |
-
)
|
| 205 |
-
)
|
| 206 |
-
|
| 207 |
-
assert reply == "Success! Your trip has been canceled."
|
| 208 |
-
assert repository.trips_by_id["trip-1"]["status"] == "cancelled"
|
| 209 |
-
assert repository.trip_embeddings == []
|
| 210 |
-
assert ai.calls == []
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
@pytest.mark.asyncio
|
| 214 |
-
async def test_conversation_handles_modify_interactive_reply(settings):
|
| 215 |
-
repository = FakeRepository()
|
| 216 |
-
customer = await repository.upsert_customer(phone_number="967700000010", name="Ali")
|
| 217 |
-
customer["user_mode"] = "driver"
|
| 218 |
-
repository.drivers_by_phone["967700000010"] = {
|
| 219 |
-
"id": "driver-1",
|
| 220 |
-
"name": "Ali",
|
| 221 |
-
"phone_number": "967700000010",
|
| 222 |
-
}
|
| 223 |
-
repository.trips_by_id["trip-1"] = {
|
| 224 |
-
"id": "trip-1",
|
| 225 |
-
"driver_id": "driver-1",
|
| 226 |
-
"departure": "Aden",
|
| 227 |
-
"destination": "Mukalla",
|
| 228 |
-
"departure_date": "2026-12-01",
|
| 229 |
-
"departure_time": "morning",
|
| 230 |
-
"status": "active",
|
| 231 |
-
"driver_cars": {"car_type": "SUV"},
|
| 232 |
-
"drivers": {"name": "Ali"},
|
| 233 |
-
}
|
| 234 |
-
ai = FakeAI(reply="What would you like to change?")
|
| 235 |
-
service = ConversationService(
|
| 236 |
-
repository=repository,
|
| 237 |
-
embeddings=FakeEmbeddings(),
|
| 238 |
-
whatsapp=FakeWhatsApp(),
|
| 239 |
-
ai=ai,
|
| 240 |
-
settings=settings,
|
| 241 |
-
)
|
| 242 |
-
|
| 243 |
-
reply = await service.handle_inbound_message(
|
| 244 |
-
WhatsAppInboundMessage(
|
| 245 |
-
message_id="wamid.modify",
|
| 246 |
-
from_phone="967700000010",
|
| 247 |
-
text="Morning - Mukalla",
|
| 248 |
-
message_type="interactive",
|
| 249 |
-
interactive_reply_id="MODIFY_trip-1",
|
| 250 |
-
)
|
| 251 |
-
)
|
| 252 |
|
| 253 |
-
assert reply == "What would you like to change?"
|
| 254 |
-
session = await repository.get_customer_session(customer["id"])
|
| 255 |
-
assert session["active_edit_trip_id"] == "trip-1"
|
| 256 |
-
assert ai.calls[0]["messages"][-1]["content"].startswith("SYSTEM: Driver selected trip trip-1")
|
|
|
|
| 28 |
reply = await service.handle_inbound_message(
|
| 29 |
WhatsAppInboundMessage(
|
| 30 |
message_id="wamid.new",
|
| 31 |
+
remoteJid="967700000001",
|
| 32 |
text="Aden to Mukalla tomorrow",
|
| 33 |
profile_name="Customer",
|
| 34 |
)
|
|
|
|
| 40 |
|
| 41 |
ai_messages = ai.calls[0]["messages"]
|
| 42 |
assert ai_messages[0]["role"] == "system"
|
| 43 |
+
assert "travel as passenger" in ai_messages[0]["content"]
|
| 44 |
+
context_contents = [message["content"] for message in ai_messages[1:]]
|
| 45 |
+
assert context_contents[-1] == "Aden to Mukalla tomorrow"
|
| 46 |
+
assert len(context_contents) <= 9
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
tool_names = {tool["function"]["name"] for tool in ai.calls[0]["tools"]}
|
| 48 |
assert tool_names == {
|
| 49 |
"about_falsa",
|
|
|
|
| 76 |
result = await service.handle_inbound_message(
|
| 77 |
WhatsAppInboundMessage(
|
| 78 |
message_id="wamid.duplicate",
|
| 79 |
+
remoteJid="967700000001",
|
| 80 |
text="same message",
|
| 81 |
)
|
| 82 |
)
|
|
|
|
| 103 |
await service.handle_inbound_message(
|
| 104 |
WhatsAppInboundMessage(
|
| 105 |
message_id="wamid.passenger",
|
| 106 |
+
remoteJid="967700000001",
|
| 107 |
text="Aden to Mukalla tomorrow",
|
| 108 |
profile_name="Customer",
|
| 109 |
)
|
|
|
|
| 138 |
await service.handle_inbound_message(
|
| 139 |
WhatsAppInboundMessage(
|
| 140 |
message_id="wamid.driver",
|
| 141 |
+
remoteJid="967700000010",
|
| 142 |
text="Check my trips",
|
| 143 |
profile_name="Ali",
|
| 144 |
)
|
|
|
|
| 151 |
"check_driver_trips",
|
| 152 |
"add_driver_car",
|
| 153 |
"add_trip_by_driver",
|
|
|
|
|
|
|
| 154 |
"initiate_trip_action",
|
| 155 |
"update_trip_field",
|
| 156 |
"switch_to_passenger",
|
| 157 |
}
|
| 158 |
|
| 159 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
|
|
|
|
|
|
|
|
|
|
|
|
tests/test_tool_schemas.py
CHANGED
|
@@ -30,4 +30,4 @@ def test_passenger_tools_exclude_driver_management_tools():
|
|
| 30 |
|
| 31 |
def test_all_tool_schemas_include_every_tool():
|
| 32 |
names = {schema["function"]["name"] for schema in get_all_tool_schemas()}
|
| 33 |
-
assert len(names) ==
|
|
|
|
| 30 |
|
| 31 |
def test_all_tool_schemas_include_every_tool():
|
| 32 |
names = {schema["function"]["name"] for schema in get_all_tool_schemas()}
|
| 33 |
+
assert len(names) == 12
|
tests/test_tools.py
CHANGED
|
@@ -12,11 +12,16 @@ def make_handlers(
|
|
| 12 |
customer: dict | None = None,
|
| 13 |
sender_phone: str = "967700000001",
|
| 14 |
) -> FalsaToolHandlers:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
return FalsaToolHandlers(
|
| 16 |
-
repository=
|
| 17 |
embeddings=embeddings or FakeEmbeddings(),
|
| 18 |
whatsapp=whatsapp or FakeWhatsApp(),
|
| 19 |
-
customer=customer
|
| 20 |
remoteJid=sender_phone,
|
| 21 |
embedding_model="jina-embeddings-v5-text-small",
|
| 22 |
)
|
|
@@ -302,7 +307,7 @@ async def test_create_driver_account_uses_sender_phone():
|
|
| 302 |
|
| 303 |
assert result.ok is True
|
| 304 |
assert repository.created_drivers[0]["customers"]["remoteJid"] == "967700000010"
|
| 305 |
-
assert
|
| 306 |
|
| 307 |
|
| 308 |
@pytest.mark.asyncio
|
|
@@ -425,7 +430,7 @@ async def test_check_driver_trips_returns_upcoming_trips():
|
|
| 425 |
|
| 426 |
assert result.ok is True
|
| 427 |
assert result.data["count"] == 1
|
| 428 |
-
assert result.
|
| 429 |
|
| 430 |
|
| 431 |
@pytest.mark.asyncio
|
|
@@ -612,7 +617,7 @@ async def test_switch_to_passenger_without_name():
|
|
| 612 |
|
| 613 |
|
| 614 |
def _driver_setup(repository: FakeRepository, *, phone: str = "967700000010") -> None:
|
| 615 |
-
repository.
|
| 616 |
"id": "driver-1",
|
| 617 |
"name": "Ali",
|
| 618 |
"phone_number": phone,
|
|
@@ -634,7 +639,7 @@ def _driver_setup(repository: FakeRepository, *, phone: str = "967700000010") ->
|
|
| 634 |
|
| 635 |
|
| 636 |
@pytest.mark.asyncio
|
| 637 |
-
async def
|
| 638 |
repository = FakeRepository()
|
| 639 |
whatsapp = FakeWhatsApp()
|
| 640 |
_driver_setup(repository)
|
|
@@ -648,10 +653,11 @@ async def test_initiate_trip_action_sends_whatsapp_list():
|
|
| 648 |
|
| 649 |
assert result.ok is True
|
| 650 |
assert result.data["count"] == 1
|
| 651 |
-
assert
|
| 652 |
-
assert len(whatsapp.
|
| 653 |
-
|
| 654 |
-
assert
|
|
|
|
| 655 |
|
| 656 |
|
| 657 |
@pytest.mark.asyncio
|
|
@@ -668,37 +674,6 @@ async def test_initiate_trip_action_returns_no_trips_message():
|
|
| 668 |
assert "No trips found" in result.data["message"]
|
| 669 |
|
| 670 |
|
| 671 |
-
@pytest.mark.asyncio
|
| 672 |
-
async def test_delete_trip_by_number_cancels_trip():
|
| 673 |
-
repository = FakeRepository()
|
| 674 |
-
_driver_setup(repository)
|
| 675 |
-
handlers = make_handlers(repository=repository, sender_phone="967700000010")
|
| 676 |
-
|
| 677 |
-
result = await handlers.delete_trip_by_number({"trip_number": 1})
|
| 678 |
-
|
| 679 |
-
assert result.ok is True
|
| 680 |
-
assert result.data["trip_number"] == 1
|
| 681 |
-
assert result.data["trip_id"] == "trip-1"
|
| 682 |
-
assert repository.trips_by_id["trip-1"]["status"] == "cancelled"
|
| 683 |
-
|
| 684 |
-
|
| 685 |
-
@pytest.mark.asyncio
|
| 686 |
-
async def test_modify_trip_by_number_updates_trip_field():
|
| 687 |
-
repository = FakeRepository()
|
| 688 |
-
_driver_setup(repository)
|
| 689 |
-
handlers = make_handlers(repository=repository, sender_phone="967700000010")
|
| 690 |
-
|
| 691 |
-
result = await handlers.modify_trip_by_number(
|
| 692 |
-
{"trip_number": 1, "field": "departure", "value": "تعز"}
|
| 693 |
-
)
|
| 694 |
-
|
| 695 |
-
assert result.ok is True
|
| 696 |
-
assert result.data["trip_number"] == 1
|
| 697 |
-
assert result.data["field"] == "departure"
|
| 698 |
-
assert result.data["value"] == "تعز"
|
| 699 |
-
assert repository.trips_by_id["trip-1"]["departure"] == "تعز"
|
| 700 |
-
|
| 701 |
-
|
| 702 |
@pytest.mark.asyncio
|
| 703 |
async def test_update_trip_field_uses_active_session_and_clears_it():
|
| 704 |
repository = FakeRepository()
|
|
|
|
| 12 |
customer: dict | None = None,
|
| 13 |
sender_phone: str = "967700000001",
|
| 14 |
) -> FalsaToolHandlers:
|
| 15 |
+
repo = repository or FakeRepository()
|
| 16 |
+
if customer is None:
|
| 17 |
+
cust = {"id": "cust-1", "remoteJid": sender_phone, "session_data": {}}
|
| 18 |
+
repo.customers_by_remote_jid[sender_phone] = cust
|
| 19 |
+
customer = cust
|
| 20 |
return FalsaToolHandlers(
|
| 21 |
+
repository=repo,
|
| 22 |
embeddings=embeddings or FakeEmbeddings(),
|
| 23 |
whatsapp=whatsapp or FakeWhatsApp(),
|
| 24 |
+
customer=customer,
|
| 25 |
remoteJid=sender_phone,
|
| 26 |
embedding_model="jina-embeddings-v5-text-small",
|
| 27 |
)
|
|
|
|
| 307 |
|
| 308 |
assert result.ok is True
|
| 309 |
assert repository.created_drivers[0]["customers"]["remoteJid"] == "967700000010"
|
| 310 |
+
assert result.data["driver_id"] == "driver-1"
|
| 311 |
|
| 312 |
|
| 313 |
@pytest.mark.asyncio
|
|
|
|
| 430 |
|
| 431 |
assert result.ok is True
|
| 432 |
assert result.data["count"] == 1
|
| 433 |
+
assert result.suppress_llm_reply is True
|
| 434 |
|
| 435 |
|
| 436 |
@pytest.mark.asyncio
|
|
|
|
| 617 |
|
| 618 |
|
| 619 |
def _driver_setup(repository: FakeRepository, *, phone: str = "967700000010") -> None:
|
| 620 |
+
repository.drivers_by_remote_jid[phone] = {
|
| 621 |
"id": "driver-1",
|
| 622 |
"name": "Ali",
|
| 623 |
"phone_number": phone,
|
|
|
|
| 639 |
|
| 640 |
|
| 641 |
@pytest.mark.asyncio
|
| 642 |
+
async def test_initiate_trip_action_sends_trip_cards():
|
| 643 |
repository = FakeRepository()
|
| 644 |
whatsapp = FakeWhatsApp()
|
| 645 |
_driver_setup(repository)
|
|
|
|
| 653 |
|
| 654 |
assert result.ok is True
|
| 655 |
assert result.data["count"] == 1
|
| 656 |
+
assert result.suppress_llm_reply is True
|
| 657 |
+
assert len(whatsapp.sent) == 2 # card + prompt
|
| 658 |
+
card_text = whatsapp.sent[0][1]
|
| 659 |
+
assert "عدن" in card_text
|
| 660 |
+
assert "المكلا" in card_text
|
| 661 |
|
| 662 |
|
| 663 |
@pytest.mark.asyncio
|
|
|
|
| 674 |
assert "No trips found" in result.data["message"]
|
| 675 |
|
| 676 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 677 |
@pytest.mark.asyncio
|
| 678 |
async def test_update_trip_field_uses_active_session_and_clears_it():
|
| 679 |
repository = FakeRepository()
|
tests/test_trip_selection.py
CHANGED
|
@@ -1,28 +1,27 @@
|
|
| 1 |
-
from app.whatsapp.trip_selection import
|
| 2 |
|
| 3 |
|
| 4 |
-
def
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
assert
|
| 25 |
-
|
| 26 |
-
assert "
|
| 27 |
-
assert "
|
| 28 |
-
assert "قائمة الرحلات" in body
|
|
|
|
| 1 |
+
from app.whatsapp.trip_selection import format_trip_card
|
| 2 |
|
| 3 |
|
| 4 |
+
def test_format_trip_card():
|
| 5 |
+
trip = {
|
| 6 |
+
"id": "trip-1",
|
| 7 |
+
"departure": "صنعاء",
|
| 8 |
+
"destination": "تعز",
|
| 9 |
+
"departure_date": "2026-12-01",
|
| 10 |
+
"departure_time": "noon",
|
| 11 |
+
"available_seats": 3,
|
| 12 |
+
"total_seats": 4,
|
| 13 |
+
"price": 5000,
|
| 14 |
+
"driver_cars": [{"car_type": "باص"}],
|
| 15 |
+
"drivers": [{"name": "أحمد"}],
|
| 16 |
+
"selection_count": 2,
|
| 17 |
+
}
|
| 18 |
+
card = format_trip_card(trip)
|
| 19 |
+
assert "صنعاء" in card
|
| 20 |
+
assert "تعز" in card
|
| 21 |
+
assert "2026-12-01" in card
|
| 22 |
+
assert "ظهرا" in card
|
| 23 |
+
assert "3 من 4" in card
|
| 24 |
+
assert "5000" in card
|
| 25 |
+
assert "باص" in card
|
| 26 |
+
assert "أحمد" in card
|
| 27 |
+
assert "2" in card
|
|
|
tests/test_webhooks.py
CHANGED
|
@@ -123,7 +123,7 @@ def test_admin_driver_debug_returns_llm_and_tool_results(settings):
|
|
| 123 |
)
|
| 124 |
|
| 125 |
class DummyConversation:
|
| 126 |
-
def _tool_registry(self, customer,
|
| 127 |
class DummyRegistry:
|
| 128 |
async def execute(self, name, arguments):
|
| 129 |
return ToolResult(ok=True, data={"name": name, "arguments": arguments})
|
|
|
|
| 123 |
)
|
| 124 |
|
| 125 |
class DummyConversation:
|
| 126 |
+
def _tool_registry(self, customer, *, remoteJid, user_mode="driver", current_message=None):
|
| 127 |
class DummyRegistry:
|
| 128 |
async def execute(self, name, arguments):
|
| 129 |
return ToolResult(ok=True, data={"name": name, "arguments": arguments})
|