Toadoum commited on
Commit
7b36576
·
verified ·
1 Parent(s): 31ff13b

Update dialogue.py

Browse files
Files changed (1) hide show
  1. dialogue.py +90 -23
dialogue.py CHANGED
@@ -3,12 +3,29 @@ PlotWeaver Voice Agent — Dialogue Manager
3
  ==========================================
4
  FSM for multi-turn Hausa conversations across 3 verticals.
5
  State lives in Gradio session state (dict) — no Redis needed in the Space.
 
 
 
 
 
 
 
 
 
6
  """
7
  from __future__ import annotations
 
 
8
  from dataclasses import dataclass, field, asdict
9
  from enum import Enum
10
  from typing import Optional
11
 
 
 
 
 
 
 
12
 
13
  class Vertical(str, Enum):
14
  BANK = "bank"
@@ -53,8 +70,9 @@ SCENARIOS = {
53
  "transitions": {"provide_digits": "return_balance"},
54
  },
55
  "return_balance": {
56
- "ha": "Ma'aunin asusunka shine Naira dubu ɗari biyu da arba'in da biyar. Akwai wani abu?",
57
- "en": "Your account balance is two hundred forty-five thousand Naira. Anything else?",
 
58
  "expects": "yesno",
59
  "transitions": {"yes": "greeting", "no": "exit"},
60
  },
@@ -65,8 +83,9 @@ SCENARIOS = {
65
  "transitions": {"yes": "card_blocked", "no": "greeting"},
66
  },
67
  "card_blocked": {
68
- "ha": "An toshe katinka. Sabon kati zai iso a cikin kwanaki uku zuwa biyar. Ana juya ka ga wakili don tabbatar.",
69
- "en": "Your card is blocked. A new card will arrive in 3-5 days. Transferring you to an agent for confirmation.",
 
70
  "expects": None, "terminal": True, "escalate": True,
71
  },
72
  "ask_recipient": {
@@ -82,14 +101,15 @@ SCENARIOS = {
82
  "transitions": {"provide_amount": "confirm_transfer"},
83
  },
84
  "confirm_transfer": {
85
- "ha": "Zan tura kuɗin yanzu. Ka ce 'i' don ci gaba.",
86
- "en": "I'll send the money now. Say 'yes' to continue.",
87
  "expects": "yesno",
88
  "transitions": {"yes": "transfer_done", "no": "greeting"},
89
  },
90
  "transfer_done": {
91
- "ha": "An tura kuɗin. Godiya da zabar PlotWeaver Bank.",
92
- "en": "Money sent. Thank you for choosing PlotWeaver Bank.",
 
93
  "expects": None, "terminal": True,
94
  },
95
  },
@@ -110,8 +130,9 @@ SCENARIOS = {
110
  "transitions": {"provide_amount": "airtime_done"},
111
  },
112
  "airtime_done": {
113
- "ha": "An kara airtime. Ma'aunin ka sabo shine Naira dubu ɗaya da ɗari biyar.",
114
- "en": "Airtime loaded. Your new balance is 1500 Naira.",
 
115
  "expects": None, "terminal": True,
116
  },
117
  "ask_bundle_type": {
@@ -121,8 +142,9 @@ SCENARIOS = {
121
  "transitions": {"provide_bundle": "bundle_done"},
122
  },
123
  "bundle_done": {
124
- "ha": "An kunna bundle ɗinka. Za ka iya yin amfani da shi yanzu.",
125
- "en": "Your bundle is active. You can use it now.",
 
126
  "expects": None, "terminal": True,
127
  },
128
  "ask_complaint": {
@@ -132,8 +154,9 @@ SCENARIOS = {
132
  "transitions": {"provide_text": "escalate"},
133
  },
134
  "escalate": {
135
- "ha": "Nagode. Zan juya ka ga wakili na mutum yanzu.",
136
- "en": "Thank you. I'll transfer you to a human agent now.",
 
137
  "expects": None, "terminal": True, "escalate": True,
138
  },
139
  },
@@ -154,8 +177,9 @@ SCENARIOS = {
154
  "transitions": {"provide_digits": "order_status"},
155
  },
156
  "order_status": {
157
- "ha": "Oda ɗinka yana kan hanya. Za a isar gobe da yamma.",
158
- "en": "Your order is on the way. It will be delivered tomorrow evening.",
 
159
  "expects": None, "terminal": True,
160
  },
161
  "ask_order_id_reschedule": {
@@ -171,8 +195,9 @@ SCENARIOS = {
171
  "transitions": {"provide_date": "reschedule_done"},
172
  },
173
  "reschedule_done": {
174
- "ha": "An sake tsara isar. Za ka sami SMS na tabbatarwa.",
175
- "en": "Delivery rescheduled. You'll receive a confirmation SMS.",
 
176
  "expects": None, "terminal": True,
177
  },
178
  "ask_order_id_return": {
@@ -188,8 +213,9 @@ SCENARIOS = {
188
  "transitions": {"provide_reason": "return_done"},
189
  },
190
  "return_done": {
191
- "ha": "An karɓi buƙatarka. Wakili zai tattara kaya a gobe.",
192
- "en": "Your request is received. An agent will collect the item tomorrow.",
 
193
  "expects": None, "terminal": True,
194
  },
195
  },
@@ -198,8 +224,7 @@ SCENARIOS = {
198
 
199
 
200
  # Vertical-specific fallback prompts — spoken when the user asks something
201
- # out of scope. Each prompt: (1) acknowledges confusion, (2) lists the
202
- # services this bot CAN perform in that vertical, (3) offers human agent.
203
  FALLBACK_PROMPTS = {
204
  "bank": {
205
  "ha": "Ban fahimci tambayarka ba. A nan, zan iya taimake ka da 'duba ma'auni', 'toshe kati', ko 'canjin kuɗi'. Don wasu tambayoyi, ka ce 'wakili' don yin magana da mutum.",
@@ -219,7 +244,16 @@ FALLBACK_PROMPTS = {
219
  MAX_CONSECUTIVE_UNKNOWNS = 2
220
 
221
 
 
 
 
 
 
 
 
 
222
  def get_prompt(vertical: str, state_name: str) -> dict:
 
223
  if state_name == "escalate_virtual":
224
  return {"ha": "Zan juya ka ga wakili na mutum yanzu. Ka jira ɗan lokaci.",
225
  "en": "I'll transfer you to a human agent now. Please hold."}
@@ -233,6 +267,13 @@ def get_prompt(vertical: str, state_name: str) -> dict:
233
  return {"ha": s["ha"], "en": s["en"]}
234
 
235
 
 
 
 
 
 
 
 
236
  def get_expected_slot(vertical: str, state_name: str) -> Optional[str]:
237
  if state_name == "fallback":
238
  # Fallback accepts any intent — user might repeat, rephrase, or escalate
@@ -241,7 +282,30 @@ def get_expected_slot(vertical: str, state_name: str) -> Optional[str]:
241
  return s.get("expects") if s else None
242
 
243
 
244
- def transition(state: DialogueState, intent: str, entities: dict) -> DialogueState:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
245
  state.turn_count += 1
246
  for k, v in entities.items():
247
  state.slots[k] = v
@@ -297,6 +361,9 @@ def transition(state: DialogueState, intent: str, entities: dict) -> DialogueSta
297
  if next_state:
298
  state.current_state = next_state
299
  target = SCENARIOS[state.vertical]["states"].get(next_state, {})
 
 
 
300
  if target.get("escalate"):
301
  state.escalate_to_human = True
302
  else:
 
3
  ==========================================
4
  FSM for multi-turn Hausa conversations across 3 verticals.
5
  State lives in Gradio session state (dict) — no Redis needed in the Space.
6
+
7
+ What changed vs the POC
8
+ -----------------------
9
+ 1. Backend actions: data/terminal states may carry an ``"action"`` key naming a
10
+ Backend method (see backend.py). The method runs on entry and merges
11
+ language-neutral results into ``state.slots``.
12
+ 2. Prompt templating: prompts may contain ``{slot}`` placeholders, filled from
13
+ slots via ``render_prompt``. Confirmations now echo the real amount/recipient
14
+ instead of saying "I'll send the money now" with no detail.
15
  """
