turnstack 0.1.0__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.0 → turnstack-0.1.2}/PKG-INFO +1 -1
- {turnstack-0.1.0 → turnstack-0.1.2}/pyproject.toml +1 -1
- {turnstack-0.1.0 → turnstack-0.1.2}/turnstack/engine.py +24 -5
- {turnstack-0.1.0 → turnstack-0.1.2}/turnstack/handlers/action.py +30 -34
- {turnstack-0.1.0 → turnstack-0.1.2}/turnstack.egg-info/PKG-INFO +1 -1
- {turnstack-0.1.0 → turnstack-0.1.2}/README.md +0 -0
- {turnstack-0.1.0 → turnstack-0.1.2}/setup.cfg +0 -0
- {turnstack-0.1.0 → turnstack-0.1.2}/turnstack/__init__.py +0 -0
- {turnstack-0.1.0 → turnstack-0.1.2}/turnstack/exceptions.py +0 -0
- {turnstack-0.1.0 → turnstack-0.1.2}/turnstack/handlers/__init__.py +0 -0
- {turnstack-0.1.0 → turnstack-0.1.2}/turnstack/handlers/base.py +0 -0
- {turnstack-0.1.0 → turnstack-0.1.2}/turnstack/handlers/confirm.py +0 -0
- {turnstack-0.1.0 → turnstack-0.1.2}/turnstack/handlers/input.py +0 -0
- {turnstack-0.1.0 → turnstack-0.1.2}/turnstack/handlers/list_handler.py +0 -0
- {turnstack-0.1.0 → turnstack-0.1.2}/turnstack/handlers/media_handler.py +0 -0
- {turnstack-0.1.0 → turnstack-0.1.2}/turnstack/handlers/menu.py +0 -0
- {turnstack-0.1.0 → turnstack-0.1.2}/turnstack/handlers/render_helpers.py +0 -0
- {turnstack-0.1.0 → turnstack-0.1.2}/turnstack/handlers/router.py +0 -0
- {turnstack-0.1.0 → turnstack-0.1.2}/turnstack/message.py +0 -0
- {turnstack-0.1.0 → turnstack-0.1.2}/turnstack/nodes.py +0 -0
- {turnstack-0.1.0 → turnstack-0.1.2}/turnstack/reply.py +0 -0
- {turnstack-0.1.0 → turnstack-0.1.2}/turnstack/session.py +0 -0
- {turnstack-0.1.0 → turnstack-0.1.2}/turnstack/stores/__init__.py +0 -0
- {turnstack-0.1.0 → turnstack-0.1.2}/turnstack/stores/memory.py +0 -0
- {turnstack-0.1.0 → turnstack-0.1.2}/turnstack/tree.py +0 -0
- {turnstack-0.1.0 → turnstack-0.1.2}/turnstack.egg-info/SOURCES.txt +0 -0
- {turnstack-0.1.0 → turnstack-0.1.2}/turnstack.egg-info/dependency_links.txt +0 -0
- {turnstack-0.1.0 → turnstack-0.1.2}/turnstack.egg-info/top_level.txt +0 -0
|
@@ -189,6 +189,8 @@ class BotEngine:
|
|
|
189
189
|
if incoming.type == "text" and incoming.text:
|
|
190
190
|
cmd_reply = await self._handle_global_command(session, incoming.text.strip().lower())
|
|
191
191
|
if cmd_reply:
|
|
192
|
+
if cmd_reply.type == "end":
|
|
193
|
+
return [cmd_reply]
|
|
192
194
|
# Command handled – render the resulting node
|
|
193
195
|
reply = await self._render_current(session)
|
|
194
196
|
if reply is None:
|
|
@@ -212,6 +214,24 @@ class BotEngine:
|
|
|
212
214
|
phone=session.user_id,
|
|
213
215
|
current_node=session.current_node,
|
|
214
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
|
+
|
|
215
235
|
reply = self._enrich_menu_reply(reply, session)
|
|
216
236
|
|
|
217
237
|
# ── 8. attach meta and save ───────────────────────────────────
|
|
@@ -246,14 +266,13 @@ class BotEngine:
|
|
|
246
266
|
"""
|
|
247
267
|
# Exit / reset
|
|
248
268
|
if text in self.exit_keywords:
|
|
249
|
-
|
|
250
|
-
# Optionally send a goodbye message (could be configured)
|
|
269
|
+
await self.session_store.delete(session.user_id)
|
|
251
270
|
return Reply(
|
|
252
|
-
type="
|
|
253
|
-
body="👋
|
|
271
|
+
type="end",
|
|
272
|
+
body="👋 Goodbye! Send us a message anytime to start a new session.",
|
|
254
273
|
phone=session.user_id,
|
|
255
274
|
node_type="text",
|
|
256
|
-
current_node=
|
|
275
|
+
current_node=None,
|
|
257
276
|
)
|
|
258
277
|
|
|
259
278
|
# Go home
|
|
@@ -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
|