feat: welcome returning drivers discovered from WhatsApp groups
Browse files- Detect unregistered drivers (from group trips) who later DM the bot
- Send personalized welcome with their name and explanation of FALSA benefits
- Upgrade user_mode from new_user to driver automatically
- Improve driver phone lookup to handle /-separated numbers
app/database/supabase.py
CHANGED
|
@@ -631,6 +631,18 @@ class SupabaseRepository:
|
|
| 631 |
.maybe_single()
|
| 632 |
.execute()
|
| 633 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 634 |
return _response_data(response)
|
| 635 |
|
| 636 |
async def create_driver(self, *, customer_id: str) -> dict[str, Any]:
|
|
|
|
| 631 |
.maybe_single()
|
| 632 |
.execute()
|
| 633 |
)
|
| 634 |
+
result = _response_data(response)
|
| 635 |
+
if result:
|
| 636 |
+
return result
|
| 637 |
+
|
| 638 |
+
# Check if any stored customer has a /-separated phone_number containing our phone
|
| 639 |
+
response = await (
|
| 640 |
+
self.client.table("drivers")
|
| 641 |
+
.select("*, customers!inner(*)")
|
| 642 |
+
.like("customers.phone_number", f"%{phone}%")
|
| 643 |
+
.maybe_single()
|
| 644 |
+
.execute()
|
| 645 |
+
)
|
| 646 |
return _response_data(response)
|
| 647 |
|
| 648 |
async def create_driver(self, *, customer_id: str) -> dict[str, Any]:
|
app/services/conversation_service.py
CHANGED
|
@@ -75,6 +75,16 @@ class ConversationService:
|
|
| 75 |
)
|
| 76 |
|
| 77 |
user_mode = _resolve_user_mode(customer)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
registry = self._tool_registry(
|
| 79 |
customer,
|
| 80 |
remoteJid=inbound.remoteJid,
|
|
@@ -185,11 +195,35 @@ class ConversationService:
|
|
| 185 |
)
|
| 186 |
return reply
|
| 187 |
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 193 |
|
| 194 |
if not reply:
|
| 195 |
return reply
|
|
@@ -203,6 +237,12 @@ class ConversationService:
|
|
| 203 |
metadata={"provider_flow": "groq_primary_hf_fallback", "user_mode": user_mode},
|
| 204 |
)
|
| 205 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 206 |
return reply
|
| 207 |
|
| 208 |
async def _store_and_send_assistant_reply(
|
|
|
|
| 75 |
)
|
| 76 |
|
| 77 |
user_mode = _resolve_user_mode(customer)
|
| 78 |
+
|
| 79 |
+
is_returning_driver = False
|
| 80 |
+
if user_mode == "new_user" and customer.get("phone_number"):
|
| 81 |
+
existing_driver = await self.repository.get_driver_by_phone_number(
|
| 82 |
+
customer["phone_number"],
|
| 83 |
+
)
|
| 84 |
+
if existing_driver:
|
| 85 |
+
is_returning_driver = True
|
| 86 |
+
user_mode = "driver"
|
| 87 |
+
|
| 88 |
registry = self._tool_registry(
|
| 89 |
customer,
|
| 90 |
remoteJid=inbound.remoteJid,
|
|
|
|
| 195 |
)
|
| 196 |
return reply
|
| 197 |
|
| 198 |
+
if is_returning_driver:
|
| 199 |
+
driver_name = customer.get("name") or ""
|
| 200 |
+
system_note = (
|
| 201 |
+
f"SYSTEM: This is the first message from driver \"{driver_name}\". "
|
| 202 |
+
"They were previously tracked from WhatsApp group trip posts. "
|
| 203 |
+
"Welcome them warmly by name. Tell them we have been following their trips "
|
| 204 |
+
"in the groups and we are impressed. Explain that we have registered them in "
|
| 205 |
+
"FALSA so they can now send trips directly here instead of posting in groups. "
|
| 206 |
+
"Show them how: just send the trip details (route, date, time) in chat. "
|
| 207 |
+
"Explain the benefits: passengers find their trips via AI search, they get "
|
| 208 |
+
"notified immediately when a passenger selects their trip, and registered "
|
| 209 |
+
"drivers get priority visibility in search results. Tell them we will no "
|
| 210 |
+
"longer add their trips from groups — they are in full control now. "
|
| 211 |
+
"Keep it warm, personal, and exciting. Use emojis. Write in Arabic. "
|
| 212 |
+
"8-10 lines max. Do NOT call any tools."
|
| 213 |
+
)
|
| 214 |
+
messages = self._ai_messages(context, user_mode=user_mode)
|
| 215 |
+
messages.append({"role": "system", "content": system_note})
|
| 216 |
+
reply = await self.ai.generate_reply(
|
| 217 |
+
messages=messages,
|
| 218 |
+
tools=get_tool_schemas(user_mode),
|
| 219 |
+
registry=registry,
|
| 220 |
+
)
|
| 221 |
+
else:
|
| 222 |
+
reply = await self.ai.generate_reply(
|
| 223 |
+
messages=self._ai_messages(context, user_mode=user_mode),
|
| 224 |
+
tools=get_tool_schemas(user_mode),
|
| 225 |
+
registry=registry,
|
| 226 |
+
)
|
| 227 |
|
| 228 |
if not reply:
|
| 229 |
return reply
|
|
|
|
| 237 |
metadata={"provider_flow": "groq_primary_hf_fallback", "user_mode": user_mode},
|
| 238 |
)
|
| 239 |
|
| 240 |
+
if is_returning_driver:
|
| 241 |
+
await self.repository.update_customer_user_mode(
|
| 242 |
+
customer_id=str(customer["id"]),
|
| 243 |
+
user_mode="driver",
|
| 244 |
+
)
|
| 245 |
+
|
| 246 |
return reply
|
| 247 |
|
| 248 |
async def _store_and_send_assistant_reply(
|
tests/test_conversation_service.py
CHANGED
|
@@ -120,6 +120,74 @@ async def test_conversation_uses_passenger_tools_when_user_mode_is_passenger(set
|
|
| 120 |
assert "travel booking assistant" in ai.calls[0]["messages"][0]["content"]
|
| 121 |
|
| 122 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
@pytest.mark.asyncio
|
| 124 |
async def test_conversation_uses_driver_tools_when_user_mode_is_driver(settings):
|
| 125 |
repository = FakeRepository()
|
|
|
|
| 120 |
assert "travel booking assistant" in ai.calls[0]["messages"][0]["content"]
|
| 121 |
|
| 122 |
|
| 123 |
+
@pytest.mark.asyncio
|
| 124 |
+
async def test_returning_driver_gets_welcome_and_upgraded_to_driver(settings):
|
| 125 |
+
"""An unregistered driver saved from group trips who later DMs the bot
|
| 126 |
+
should get a personalized welcome system note, use driver tools, and
|
| 127 |
+
have their user_mode upgraded to 'driver'."""
|
| 128 |
+
repository = FakeRepository()
|
| 129 |
+
|
| 130 |
+
# Step 1: Simulate group-trip extraction creating an unregistered driver
|
| 131 |
+
customer = await repository.upsert_customer(
|
| 132 |
+
remote_jid=None,
|
| 133 |
+
name="فهد",
|
| 134 |
+
phone_number="967712345678",
|
| 135 |
+
registered=False,
|
| 136 |
+
)
|
| 137 |
+
await repository.create_driver(customer_id=customer["id"])
|
| 138 |
+
|
| 139 |
+
assert customer["user_mode"] is None
|
| 140 |
+
assert customer["remoteJid"] is None
|
| 141 |
+
|
| 142 |
+
# Step 2: Same driver sends a DM
|
| 143 |
+
ai = FakeAI(reply="مرحباً فهد! نتابع رحلاتك...")
|
| 144 |
+
whatsapp = FakeWhatsApp()
|
| 145 |
+
service = ConversationService(
|
| 146 |
+
repository=repository,
|
| 147 |
+
embeddings=FakeEmbeddings(),
|
| 148 |
+
whatsapp=whatsapp,
|
| 149 |
+
ai=ai,
|
| 150 |
+
settings=settings,
|
| 151 |
+
)
|
| 152 |
+
|
| 153 |
+
reply = await service.handle_inbound_message(
|
| 154 |
+
WhatsAppInboundMessage(
|
| 155 |
+
message_id="wamid.returning",
|
| 156 |
+
remoteJid="967712345678",
|
| 157 |
+
text="مرحبا، أبي أضيف رحلة",
|
| 158 |
+
phone_number="967712345678",
|
| 159 |
+
profile_name="فهد",
|
| 160 |
+
)
|
| 161 |
+
)
|
| 162 |
+
|
| 163 |
+
assert reply == "مرحباً فهد! نتابع رحلاتك..."
|
| 164 |
+
|
| 165 |
+
# Should use driver tools (not new_user tools)
|
| 166 |
+
tool_names = {tool["function"]["name"] for tool in ai.calls[0]["tools"]}
|
| 167 |
+
assert tool_names == {
|
| 168 |
+
"about_falsa",
|
| 169 |
+
"check_driver_info",
|
| 170 |
+
"check_driver_trips",
|
| 171 |
+
"add_driver_car",
|
| 172 |
+
"add_trip_by_driver",
|
| 173 |
+
"initiate_trip_action",
|
| 174 |
+
"update_trip_field",
|
| 175 |
+
"switch_to_passenger",
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
# System note should be appended with returning-driver context
|
| 179 |
+
ai_messages = ai.calls[0]["messages"]
|
| 180 |
+
system_contents = [m["content"] for m in ai_messages if m["role"] == "system"]
|
| 181 |
+
assert any("فهد" in msg for msg in system_contents)
|
| 182 |
+
assert any("group" in msg.lower() or "مجموعات" in msg for msg in system_contents)
|
| 183 |
+
|
| 184 |
+
# user_mode should be upgraded to 'driver'
|
| 185 |
+
assert customer["user_mode"] == "driver"
|
| 186 |
+
|
| 187 |
+
# WhatsApp message was sent
|
| 188 |
+
assert len(whatsapp.sent) == 1
|
| 189 |
+
|
| 190 |
+
|
| 191 |
@pytest.mark.asyncio
|
| 192 |
async def test_conversation_uses_driver_tools_when_user_mode_is_driver(settings):
|
| 193 |
repository = FakeRepository()
|