Skip to content

Commit cbf46bd

Browse files
committed
Update to v.0.3.0b2
1 parent 4665ae7 commit cbf46bd

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

91 files changed

+6223
-1362
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ syntax: glob
66
.settings
77
.classpath
88
.pydevproject
9-
.coverage
9+
.coverage*
1010
.pytest_cache
1111
.env*
1212
htmlcov

.pre-commit-config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,4 @@ repos:
7272
entry: make test
7373
language: system
7474
always_run: True
75+
pass_filenames: false

Makefile

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,16 @@ $(eval POETRY_VERSION_NEW=$(POETRY_VERSION_MAIN)$(POETRY_VERSION_NAME)$(POETRY_V
2525
.PHONY: test
2626

2727
test:
28-
pytest --cov=maxbot --cov-report html --cov-fail-under=95
28+
pytest -p no:maxbot_stories --cov=maxbot --cov-report html --cov-fail-under=95
2929

3030
stories:
31-
maxbot stories -B examples/hello-world
32-
maxbot stories -B examples/echo
33-
maxbot stories -B examples/restaurant
34-
maxbot stories -B examples/reservation-basic
35-
maxbot stories -B examples/reservation
36-
maxbot stories -B examples/digression-showcase
37-
maxbot stories -B examples/rpc-showcase
31+
pytest --bot examples/hello-world examples/hello-world/stories.yaml
32+
pytest --bot examples/echo examples/echo/stories.yaml
33+
pytest --bot examples/restaurant examples/restaurant/stories.yaml
34+
pytest --bot examples/reservation-basic examples/reservation-basic/stories.yaml
35+
pytest --bot examples/reservation examples/reservation/stories.yaml
36+
pytest --bot examples/digression-showcase examples/digression-showcase/stories.yaml
37+
pytest --bot examples/rpc-showcase examples/rpc-showcase/stories.yaml
3838

3939
clean:
4040
rm -f dist/maxbot-*.*.*-py3-none-any.whl

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,14 @@ Press `Ctrl-C` to exit MaxBot CLI app.
127127

128128
Congratulations! You have successfully created and launched a simple bot and chatted with it.
129129

130+
### Advanced examples
131+
132+
There are several examples of services built on Maxbot. They show the advanced features of Maxbot, such as custom messanger controls, integration with different REST services, databases and so on. You can also check the implementation details of these features in the examples below.
133+
134+
- [Bank Bot example](https://github.com/maxbot-ai/bank_bot).
135+
- [Taxi Bot example](https://github.com/maxbot-ai/taxi_bot).
136+
- [Transport Bot example](https://github.com/maxbot-ai/transport_bot).
137+
130138
## Where to ask questions
131139

132140
The **Maxbot** project is maintained by the [Maxbot team](https://maxbot.ai).

maxbot/bot.py

Lines changed: 73 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
"""Create and run conversations applications."""
22
import asyncio
33
import logging
4-
import os
54

65
from .channels import ChannelsCollection
76
from .dialog_manager import DialogManager
87
from .errors import BotError
98
from .resources import Resources
10-
from .user_locks import AsyncioLocks
9+
from .user_locks import AsyncioLocks, UnixSocketStreams
1110

1211
logger = logging.getLogger(__name__)
1312

@@ -20,22 +19,28 @@ def __init__(
2019
dialog_manager=None,
2120
channels=None,
2221
user_locks=None,
23-
state_store=None,
22+
persistence_manager=None,
2423
resources=None,
24+
history_tracked=False,
2525
):
2626
"""Create new class instance.
2727
2828
:param DialogManager dialog_manager: Dialog manager.
2929
:param ChannelsCollection channels: Channels for communication with users.
30-
:param StateStore state_store: State store.
30+
:param PersistenceManager persistence_manager: Persistence manager.
3131
:param Resources resources: Resources for tracking and reloading changes.
3232
"""
3333
self.dialog_manager = dialog_manager or DialogManager()
3434
self.channels = channels or ChannelsCollection.empty()
35-
self._state_store = state_store # the default value is initialized lazily
36-
self.user_locks = user_locks or AsyncioLocks()
35+
self._persistence_manager = persistence_manager # the default value is initialized lazily
36+
self._history_tracked = history_tracked
37+
self._user_locks = user_locks
3738
self.resources = resources or Resources.empty()
3839

40+
SocketStreams = UnixSocketStreams
41+
SUFFIX_LOCKS = "-locks.sock"
42+
SUFFIX_DB = ".db"
43+
3944
@classmethod
4045
def builder(cls, **kwargs):
4146
"""Create a :class:`~BotBuilder` in a convenient way.
@@ -84,6 +89,23 @@ def from_directory(cls, bot_dir, **kwargs):
8489
builder.use_directory_resources(bot_dir)
8590
return builder.build()
8691

92+
@property
93+
def user_locks(self):
94+
"""Get user locks implementation."""
95+
if self._user_locks is None:
96+
self._user_locks = AsyncioLocks()
97+
return self._user_locks
98+
99+
def setdefault_user_locks(self, value):
100+
"""Set .user_locks field value if it is not set.
101+
102+
:param AsyncioLocks value: User locks object.
103+
:return AsyncioLocks: .user_locks field value
104+
"""
105+
if self._user_locks is None:
106+
self._user_locks = value
107+
return self._user_locks
108+
87109
@property
88110
def rpc(self):
89111
"""Get RPC manager used by the bot.
@@ -93,14 +115,24 @@ def rpc(self):
93115
return self.dialog_manager.rpc
94116

95117
@property
96-
def state_store(self):
97-
"""State store used to maintain state variables."""
98-
if self._state_store is None:
118+
def persistence_manager(self):
119+
"""Return persistence manager."""
120+
if self._persistence_manager is None:
99121
# lazy import to speed up load time
100-
from .state_store import SQLAlchemyStateStore
122+
from .persistence_manager import SQLAlchemyManager
123+
124+
self._persistence_manager = SQLAlchemyManager()
125+
return self._persistence_manager
101126

102-
self._state_store = SQLAlchemyStateStore()
103-
return self._state_store
127+
def setdefault_persistence_manager(self, factory):
128+
"""Set .persistence_manager field value if it is not set.
129+
130+
:param callable factory: Persistence manager factory.
131+
:return SQLAlchemyStateStore: .persistence_manager field value.
132+
"""
133+
if self._persistence_manager is None:
134+
self._persistence_manager = factory()
135+
return self._persistence_manager
104136

105137
def process_message(self, message, dialog=None):
106138
"""Process user message.
@@ -115,8 +147,10 @@ def process_message(self, message, dialog=None):
115147
"""
116148
if dialog is None:
117149
dialog = self._default_dialog()
118-
with self.state_store(dialog) as state:
119-
return asyncio.run(self.dialog_manager.process_message(message, dialog, state))
150+
with self.persistence_manager(dialog) as tracker:
151+
return asyncio.run(
152+
self.dialog_manager.process_message(message, dialog, tracker.get_state())
153+
)
120154

121155
def process_rpc(self, request, dialog=None):
122156
"""Process RPC request.
@@ -131,8 +165,10 @@ def process_rpc(self, request, dialog=None):
131165
"""
132166
if dialog is None:
133167
dialog = self._default_dialog()
134-
with self.state_store(dialog) as state:
135-
return asyncio.run(self.dialog_manager.process_rpc(request, dialog, state))
168+
with self.persistence_manager(dialog) as tracker:
169+
return asyncio.run(
170+
self.dialog_manager.process_rpc(request, dialog, tracker.get_state())
171+
)
136172

137173
def _default_dialog(self):
138174
return {"channel_name": "builtin", "user_id": "1"}
@@ -148,10 +184,14 @@ async def default_channel_adapter(self, data, channel):
148184
message = await channel.call_receivers(data)
149185
if message is None:
150186
return
151-
with self.state_store(dialog) as state:
152-
commands = await self.dialog_manager.process_message(message, dialog, state)
187+
with self.persistence_manager(dialog) as tracker:
188+
commands = await self.dialog_manager.process_message(
189+
message, dialog, tracker.get_state()
190+
)
153191
for command in commands:
154192
await channel.call_senders(command, dialog)
193+
if self._history_tracked:
194+
tracker.set_message_history(message, commands)
155195

156196
async def default_rpc_adapter(self, request, channel, user_id):
157197
"""Handle RPC request for specific channel.
@@ -162,80 +202,32 @@ async def default_rpc_adapter(self, request, channel, user_id):
162202
"""
163203
dialog = {"channel_name": channel.name, "user_id": str(user_id)}
164204
async with self.user_locks(dialog):
165-
with self.state_store(dialog) as state:
166-
commands = await self.dialog_manager.process_rpc(request, dialog, state)
205+
with self.persistence_manager(dialog) as tracker:
206+
commands = await self.dialog_manager.process_rpc(
207+
request, dialog, tracker.get_state()
208+
)
167209
for command in commands:
168210
await channel.call_senders(command, dialog)
169-
170-
def run_webapp(self, host="localhost", port="8080", *, public_url=None, autoreload=False):
171-
"""Run web application.
172-
173-
:param str host: Hostname or IP address on which to listen.
174-
:param int port: TCP port on which to listen.
175-
:param str public_url: Base url to register webhook.
176-
:param bool autoreload: Enable tracking and reloading bot resource changes.
177-
"""
178-
# lazy import to speed up load time
179-
import sanic
180-
181-
self._validate_at_least_one_channel()
182-
183-
app = sanic.Sanic("maxbot", configure_logging=False)
184-
app.config.FALLBACK_ERROR_FORMAT = "text"
185-
186-
for channel in self.channels:
187-
if public_url is None:
188-
logger.warning(
189-
"Make sure you have a public URL that is forwarded to -> "
190-
f"http://{host}:{port}/{channel.name} and register webhook for it."
191-
)
192-
193-
app.blueprint(
194-
channel.blueprint(
195-
self.default_channel_adapter,
196-
public_url=public_url,
197-
webhook_path=f"/{channel.name}",
198-
)
199-
)
200-
201-
if self.rpc:
202-
app.blueprint(self.rpc.blueprint(self.channels, self.default_rpc_adapter))
203-
204-
if autoreload:
205-
206-
@app.after_server_start
207-
async def start_autoreloader(app, loop):
208-
app.add_task(self.autoreloader, name="autoreloader")
209-
210-
@app.before_server_stop
211-
async def stop_autoreloader(app, loop):
212-
await app.cancel_task("autoreloader")
213-
214-
@app.after_server_start
215-
async def report_started(app, loop):
216-
logger.info(
217-
f"Started webhooks updater on http://{host}:{port}. Press 'Ctrl-C' to exit."
218-
)
219-
220-
if sanic.__version__.startswith("21."):
221-
app.run(host, port, motd=False, workers=1)
222-
else:
223-
os.environ["SANIC_IGNORE_PRODUCTION_WARNING"] = "true"
224-
app.run(host, port, motd=False, single_process=True)
211+
if self._history_tracked:
212+
tracker.set_rpc_history(request, commands)
225213

226214
def run_polling(self, autoreload=False):
227215
"""Run polling application.
228216
229217
:param bool autoreload: Enable tracking and reloading bot resource changes.
230218
"""
231219
# lazy import to speed up load time
232-
from telegram.ext import ApplicationBuilder, MessageHandler, filters
220+
from telegram.ext import ApplicationBuilder, CallbackQueryHandler, MessageHandler, filters
233221

234-
self._validate_at_least_one_channel()
222+
self.validate_at_least_one_channel()
235223
self._validate_polling_support()
236224

237225
builder = ApplicationBuilder()
238226
builder.token(self.channels.telegram.config["api_token"])
227+
228+
builder.request(self.channels.telegram.create_request())
229+
builder.get_updates_request(self.channels.telegram.create_request())
230+
239231
background_tasks = []
240232

241233
@builder.post_init
@@ -263,6 +255,7 @@ async def error_handler(update, context):
263255

264256
app = builder.build()
265257
app.add_handler(MessageHandler(filters.ALL, callback))
258+
app.add_handler(CallbackQueryHandler(callback=callback, pattern=None))
266259
app.add_error_handler(error_handler)
267260
app.run_polling()
268261

@@ -311,7 +304,8 @@ def _exclude_unsupported_changes(self, changes):
311304
)
312305
return changes - unsupported
313306

314-
def _validate_at_least_one_channel(self):
307+
def validate_at_least_one_channel(self):
308+
"""Raise BotError if at least one channel is missing."""
315309
if not self.channels:
316310
raise BotError(
317311
"At least one channel is required to run a bot. "

maxbot/builder.py

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,14 @@ def __init__(self, *, available_extensions=None):
3939
self._bot_created = False
4040
self.resources = Resources.empty()
4141
self._user_locks = None
42-
self._state_store = None
42+
self._persistence_manager = None
4343
self._nlu = None
4444
self._message_schemas = {}
4545
self._command_schemas = {}
4646
self._before_turn_hooks = []
4747
self._after_turn_hooks = []
4848
self._middlewares = []
49+
self._history_tracked = False
4950

5051
def add_message(self, schema, name):
5152
"""Register a custom message.
@@ -152,35 +153,37 @@ def user_locks(self, value):
152153
self._user_locks = value
153154

154155
@property
155-
def state_store(self):
156-
"""State store used to maintain state variables.
156+
def persistence_manager(self):
157+
"""Return persistence manager.
157158
158-
See default implementation :class:`~maxbot.state_store.SQLAlchemyStateStore` for more information.
159+
Used, for example, to save-restore state variables.
160+
161+
See default implementation :class:`~maxbot.persistence_manager.SQLAlchemyManager` for more information.
159162
You can use this property to configure default state tracker::
160163
161-
builder.state_store.engine = sqlalchemy.create_engine(...)
164+
builder.persistence_manager.engine = sqlalchemy.create_engine(...)
162165
163166
or set your own implementation::
164167
165-
class CustomStateStore:
168+
class CustomPersistenceManager:
166169
@contextmanager
167170
def __call__(self, dialog):
168171
# load variables...
169172
yield StateVariables(...)
170173
# save variables...
171174
172-
builder.state_store = CustomStateStore()
175+
builder.persistence_manager = CustomPersistenceManager()
173176
"""
174-
if self._state_store is None:
177+
if self._persistence_manager is None:
175178
# lazy import to speed up load time
176-
from .state_store import SQLAlchemyStateStore
179+
from .persistence_manager import SQLAlchemyManager
177180

178-
self._state_store = SQLAlchemyStateStore()
179-
return self._state_store
181+
self._persistence_manager = SQLAlchemyManager()
182+
return self._persistence_manager
180183

181-
@state_store.setter
182-
def state_store(self, value):
183-
self._state_store = value
184+
@persistence_manager.setter
185+
def persistence_manager(self, value):
186+
self._persistence_manager = value
184187

185188
@property
186189
def nlu(self):
@@ -407,6 +410,10 @@ def use_resources(self, resources):
407410
"""
408411
self.resources = resources
409412

413+
def track_history(self, value=True):
414+
"""Set/reset flag that controls history recording."""
415+
self._history_tracked = value
416+
410417
def _create_dialog_manager(self):
411418
message_schema = self._create_message_schema()
412419
command_schema = self._create_command_schema()
@@ -449,7 +456,8 @@ def build(self):
449456
return MaxBot(
450457
self._create_dialog_manager(),
451458
channels,
452-
self.user_locks,
453-
self._state_store,
459+
self._user_locks,
460+
self._persistence_manager,
454461
self.resources,
462+
self._history_tracked,
455463
)

0 commit comments

Comments
 (0)