Fix Arabic brand spelling by normalizing incorrect variants in AI replies and strengthen system prompt guidance
Browse files- app/ai/orchestrator.py +22 -1
- prompts/system.md +2 -1
- prompts/system_new_user.md +1 -1
- tests/test_ai_orchestrator.py +18 -0
app/ai/orchestrator.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
import json
|
| 2 |
import logging
|
|
|
|
| 3 |
from typing import Any
|
| 4 |
|
| 5 |
from app.ai.providers import (
|
|
@@ -120,7 +121,7 @@ class AIOrchestrator:
|
|
| 120 |
temperature=temperature,
|
| 121 |
)
|
| 122 |
if not response.tool_calls:
|
| 123 |
-
content = (response.content or "").strip()
|
| 124 |
if content:
|
| 125 |
return content
|
| 126 |
raise RetryableProviderError(f"{provider.name} returned an empty response")
|
|
@@ -163,6 +164,26 @@ def _assistant_tool_message(response: AIProviderResponse) -> dict[str, Any]:
|
|
| 163 |
}
|
| 164 |
|
| 165 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 166 |
async def _execute_tool_call(registry: ToolRegistry, tool_call: ToolCall) -> ToolResult:
|
| 167 |
try:
|
| 168 |
arguments = json.loads(tool_call.arguments or "{}")
|
|
|
|
| 1 |
import json
|
| 2 |
import logging
|
| 3 |
+
import re
|
| 4 |
from typing import Any
|
| 5 |
|
| 6 |
from app.ai.providers import (
|
|
|
|
| 121 |
temperature=temperature,
|
| 122 |
)
|
| 123 |
if not response.tool_calls:
|
| 124 |
+
content = _normalize_brand_name((response.content or "").strip())
|
| 125 |
if content:
|
| 126 |
return content
|
| 127 |
raise RetryableProviderError(f"{provider.name} returned an empty response")
|
|
|
|
| 164 |
}
|
| 165 |
|
| 166 |
|
| 167 |
+
def _normalize_brand_name(text: str) -> str:
|
| 168 |
+
if not text:
|
| 169 |
+
return text
|
| 170 |
+
|
| 171 |
+
replacements = {
|
| 172 |
+
"ููุณุง": "ููุฒุฉ",
|
| 173 |
+
"ููุธ": "ููุฒุฉ",
|
| 174 |
+
"ููุธุฉ": "ููุฒุฉ",
|
| 175 |
+
"ููุฒ": "ููุฒุฉ",
|
| 176 |
+
"ูููุฒุฉ": "ููุฒุฉ",
|
| 177 |
+
"ูููุฒุฉ": "ููุฒุฉ",
|
| 178 |
+
"ูููฐุฒุฉ": "ููุฒุฉ",
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
for wrong, correct in sorted(replacements.items(), key=lambda item: -len(item[0])):
|
| 182 |
+
pattern = rf"(?<![\w\u0600-\u06FF]){re.escape(wrong)}(?![\w\u0600-\u06FF])"
|
| 183 |
+
text = re.sub(pattern, correct, text)
|
| 184 |
+
return text
|
| 185 |
+
|
| 186 |
+
|
| 187 |
async def _execute_tool_call(registry: ToolRegistry, tool_call: ToolCall) -> ToolResult:
|
| 188 |
try:
|
| 189 |
arguments = json.loads(tool_call.arguments or "{}")
|
prompts/system.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
| 1 |
-
You are FALZH, a WhatsApp travel booking assistant.
|
| 2 |
|
| 3 |
Rules:
|
| 4 |
- Reply in sender's language (Arabic/English). Be concise, warm, practical.
|
|
|
|
| 5 |
- For new users: be extra warm and welcoming. Greet them, introduce FALZH briefly, and guide them to their first action.
|
| 6 |
- Use `about_falzh` for company/FAQ/policy/pricing/support.
|
| 7 |
- Tool args: Arabic for departure/destination/vehicle_type/time labels. English for digits/times.
|
|
|
|
| 1 |
+
You are FALZH | ููุฒุฉ, a WhatsApp travel booking assistant.
|
| 2 |
|
| 3 |
Rules:
|
| 4 |
- Reply in sender's language (Arabic/English). Be concise, warm, practical.
|
| 5 |
+
- When writing in Arabic, the brand name must always be spelled exactly "ููุฒุฉ" (ู-ู-ุฒ-ุง). Never use "ููุณุง", "ููุธ", "ููุธุฉ", "ููุฒ", or any other variant.
|
| 6 |
- For new users: be extra warm and welcoming. Greet them, introduce FALZH briefly, and guide them to their first action.
|
| 7 |
- Use `about_falzh` for company/FAQ/policy/pricing/support.
|
| 8 |
- Tool args: Arabic for departure/destination/vehicle_type/time labels. English for digits/times.
|
prompts/system_new_user.md
CHANGED
|
@@ -8,4 +8,4 @@ It is the sender's first message. Welcome them warmly and introduce FALZH with a
|
|
| 8 |
4. Use emojis (๐, ๐, etc.) and friendly tone. Be warm, not robotic.
|
| 9 |
5. Keep it short โ 4-5 lines max. Do not over-explain.
|
| 10 |
6. Never ask for name of a passenger. Only ask for name if they want to become a driver.
|
| 11 |
-
7. The brand name is ALWAYS "ููุฒุฉ" (ู-ู-ุฒ-ุง) โ never "ููุณุง" or any other variant.
|
|
|
|
| 8 |
4. Use emojis (๐, ๐, etc.) and friendly tone. Be warm, not robotic.
|
| 9 |
5. Keep it short โ 4-5 lines max. Do not over-explain.
|
| 10 |
6. Never ask for name of a passenger. Only ask for name if they want to become a driver.
|
| 11 |
+
7. The brand name is ALWAYS "ููุฒุฉ" (ู-ู-ุฒ-ุง) โ never "ููุณุง", "ููุธ", "ููุธุฉ", "ููุฒ", or any other variant.
|
tests/test_ai_orchestrator.py
CHANGED
|
@@ -109,6 +109,24 @@ async def test_ai_executes_tool_call_and_returns_final_response():
|
|
| 109 |
assert '"ok": true' in second_call_messages[-1]["content"]
|
| 110 |
|
| 111 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
@pytest.mark.asyncio
|
| 113 |
async def test_ai_reports_invalid_tool_arguments_to_model():
|
| 114 |
primary = ScriptedProvider(
|
|
|
|
| 109 |
assert '"ok": true' in second_call_messages[-1]["content"]
|
| 110 |
|
| 111 |
|
| 112 |
+
@pytest.mark.asyncio
|
| 113 |
+
async def test_ai_normalizes_incorrect_arabic_brand_spelling():
|
| 114 |
+
primary = ScriptedProvider(
|
| 115 |
+
"groq",
|
| 116 |
+
[AIProviderResponse(content="ุฃููุงู ุจู ูู ููุธ! ุฃุชู
ูู ุฃุญุฌุฒ ูู ุฑุญูุฉ.")],
|
| 117 |
+
)
|
| 118 |
+
orchestrator = AIOrchestrator(
|
| 119 |
+
primary=primary,
|
| 120 |
+
fallback=ScriptedProvider("openrouter", []),
|
| 121 |
+
temperature=0.2,
|
| 122 |
+
max_tool_iterations=3,
|
| 123 |
+
)
|
| 124 |
+
|
| 125 |
+
reply = await orchestrator.generate_reply(messages=[], tools=[], registry=ToolRegistry())
|
| 126 |
+
|
| 127 |
+
assert reply == "ุฃููุงู ุจู ูู ููุฒุฉ! ุฃุชู
ูู ุฃุญุฌุฒ ูู ุฑุญูุฉ."
|
| 128 |
+
|
| 129 |
+
|
| 130 |
@pytest.mark.asyncio
|
| 131 |
async def test_ai_reports_invalid_tool_arguments_to_model():
|
| 132 |
primary = ScriptedProvider(
|