""" 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")}