16
  from __future__ import annotations
17
+
18
+ import logging
19
  from dataclasses import dataclass, field, asdict
20
  from enum import Enum
21
  from typing import Optional
22
 
23
+ from backend import Backend, MockBackend
24
+
25
+ logger = logging.getLogger("plotweaver.dialogue")
26
+
27
+ _DEFAULT_BACKEND: Backend = MockBackend()
28
+
29
 
30
  class Vertical(str, Enum):
31
  BANK = "bank"
 
70
  "transitions": {"provide_digits": "return_balance"},
71
  },
72
  "return_balance": {
73
+ "ha": "Ma'aunin asusunka shine Naira {balance}. Akwai wani abu?",
74
+ "en": "Your account balance is {balance} Naira. Anything else?",
75
+ "action": "get_balance",
76
  "expects": "yesno",
77
  "transitions": {"yes": "greeting", "no": "exit"},
78
  },
 
83
  "transitions": {"yes": "card_blocked", "no": "greeting"},
84
  },
85
  "card_blocked": {
86
+ "ha": "An toshe katinka (lamba {block_ref}). Sabon kati zai iso a cikin kwanaki {card_eta_days}. Ana juya ka ga wakili don tabbatarwa.",
87
+ "en": "Your card is blocked (ref {block_ref}). A new card will arrive in {card_eta_days} days. Transferring you to an agent for confirmation.",
88
+ "action": "block_card",
89
  "expects": None, "terminal": True, "escalate": True,
90
  },
