codeBOKER commited on
Commit
1b62af7
·
1 Parent(s): ee216a0

suppress LLM text reply after search_trips sends trip cards

Browse files

- Add suppress_llm_reply flag to ToolResult (not serialized to LLM)
- search_trips handler now sends a 'choose a card' prompt and sets
suppress_llm_reply=True when trips are found
- Orchestrator returns empty string immediately when flag is set,
avoiding a second LLM call
- Guard conversation service against sending empty replies
- Update system prompt and tool schema to reflect new behavior

app/ai/orchestrator.py CHANGED
@@ -7,7 +7,7 @@ from app.ai.providers import (
7
  InvalidToolCallGenerationError,
8
  RetryableProviderError,
9
  )
10
- from app.models.domain import AIProviderResponse, ToolCall
11
  from app.tools.registry import ToolRegistry
12
 
13
  logger = logging.getLogger(__name__)
@@ -93,12 +93,14 @@ class AIOrchestrator:
93
  logger.warning("++++++++"+str(tool_call)+ "&&&"+ str(registry))
94
  result = await _execute_tool_call(registry, tool_call)
95
  logger.warning("+++++++++++++"+str(result))
 
 
96
  working_messages.append(
97
  {
98
  "role": "tool",
99
  "tool_call_id": tool_call.id,
100
  "name": tool_call.name,
101
- "content": json.dumps(result, ensure_ascii=False),
102
  }
103
  )
104
 
@@ -125,12 +127,12 @@ def _assistant_tool_message(response: AIProviderResponse) -> dict[str, Any]:
125
  }
126
 
127
 
128
- async def _execute_tool_call(registry: ToolRegistry, tool_call: ToolCall) -> dict[str, Any]:
129
  try:
130
  arguments = json.loads(tool_call.arguments or "{}")
131
  if not isinstance(arguments, dict):
132
  raise ValueError("Tool arguments must be a JSON object")
133
  except (json.JSONDecodeError, ValueError) as exc:
134
- return {"ok": False, "error": f"Invalid tool arguments: {exc}", "data": {}}
135
 
136
- return (await registry.execute(tool_call.name, arguments)).to_payload()
 
7
  InvalidToolCallGenerationError,
8
  RetryableProviderError,
9
  )
10
+ from app.models.domain import AIProviderResponse, ToolCall, ToolResult
11
  from app.tools.registry import ToolRegistry
12
 
13
  logger = logging.getLogger(__name__)
 
93
  logger.warning("++++++++"+str(tool_call)+ "&&&"+ str(registry))
94
  result = await _execute_tool_call(registry, tool_call)
95
  logger.warning("+++++++++++++"+str(result))
96
+ if result.suppress_llm_reply:
97
+ return ""
98
  working_messages.append(
99
  {
100
  "role": "tool",
101
  "tool_call_id": tool_call.id,
102
  "name": tool_call.name,
103
+ "content": json.dumps(result.to_payload(), ensure_ascii=False),
104
  }
105
  )
106
 
 
127
  }
128
 
129
 
130
+ async def _execute_tool_call(registry: ToolRegistry, tool_call: ToolCall) -> ToolResult:
131
  try:
132
  arguments = json.loads(tool_call.arguments or "{}")
133
  if not isinstance(arguments, dict):
134
  raise ValueError("Tool arguments must be a JSON object")
135
  except (json.JSONDecodeError, ValueError) as exc:
136
+ return ToolResult(ok=False, data={}, error=f"Invalid tool arguments: {exc}")
137
 
138
+ return await registry.execute(tool_call.name, arguments)
app/ai/tool_schemas.py CHANGED
@@ -37,8 +37,8 @@ _SEARCH_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
  ),
43
  "parameters": {
44
  "type": "object",
 
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
+ "Matching trips are sent as WhatsApp cards and user is prompted to pick one. "
41
+ "Do NOT add follow-up text response is handled automatically."
42
  ),
43
  "parameters": {
44
  "type": "object",
app/models/domain.py CHANGED
@@ -39,6 +39,7 @@ class ToolResult:
39
  ok: bool
40
  data: dict[str, Any]
41
  error: str | None = None
 
42
 
43
  def to_payload(self) -> dict[str, Any]:
44
  payload: dict[str, Any] = {"ok": self.ok, "data": self.data}
 
39
  ok: bool
40
  data: dict[str, Any]
41
  error: str | None = None
42
+ suppress_llm_reply: bool = False
43
 
44
  def to_payload(self) -> dict[str, Any]:
45
  payload: dict[str, Any] = {"ok": self.ok, "data": self.data}
app/services/conversation_service.py CHANGED
@@ -115,6 +115,9 @@ class ConversationService:
115
  registry=registry,
116
  )
117
 
 
 
 
118
  await self.whatsapp.send_text(inbound.remoteJid, reply)
119
 
120
  await self.repository.create_message(
 
115
  registry=registry,
116
  )
117
 
118
+ if not reply:
119
+ return reply
120
+
121
  await self.whatsapp.send_text(inbound.remoteJid, reply)
122
 
123
  await self.repository.create_message(
app/tools/handlers.py CHANGED
@@ -182,19 +182,35 @@ class FalsaToolHandlers:
182
  except WhatsAppClientError:
183
  logger.warning("Failed to send trip card for trip %s", trip_id)
184
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
  return ToolResult(
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": (
193
- "No active matching trips were found."
194
- if not top_trips
195
- else "Trips were sent as separate WhatsApp messages. "
196
- "Ask the user to reply to a trip card to select it."
197
- ),
198
  },
199
  )
200
 
 
182
  except WhatsAppClientError:
183
  logger.warning("Failed to send trip card for trip %s", trip_id)
184
 
185
+ prompt = "يرجى الرد على إحدى بطاقات الرحلات أعلاه لاختيار رحلتك"
186
+ await self.whatsapp.send_text(self.remoteJid, prompt)
187
+ await self.repository.create_message(
188
+ customer_id=str(self.customer["id"]),
189
+ sender_type="assistant",
190
+ message=prompt,
191
+ metadata={"type": "trip_selection_prompt"},
192
+ )
193
+
194
+ return ToolResult(
195
+ ok=True,
196
+ data={
197
+ "count": len(top_trips),
198
+ "matches": top_trips,
199
+ "alternate_alert": alternate_alert,
200
+ "sent_as_messages": True,
201
+ "note": "Cards sent. No text reply needed.",
202
+ },
203
+ suppress_llm_reply=True,
204
+ )
205
+
206
  return ToolResult(
207
  ok=True,
208
  data={
209
+ "count": 0,
210
+ "matches": [],
211
  "alternate_alert": alternate_alert,
212
+ "sent_as_messages": False,
213
+ "note": "No active matching trips were found.",
 
 
 
 
 
214
  },
215
  )
216
 
prompts/system_passenger.md CHANGED
@@ -1,5 +1,5 @@
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.
 
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 finds matches, trip cards are sent to the user automatically along with a prompt to choose one. Do NOT add any text after calling search_trips the tool will handle the response.
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.