turnstack 0.1.1__tar.gz → 0.1.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {turnstack-0.1.1 → turnstack-0.1.2}/PKG-INFO +1 -1
- {turnstack-0.1.1 → turnstack-0.1.2}/pyproject.toml +1 -1
- {turnstack-0.1.1 → turnstack-0.1.2}/turnstack/engine.py +18 -0
- {turnstack-0.1.1 → turnstack-0.1.2}/turnstack/handlers/action.py +30 -34
- {turnstack-0.1.1 → turnstack-0.1.2}/turnstack.egg-info/PKG-INFO +1 -1
- {turnstack-0.1.1 → turnstack-0.1.2}/README.md +0 -0
- {turnstack-0.1.1 → turnstack-0.1.2}/setup.cfg +0 -0
- {turnstack-0.1.1 → turnstack-0.1.2}/turnstack/__init__.py +0 -0
- {turnstack-0.1.1 → turnstack-0.1.2}/turnstack/exceptions.py +0 -0
- {turnstack-0.1.1 → turnstack-0.1.2}/turnstack/handlers/__init__.py +0 -0
- {turnstack-0.1.1 → turnstack-0.1.2}/turnstack/handlers/base.py +0 -0
- {turnstack-0.1.1 → turnstack-0.1.2}/turnstack/handlers/confirm.py +0 -0
- {turnstack-0.1.1 → turnstack-0.1.2}/turnstack/handlers/input.py +0 -0
- {turnstack-0.1.1 → turnstack-0.1.2}/turnstack/handlers/list_handler.py +0 -0
- {turnstack-0.1.1 → turnstack-0.1.2}/turnstack/handlers/media_handler.py +0 -0
- {turnstack-0.1.1 → turnstack-0.1.2}/turnstack/handlers/menu.py +0 -0
- {turnstack-0.1.1 → turnstack-0.1.2}/turnstack/handlers/render_helpers.py +0 -0
- {turnstack-0.1.1 → turnstack-0.1.2}/turnstack/handlers/router.py +0 -0
- {turnstack-0.1.1 → turnstack-0.1.2}/turnstack/message.py +0 -0
- {turnstack-0.1.1 → turnstack-0.1.2}/turnstack/nodes.py +0 -0
- {turnstack-0.1.1 → turnstack-0.1.2}/turnstack/reply.py +0 -0
- {turnstack-0.1.1 → turnstack-0.1.2}/turnstack/session.py +0 -0
- {turnstack-0.1.1 → turnstack-0.1.2}/turnstack/stores/__init__.py +0 -0
- {turnstack-0.1.1 → turnstack-0.1.2}/turnstack/stores/memory.py +0 -0
- {turnstack-0.1.1 → turnstack-0.1.2}/turnstack/tree.py +0 -0
- {turnstack-0.1.1 → turnstack-0.1.2}/turnstack.egg-info/SOURCES.txt +0 -0
- {turnstack-0.1.1 → turnstack-0.1.2}/turnstack.egg-info/dependency_links.txt +0 -0
- {turnstack-0.1.1 → turnstack-0.1.2}/turnstack.egg-info/top_level.txt +0 -0
|
@@ -214,6 +214,24 @@ class BotEngine:
|
|
|
214
214
|
phone=session.user_id,
|
|
215
215
|
current_node=session.current_node,
|
|
216
216
|
)
|
|
217
|
+
|
|
218
|
+
# ── 7a. action two-message split ──────────────────────────────
|
|
219
|
+
# ActionHandler tags the reply with _action_replies=[action_reply, next_reply]
|
|
220
|
+
# when the fn returned text/Reply. Unwrap them and send as two
|
|
221
|
+
# separate messages so the action text and the follow-up node
|
|
222
|
+
# always arrive as distinct WhatsApp bubbles.
|
|
223
|
+
from .handlers.action import ActionHandler
|
|
224
|
+
action_pair = getattr(reply, ActionHandler.MULTI_REPLY_ATTR, None)
|
|
225
|
+
if action_pair and len(action_pair) == 2:
|
|
226
|
+
action_reply, next_reply = action_pair
|
|
227
|
+
next_reply = self._enrich_menu_reply(next_reply, session)
|
|
228
|
+
action_reply.session_state = session.lifecycle_state
|
|
229
|
+
action_reply.current_node = session.current_node
|
|
230
|
+
next_reply.session_state = session.lifecycle_state
|
|
231
|
+
next_reply.current_node = session.current_node
|
|
232
|
+
await self.session_store.save(session)
|
|
233
|
+
return [action_reply, next_reply]
|
|
234
|
+
|
|
217
235
|
reply = self._enrich_menu_reply(reply, session)
|
|
218
236
|
|
|
219
237
|
# ── 8. attach meta and save ───────────────────────────────────
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
import inspect
|
|
3
|
-
from typing import Any, Dict, TYPE_CHECKING
|
|
3
|
+
from typing import Any, Dict, List, TYPE_CHECKING
|
|
4
4
|
|
|
5
5
|
from ..message import IncomingMessage
|
|
6
6
|
from ..reply import Reply
|
|
@@ -16,14 +16,21 @@ class ActionHandler(NodeHandler):
|
|
|
16
16
|
Handles ``action`` nodes.
|
|
17
17
|
|
|
18
18
|
The developer's ``fn`` may return:
|
|
19
|
-
- A ``str`` — sent as a text message, then the
|
|
19
|
+
- A ``str`` — sent as a separate text message, then the next node is
|
|
20
|
+
sent as a second message.
|
|
20
21
|
- A ``Reply`` — used as-is (allows sending media, ending session, etc.),
|
|
21
|
-
then the
|
|
22
|
-
- ``None`` — nothing extra is sent; the
|
|
22
|
+
then the next node is sent as a second message.
|
|
23
|
+
- ``None`` — nothing extra is sent; only the next node reply is sent.
|
|
23
24
|
|
|
24
25
|
Both sync and async functions are supported.
|
|
26
|
+
|
|
27
|
+
The engine receives a list and sends each reply in order, so the action
|
|
28
|
+
text and the follow-up node always arrive as two distinct messages.
|
|
25
29
|
"""
|
|
26
30
|
|
|
31
|
+
# Sentinel attribute the engine checks to unwrap multi-reply actions.
|
|
32
|
+
MULTI_REPLY_ATTR = "_action_replies"
|
|
33
|
+
|
|
27
34
|
async def handle(
|
|
28
35
|
self,
|
|
29
36
|
node: Dict[str, Any],
|
|
@@ -65,39 +72,28 @@ class ActionHandler(NodeHandler):
|
|
|
65
72
|
|
|
66
73
|
self._transition_to(session, next_key)
|
|
67
74
|
|
|
68
|
-
# ──
|
|
69
|
-
# Enter next node (may chain another action/router silently)
|
|
75
|
+
# ── get next node reply ───────────────────────────────────────
|
|
70
76
|
next_reply = await self._enter_node(session, tree, _depth + 1)
|
|
71
77
|
if next_reply is None:
|
|
72
78
|
next_reply = self._error(session, "Next node returned no reply.")
|
|
73
79
|
|
|
80
|
+
# ── no action text → just return next reply as usual ─────────
|
|
81
|
+
if result is None:
|
|
82
|
+
return next_reply
|
|
83
|
+
|
|
84
|
+
# ── action produced text or a Reply → send as TWO messages ───
|
|
85
|
+
# We attach the pair on the reply object; engine.py unwraps it.
|
|
74
86
|
if isinstance(result, Reply):
|
|
75
|
-
|
|
76
|
-
# We concatenate bodies so the user sees both in one message
|
|
77
|
-
if next_reply.body:
|
|
78
|
-
result.body = result.body + "\n\n" + next_reply.body
|
|
79
|
-
result.current_node = next_reply.current_node
|
|
80
|
-
result.options = next_reply.options
|
|
81
|
-
result.node_type = next_reply.node_type
|
|
82
|
-
return result
|
|
83
|
-
|
|
84
|
-
# Developer returned a str or None
|
|
85
|
-
action_text = str(result) if result is not None else ""
|
|
86
|
-
if action_text and next_reply.body:
|
|
87
|
-
combined = action_text + "\n\n" + next_reply.body
|
|
88
|
-
elif action_text:
|
|
89
|
-
combined = action_text
|
|
87
|
+
action_reply = result
|
|
90
88
|
else:
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
current_node=next_reply.current_node,
|
|
103
|
-
)
|
|
89
|
+
action_reply = Reply(
|
|
90
|
+
type="text",
|
|
91
|
+
body=str(result),
|
|
92
|
+
phone=session.user_id,
|
|
93
|
+
current_node=session.current_node,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# Carry options/node_type only on the SECOND (next_reply) message.
|
|
97
|
+
# Tag the first reply so the engine knows to split them.
|
|
98
|
+
setattr(action_reply, self.MULTI_REPLY_ATTR, [action_reply, next_reply])
|
|
99
|
+
return action_reply
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|