91
  "ask_recipient": {
 
101
  "transitions": {"provide_amount": "confirm_transfer"},
102
  },
103
  "confirm_transfer": {
104
+ "ha": "Don tabbatarwa: zan tura Naira {amount} zuwa {name}. Ka ce 'i' don ci gaba.",
105
+ "en": "To confirm: I'll transfer {amount} Naira to {name}. Say 'yes' to continue.",
106
  "expects": "yesno",
107
  "transitions": {"yes": "transfer_done", "no": "greeting"},
108
  },
109
  "transfer_done": {
110
+ "ha": "An tura Naira {amount} zuwa {name}. Lambar tabbatarwa: {txn_ref}. Godiya da zabar PlotWeaver Bank.",
111
+ "en": "Sent {amount} Naira to {name}. Reference: {txn_ref}. Thank you for choosing PlotWeaver Bank.",
112
+ "action": "transfer",
113
  "expects": None, "terminal": True,
114
  },
115
  },
 
130
  "transitions": {"provide_amount": "airtime_done"},
131
  },
132
  "airtime_done": {
133
+ "ha": "An kara Naira {amount} na airtime. Ma'aunin ka sabo shine Naira {airtime_balance}.",
134
+ "en": "Loaded {amount} Naira airtime. Your new balance is {airtime_balance} Naira.",
135
+ "action": "buy_airtime",
136
  "expects": None, "terminal": True,
137
  },
138
  "ask_bundle_type": {
 
142
  "transitions": {"provide_bundle": "bundle_done"},
143
  },
144
  "bundle_done": {
145
+ "ha": "An kunna bundle na '{bundle}'. Za ka iya yin amfani da shi yanzu.",
146
+ "en": "Your '{bundle}' bundle is active. You can use it now.",
147
+ "action": "buy_bundle",
148
  "expects": None, "terminal": True,
149
  },
150
  "ask_complaint": {
 
154
  "transitions": {"provide_text": "escalate"},
155
  },
156
  "escalate": {
157
+ "ha": "Nagode. An buɗe lamba {ticket_id}. Zan juya ka ga wakili na mutum yanzu.",
158
+ "en": "Thank you. Ticket {ticket_id} opened. I'll transfer you to a human agent now.",
159
+ "action": "file_complaint",
160
  "expects": None, "terminal": True, "escalate": True,
161
  },
162
  },
 
177
  "transitions": {"provide_digits": "order_status"},
178
  },
179
  "order_status": {
180
+ "ha": "Oda {order_id} yana kan hanya. Za a isar gobe da yamma.",
181
+ "en": "Order {order_id} is on the way. It will be delivered tomorrow evening.",
182
+ "action": "check_order",
183
  "expects": None, "terminal": True,
184
  },
185
  "ask_order_id_reschedule": {
 
195
  "transitions": {"provide_date": "reschedule_done"},
196
  },
