File size: 3,288 Bytes
3c8a5a9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
"""
PlotWeaver Voice Agent — Backend interface
==========================================
Separates *what the bot says* (dialogue.py) from *what actually happens*
(account lookups, card blocks, transfers, order status, ...).

How it wires in
---------------
A state spec in dialogue.py may carry an optional ``"action"`` key naming a
Backend method. When the FSM transitions *into* that state, it calls the
method with the current slots dict; the method returns a dict of values that
are merged back into slots, which the prompt templates then interpolate.

Contract
--------
Backends return ONLY language-neutral data — numbers, ids, names, dates the
caller supplied. They never return Hausa/English sentences: all phrasing lives
in dialogue.py prompts. This keeps a real integration (bank sandbox, telecom
API, logistics API) free of any localization concern.

MockBackend ships canned-but-input-aware data for the demo. To go live, drop in
an implementation satisfying the ``Backend`` protocol; dialogue.py is unchanged.
"""
from __future__ import annotations

import random
import string
from typing import Protocol, runtime_checkable


def _ref(prefix: str, n: int = 6) -> str:
    return prefix + "".join(random.choices(string.ascii_uppercase + string.digits, k=n))


@runtime_checkable
class Backend(Protocol):
    # --- bank ---
    def get_balance(self, slots: dict) -> dict: ...
    def block_card(self, slots: dict) -> dict: ...
    def transfer(self, slots: dict) -> dict: ...
    # --- telecom ---
    def buy_airtime(self, slots: dict) -> dict: ...
    def buy_bundle(self, slots: dict) -> dict: ...
    def file_complaint(self, slots: dict) -> dict: ...
    # --- ecommerce ---
    def check_order(self, slots: dict) -> dict: ...
    def reschedule(self, slots: dict) -> dict: ...
    def return_item(self, slots: dict) -> dict: ...


class MockBackend:
    """Deterministic-enough mock. Echoes user-provided slots so confirmations
    and receipts reflect what the caller actually said, and fabricates only the
    values a real backend would return (balances, refs, etc.)."""

    # --- bank ---
    def get_balance(self, slots: dict) -> dict:
        return {"balance": "245,000", "account_last4": slots.get("digits", "")}

    def block_card(self, slots: dict) -> dict:
        return {"card_eta_days": "3-5", "block_ref": _ref("BLK")}

    def transfer(self, slots: dict) -> dict:
        return {
            "txn_ref": _ref("TXN"),
            "amount": slots.get("amount", ""),
            "name": slots.get("name", ""),
        }

    # --- telecom ---
    def buy_airtime(self, slots: dict) -> dict:
        return {"amount": slots.get("amount", ""), "airtime_balance": "1,500"}

    def buy_bundle(self, slots: dict) -> dict:
        return {"bundle": slots.get("bundle", "")}

    def file_complaint(self, slots: dict) -> dict:
        return {"ticket_id": _ref("TKT")}

    # --- ecommerce ---
    def check_order(self, slots: dict) -> dict:
        return {"order_id": slots.get("digits", "")}

    def reschedule(self, slots: dict) -> dict:
        return {"order_id": slots.get("digits", ""), "date": slots.get("date", "")}

    def return_item(self, slots: dict) -> dict:
        return {"order_id": slots.get("digits", ""), "return_ref": _ref("RET")}