feat: LLM-driven driver message cleanup for phone numbers and names
Browse files- Add driver_message parameter to add_trip_by_driver tool schema
- Update system_driver.md prompt to instruct LLM to clean messages
(remove phone numbers, surrounding text, and driver names)
- Update handler to use LLM-provided driver_message
- Add app/utils/phone.py with regex-based phone number detection
- Add tests/test_phone.py with 25 test cases
- Add test_add_trip_by_driver_uses_llm_cleaned_message test
- app/ai/tool_schemas.py +8 -0
- app/tools/handlers.py +2 -1
- app/utils/phone.py +29 -0
- prompts/system_driver.md +8 -0
- tests/test_phone.py +100 -0
- tests/test_tools.py +37 -0
app/ai/tool_schemas.py
CHANGED
|
@@ -243,6 +243,14 @@ _ADD_TRIP_BY_DRIVER = {
|
|
| 243 |
"minimum": 0,
|
| 244 |
"description": "Optional trip price.",
|
| 245 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 246 |
},
|
| 247 |
"required": ["departure", "destination", "departure_date", "departure_time"],
|
| 248 |
"additionalProperties": False,
|
|
|
|
| 243 |
"minimum": 0,
|
| 244 |
"description": "Optional trip price.",
|
| 245 |
},
|
| 246 |
+
"driver_message": {
|
| 247 |
+
"type": "string",
|
| 248 |
+
"description": (
|
| 249 |
+
"Optional cleaned version of the driver's original message. "
|
| 250 |
+
"Remove phone numbers, personal info, and driver name. "
|
| 251 |
+
"Keep emojis and decorations. Only set when >2 emojis."
|
| 252 |
+
),
|
| 253 |
+
},
|
| 254 |
},
|
| 255 |
"required": ["departure", "destination", "departure_date", "departure_time"],
|
| 256 |
"additionalProperties": False,
|
app/tools/handlers.py
CHANGED
|
@@ -587,7 +587,8 @@ class FalsaToolHandlers:
|
|
| 587 |
if self.current_message:
|
| 588 |
raw_msg = self.current_message.get("message") or ""
|
| 589 |
if count_emojis(raw_msg) > 2:
|
| 590 |
-
|
|
|
|
| 591 |
use_driver_message = True
|
| 592 |
|
| 593 |
trip = await self.repository.create_driver_trip(
|
|
|
|
| 587 |
if self.current_message:
|
| 588 |
raw_msg = self.current_message.get("message") or ""
|
| 589 |
if count_emojis(raw_msg) > 2:
|
| 590 |
+
llm_message = _optional_string(arguments.get("driver_message"))
|
| 591 |
+
driver_message = llm_message or raw_msg
|
| 592 |
use_driver_message = True
|
| 593 |
|
| 594 |
trip = await self.repository.create_driver_trip(
|
app/utils/phone.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import re
|
| 2 |
+
|
| 3 |
+
_PHONE_RE = re.compile(
|
| 4 |
+
r"(?:\+|00)\d[\d\s\-\.\(\)]{6,18}\d"
|
| 5 |
+
)
|
| 6 |
+
|
| 7 |
+
_LOCAL_PHONE_RE = re.compile(
|
| 8 |
+
r"(?<!\d)"
|
| 9 |
+
r"(?:\d[\d\s\-\.\(\)]{7,18}\d)"
|
| 10 |
+
r"(?!\d)"
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def count_digits(text: str) -> int:
|
| 15 |
+
return sum(1 for c in text if c.isdigit())
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def strip_phone_numbers(text: str, replacement: str = "[ุฑูู
ุงููุงุชู]") -> str:
|
| 19 |
+
if not text:
|
| 20 |
+
return text
|
| 21 |
+
|
| 22 |
+
def _replace(match: re.Match) -> str:
|
| 23 |
+
if count_digits(match.group()) >= 7:
|
| 24 |
+
return replacement
|
| 25 |
+
return match.group()
|
| 26 |
+
|
| 27 |
+
result = _PHONE_RE.sub(_replace, text)
|
| 28 |
+
result = _LOCAL_PHONE_RE.sub(_replace, result)
|
| 29 |
+
return result
|
prompts/system_driver.md
CHANGED
|
@@ -8,3 +8,11 @@
|
|
| 8 |
- For delete/modify: initiate_trip_action (sends numbered list). Driver replies with number.
|
| 9 |
- delete_trip_by_number(trip_number) / modify_trip_by_number(trip_number, field, value). No trip_id.
|
| 10 |
- To search/book as traveler: switch_to_passenger (name optional).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
- For delete/modify: initiate_trip_action (sends numbered list). Driver replies with number.
|
| 9 |
- delete_trip_by_number(trip_number) / modify_trip_by_number(trip_number, field, value). No trip_id.
|
| 10 |
- To search/book as traveler: switch_to_passenger (name optional).
|
| 11 |
+
|
| 12 |
+
Driver message cleanup:
|
| 13 |
+
- When the driver's message has more than 2 emojis, set `driver_message` in add_trip_by_driver.
|
| 14 |
+
- Remove ALL phone numbers AND the surrounding text (e.g. "ููุชูุงุตู ุงูุงุชุตุงู ุนูู ุงูุฑูู
ุงูุขุชู๐:-770026665" โ remove entirely).
|
| 15 |
+
- Remove the driver's name wherever it appears in the message.
|
| 16 |
+
- Keep emojis, decorative elements, route info, dates, times, and seat info.
|
| 17 |
+
- Example: "๐ุจุต ููุฏ ู
ุฑูุญ ๐. ูุงุฒู ุงูุฌู
ุนุฉ ูง ุตุจุงุญุง ๐ุชุฑูู
ุงูู
ููุง๐ ุจู ุจูุฑ ๐770026665 ๐ุจู ุจูุฑ๐"
|
| 18 |
+
โ "๐ุจุต ููุฏ ู
ุฑูุญ ๐. ูุงุฒู ุงูุฌู
ุนุฉ ูง ุตุจุงุญุง ๐ุชุฑูู
ุงูู
ููุง๐ ๐ซต๐ป ๐ ๐"
|
tests/test_phone.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from app.utils.phone import count_digits, strip_phone_numbers
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
class TestCountDigits:
|
| 5 |
+
def test_no_digits(self):
|
| 6 |
+
assert count_digits("hello world") == 0
|
| 7 |
+
|
| 8 |
+
def test_all_digits(self):
|
| 9 |
+
assert count_digits("1234567") == 7
|
| 10 |
+
|
| 11 |
+
def test_mixed(self):
|
| 12 |
+
assert count_digits("abc123def456") == 6
|
| 13 |
+
|
| 14 |
+
def test_empty(self):
|
| 15 |
+
assert count_digits("") == 0
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class TestStripPhoneNumbers:
|
| 19 |
+
def test_yemeni_number_with_plus(self):
|
| 20 |
+
result = strip_phone_numbers("ุงุชุตู ุนูู +967712345678")
|
| 21 |
+
assert result == "ุงุชุตู ุนูู [ุฑูู
ุงููุงุชู]"
|
| 22 |
+
|
| 23 |
+
def test_yemeni_number_with_00(self):
|
| 24 |
+
result = strip_phone_numbers("ุงุชุตู ุนูู 00967712345678")
|
| 25 |
+
assert result == "ุงุชุตู ุนูู [ุฑูู
ุงููุงุชู]"
|
| 26 |
+
|
| 27 |
+
def test_international_with_dashes(self):
|
| 28 |
+
result = strip_phone_numbers("Call +1-234-567-8901")
|
| 29 |
+
assert result == "Call [ุฑูู
ุงููุงุชู]"
|
| 30 |
+
|
| 31 |
+
def test_international_with_spaces(self):
|
| 32 |
+
result = strip_phone_numbers("Call +44 20 7946 0958")
|
| 33 |
+
assert result == "Call [ุฑูู
ุงููุงุชู]"
|
| 34 |
+
|
| 35 |
+
def test_international_with_dots(self):
|
| 36 |
+
result = strip_phone_numbers("Call +49.30.1234567")
|
| 37 |
+
assert result == "Call [ุฑูู
ุงููุงุชู]"
|
| 38 |
+
|
| 39 |
+
def test_international_with_parentheses(self):
|
| 40 |
+
result = strip_phone_numbers("Call +1 (234) 567-8901")
|
| 41 |
+
assert result == "Call [ุฑูู
ุงููุงุชู]"
|
| 42 |
+
|
| 43 |
+
def test_multiple_numbers(self):
|
| 44 |
+
result = strip_phone_numbers("Call +967712345678 or +967776543210")
|
| 45 |
+
assert result == "Call [ุฑูู
ุงููุงุชู] or [ุฑูู
ุงููุงุชู]"
|
| 46 |
+
|
| 47 |
+
def test_no_phone_numbers(self):
|
| 48 |
+
text = "ุฑุญูุฉ ู
ู ุตูุนุงุก ุฅูู ุชุนุฒ"
|
| 49 |
+
result = strip_phone_numbers(text)
|
| 50 |
+
assert result == text
|
| 51 |
+
|
| 52 |
+
def test_short_number_not_matched(self):
|
| 53 |
+
result = strip_phone_numbers("Room 123")
|
| 54 |
+
assert result == "Room 123"
|
| 55 |
+
|
| 56 |
+
def test_empty_string(self):
|
| 57 |
+
assert strip_phone_numbers("") == ""
|
| 58 |
+
|
| 59 |
+
def test_none_returns_none(self):
|
| 60 |
+
assert strip_phone_numbers(None) is None
|
| 61 |
+
|
| 62 |
+
def test_custom_replacement(self):
|
| 63 |
+
result = strip_phone_numbers("Call +967712345678", replacement="***")
|
| 64 |
+
assert result == "Call ***"
|
| 65 |
+
|
| 66 |
+
def test_number_at_start(self):
|
| 67 |
+
result = strip_phone_numbers("+967712345678 ุงุชุตู")
|
| 68 |
+
assert result == "[ุฑูู
ุงููุงุชู] ุงุชุตู"
|
| 69 |
+
|
| 70 |
+
def test_number_at_end(self):
|
| 71 |
+
result = strip_phone_numbers("ุงุชุตู +967712345678")
|
| 72 |
+
assert result == "ุงุชุตู [ุฑูู
ุงููุงุชู]"
|
| 73 |
+
|
| 74 |
+
def test_number_in_middle_of_text(self):
|
| 75 |
+
result = strip_phone_numbers("ุงุชุตู ุนูู +967712345678 ููุญุฌุฒ")
|
| 76 |
+
assert result == "ุงุชุตู ุนูู [ุฑูู
ุงููุงุชู] ููุญุฌุฒ"
|
| 77 |
+
|
| 78 |
+
def test_yemeni_local_format(self):
|
| 79 |
+
result = strip_phone_numbers("0712345678")
|
| 80 |
+
assert result == "[ุฑูู
ุงููุงุชู]"
|
| 81 |
+
|
| 82 |
+
def test_number_with_country_code_no_plus(self):
|
| 83 |
+
result = strip_phone_numbers("967712345678")
|
| 84 |
+
assert result == "[ุฑูู
ุงููุงุชู]"
|
| 85 |
+
|
| 86 |
+
def test_long_number_with_separators(self):
|
| 87 |
+
result = strip_phone_numbers("+1 (555) 123-4567 ext 890")
|
| 88 |
+
assert result == "[ุฑูู
ุงููุงุชู] ext 890"
|
| 89 |
+
|
| 90 |
+
def test_local_yemeni_9_digit(self):
|
| 91 |
+
result = strip_phone_numbers("770026665")
|
| 92 |
+
assert result == "[ุฑูู
ุงููุงุชู]"
|
| 93 |
+
|
| 94 |
+
def test_local_yemeni_in_message(self):
|
| 95 |
+
result = strip_phone_numbers("ููุชูุงุตู ุนูู ุงูุฑูู
770026665")
|
| 96 |
+
assert result == "ููุชูุงุตู ุนูู ุงูุฑูู
[ุฑูู
ุงููุงุชู]"
|
| 97 |
+
|
| 98 |
+
def test_local_yemeni_with_dash_prefix(self):
|
| 99 |
+
result = strip_phone_numbers("ุงูุฑูู
:-770026665")
|
| 100 |
+
assert result == "ุงูุฑูู
:-[ุฑูู
ุงููุงุชู]"
|
tests/test_tools.py
CHANGED
|
@@ -625,6 +625,43 @@ async def test_add_trip_by_driver_stores_driver_message_with_emojis():
|
|
| 625 |
assert created_trip["driver_message"] == "ุจุงุต ู
ู ุนุฏู ุงูู ุงูู
ููุง ๐๐๐"
|
| 626 |
|
| 627 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 628 |
@pytest.mark.asyncio
|
| 629 |
async def test_add_trip_by_driver_no_driver_message_with_few_emojis():
|
| 630 |
repository = FakeRepository()
|
|
|
|
| 625 |
assert created_trip["driver_message"] == "ุจุงุต ู
ู ุนุฏู ุงูู ุงูู
ููุง ๐๐๐"
|
| 626 |
|
| 627 |
|
| 628 |
+
@pytest.mark.asyncio
|
| 629 |
+
async def test_add_trip_by_driver_uses_llm_cleaned_message():
|
| 630 |
+
repository = FakeRepository()
|
| 631 |
+
repository.drivers_by_remote_jid["967700000010"] = {
|
| 632 |
+
"id": "driver-1",
|
| 633 |
+
"name": "Ali",
|
| 634 |
+
"remoteJid": "967700000010",
|
| 635 |
+
}
|
| 636 |
+
repository.driver_cars_by_driver["driver-1"] = [
|
| 637 |
+
{"id": "car-1", "car_type": "SUV", "seat_count": 4},
|
| 638 |
+
]
|
| 639 |
+
raw_msg = "๐ุจุต ููุฏ ู
ุฑูุญ ๐. ูุงุฒู ุงูุฌู
ุนุฉ ูง ุตุจุงุญุง ๐ุชุฑูู
ุงูู
ููุง๐ ุจู ุจูุฑ ๐770026665 ๐ุจู ุจูุฑ๐"
|
| 640 |
+
cleaned_msg = "๐ุจุต ููุฏ ู
ุฑูุญ ๐. ูุงุฒู ุงูุฌู
ุนุฉ ูง ุตุจุงุญุง ๐ุชุฑูู
ุงูู
ููุง๐ ๐"
|
| 641 |
+
current_message = {"message": raw_msg}
|
| 642 |
+
handlers = make_handlers(repository=repository, sender_phone="967700000010")
|
| 643 |
+
handlers.current_message = current_message
|
| 644 |
+
|
| 645 |
+
result = await handlers.add_trip_by_driver(
|
| 646 |
+
{
|
| 647 |
+
"departure": "ุชุฑูู
",
|
| 648 |
+
"destination": "ุงูู
ููุง",
|
| 649 |
+
"departure_date": "2026-06-10",
|
| 650 |
+
"departure_time": "morning",
|
| 651 |
+
"vehicle_type": "SUV",
|
| 652 |
+
"available_seats": 2,
|
| 653 |
+
"total_seats": 4,
|
| 654 |
+
"price": 50,
|
| 655 |
+
"driver_message": cleaned_msg,
|
| 656 |
+
}
|
| 657 |
+
)
|
| 658 |
+
|
| 659 |
+
assert result.ok is True
|
| 660 |
+
created_trip = repository.created_trips[-1]
|
| 661 |
+
assert created_trip["use_driver_message"] is True
|
| 662 |
+
assert created_trip["driver_message"] == cleaned_msg
|
| 663 |
+
|
| 664 |
+
|
| 665 |
@pytest.mark.asyncio
|
| 666 |
async def test_add_trip_by_driver_no_driver_message_with_few_emojis():
|
| 667 |
repository = FakeRepository()
|