197
  "reschedule_done": {
198
+ "ha": "An sake tsara isar oda {order_id} zuwa {date}. Za ka sami SMS na tabbatarwa.",
199
+ "en": "Order {order_id} rescheduled to {date}. You'll receive a confirmation SMS.",
200
+ "action": "reschedule",
201
  "expects": None, "terminal": True,
202
  },
203
  "ask_order_id_return": {
 
213
  "transitions": {"provide_reason": "return_done"},
214
  },
215
  "return_done": {
216
+ "ha": "An karɓi buƙatarka (lamba {return_ref}). Wakili zai tattara kaya a gobe.",
217
+ "en": "Your request is received (ref {return_ref}). An agent will collect the item tomorrow.",
218
+ "action": "return_item",
219
  "expects": None, "terminal": True,
220
  },
221
  },
 
224
 
225
 
226
  # Vertical-specific fallback prompts — spoken when the user asks something
227
+ # out of scope.
 
228
  FALLBACK_PROMPTS = {
229
  "bank": {
230
  "ha": "Ban fahimci tambayarka ba. A nan, zan iya taimake ka da 'duba ma'auni', 'toshe kati', ko 'canjin kuɗi'. Don wasu tambayoyi, ka ce 'wakili' don yin magana da mutum.",
 
244
  MAX_CONSECUTIVE_UNKNOWNS = 2
245
 
246
 
247
+ class _SafeDict(dict):
248
+ """format_map helper: leaves unknown placeholders intact instead of raising,
249
+ so a missing slot surfaces as a visible literal during testing."""
250
+
251
+ def __missing__(self, key):
252
+ return "{" + key + "}"
253
+
254
+
255
  def get_prompt(vertical: str, state_name: str) -> dict:
256
+ """Raw prompt (may contain {slot} placeholders). Use render_prompt to fill."""
257
  if state_name == "escalate_virtual":
258
  return {"ha": "Zan juya ka ga wakili na mutum yanzu. Ka jira ɗan lokaci.",
259
  "en": "I'll transfer you to a human agent now. Please hold."}
 
267
  return {"ha": s["ha"], "en": s["en"]}
268
 
269
 
270
+ def render_prompt(vertical: str, state_name: str, slots: dict) -> dict:
271
+ """Prompt with {slot} placeholders interpolated from slots."""
272
+ p = get_prompt(vertical, state_name)
273
+ safe = _SafeDict(slots or {})
274
+ return {k: v.format_map(safe) for k, v in p.items()}
275
+
276
+
277
  def get_expected_slot(vertical: str, state_name: str) -> Optional[str]:
278
  if state_name == "fallback":
279
  # Fallback accepts any intent — user might repeat, rephrase, or escalate
 
282
  return s.get("expects") if s else None
283
 
284
 
285
+ def _maybe_run_action(backend: Backend, vertical: str, state_name: str, state: "DialogueState") -> None:
286
+ """If the target state declares an action, call the backend and merge the
287
+ returned (language-neutral) values into slots. Never raises into the FSM."""
288
+ spec = SCENARIOS[vertical]["states"].get(state_name, {})
289
+ action = spec.get("action")
290
+ if not action or backend is None:
291
+ return
292
+ method = getattr(backend, action, None)
293
+ if method is None:
294
+ logger.warning("Backend has no action %r", action)
295
+ return
296
+ try:
297
+ result = method(dict(state.slots))
298
+ if result:
299
+ state.slots.update(result)
300
+ except Exception as e:
301
+ logger.warning("Backend action %r failed: %s", action, e)
302
+
303
+
304
+ def transition(state: DialogueState, intent: str, entities: dict,
305
+ backend: Optional[Backend] = None) -> DialogueState:
306
+ if backend is None:
307
+ backend = _DEFAULT_BACKEND
308
+
309
  state.turn_count += 1
310
  for k, v in entities.items():
311
  state.slots[k] = v
 
361
  if next_state:
362
  state.current_state = next_state
363
  target = SCENARIOS[state.vertical]["states"].get(next_state, {})
364
+ # Run any backend action attached to the state we just entered, so its
365
+ # results are in slots before the prompt is rendered.
366
+ _maybe_run_action(backend, state.vertical, next_state, state)
367
  if target.get("escalate"):
368
  state.escalate_to_human = True
369
  else: