Taxi Example
is the simplest service to order a taxi. The service allows the customer to get to the right place quickly, and the drivers provide a constant flow of orders.
The customer orders a taxi through CustomerBot
, specifying:
- pick up location,
- destination point.
The driver provides through the DriverBot
:
- phone number/name on the first visit (registration),
- location,
- radius in which the taxi service is provided.
TaxiService
service based on these data finds the best driver on the customer's order.
The service includes three roles:
The customer, CustomerBot
:
- creates the order,
- searches for a driver,
- manages the state of the trip.
The driver, DriverBot
:
- starts working (informs the system that he works and determines the geography of orders),
- stops working (tells the system that he has finished working),
- accepts the order,
- rejects the order,
- informs about the waiting for the customer,
- informs about the beginning of the trip,
- informs about the end of the trip,
- reports readiness for new trips.
The system, TaxiService
:
- registers the customer,
- registers the driver,
- calculates the trip distance and cost,
- connects customer and driver,
- registers the start/end of the driver's work,
- registers and monitors the state of the trip,
- cancels the trip,
- looking for suitable drivers,
- sends offers to drivers for the trip,
- registers the agreement/disagreement to make a trip by the driver.
The customer's role is implemented as a chat-bot for two channels Viber
and Telegram
.
The driver's role is implemented as a separate chat-bot in Telegram
.
The system's role is implemented as a separate REST API
application.
The general scheme of the service is shown below:
Before you start, you should make sure that the system has:
- Python 3.9 - 3.11,
- Git,
- Poetry,
- Pagekite.
We recommend to just download pagekite.py and use it as a separate scenario.
curl -O https://pagekite.net/pk/pagekite.py
It's also required:
- To register and receive a token for the Routing Service,
- To create two bots in
Telegram
: driver’s chat and customer's chat: see BotFather and instruction, - To create a
Viber
bot for customer: see instruction.
Note You'll need a smartphone with Telegram
and Viber
apps to work properly. Desktop versions of Telegram
and Viber
do not fit, as they don't allow you to send the location.
Clone this repository and change current directory
git clone https://github.com/maxbot-ai/taxi_bot
cd taxi_bot
Install the package with dependencies
poetry shell
poetry install
The service implements the entire business logic of the project:
- registers the driver and customers,
- calculates the trip distance and cost,
- builds the route,
- connects customer and driver,
- monitors the state of the trip.
Below is a scheme of the service with some details that will be used to customize the example.
-
To handle incoming requests from
DriverBot
andCustomerBot
TaxiService
providesHTTP REST API
(it is a server). Thebind_port
parameter sets the port whichTaxiService REST API
will use. The address and port must be set in the startup parameters ofDriverBot
andCustomerBot
. The file in whichbind_port
is set and the commands to start the bots are described below. -
REST API
is also used to transfer data toDriverBot
and toCustomerBot
. In this caseTaxiService
is the client. Thedriver_bot_url
andcustomer_bot_url
parameters intaxi_bot.conf
file are responsible for this. These URLs can be either local (localhost) or external when using tunnels. -
The external service openrouteservice.org is used to generate a map and calculate travel distances. The service supports access via
HTTP REST API
.TaxiService
is a client. To work with the service you need to register for free and get the access key. See instructions. Theopenrouteservice_token
parameter defines a key for using the route building API. This setting is set in the filetaxi_bot.conf
.
To configure the service, save the file taxi_bot.conf.template
with the name taxi_bot.conf
.
cp taxi_bot.conf.template taxi_bot.conf
Next, open the taxi_bot.conf
file in your favorite editor and set the actual values for all parameters:
bind_port
— port on whichTaxiService
will wait to incomingHTTP
requests. For example,6010
. Requests fromCustomerBot
andDriverBot
will be received on this port.openrouteservice_token
— token to access the openrouteservice.org service. Free registration is required to obtain the key. See instructions.driver_bot_url
— address for interaction withDriverBot
. For example,http://localhost:6011
.customer_bot_url
— address for interaction withCustomerBot
. For example,http://localhost:6012
.image_storage_url
— external address whereTaxiService
should be available. The external address is required in order to send pictures toTelegram
andViber
. You can use pagekite or ngrok to get the external address and forward it to the local port ofTaxiService
. See examples of launches below.upload_file_path
— full path to the directory where the maps of the built trips will be saved. For example,/tmp
.
-
We will use pagekite to get an external address and create a tunnel to the
TaxiService
. You can use any other similar utility, such as ngrok or setup communication throughreverse-proxy
. -
If you use pagekite, you need to register and specify the subdomain to be used to create the tunnel. For example,
taxiexample.pagekite.me
. -
Run
pagekite
. Setbind_port
, for example6010
and specify the desired external address (it shouldn't be occupied by another person, if there is an error, then come up with a different name). For example,python3 pagekite.py 6010 taxiexample.pagekite.me
Then follow the instructions at the
pagekite
command line. As a result,pagekite
should create a tunnel. You should see the following line in console:<> Flying localhost:6010 as https://taxiexample.pagekite.me/
-
Create a directory to store routing files. For example,
/home/user_name/tmp/
or use/tmp
if access rights allow. And don't forget to write the full path in thetaxi_bot.conf
file,upload_file_path
parameter. -
Make sure that you are in the
taxi_bot
directory. -
Run the service with the command
taxi_bot --config taxi_bot.conf
The bot implements the interaction between the driver and TaxiService
. It provides the following features:
- driver registration,
- confirmation and cancellation of the trip,
- trip execution.
- The bot supports the
Telegram
channel. Interaction with theTelegram
service is realized through theWebHooks
mechanism. You should have an external address to registerWebHook
. See the Launch section. DriverBot
transfers data toTaxiService
viaREST API
. TheDriverBot
is a client. The address to access is set in the bot settings. See the Setup section,TAXI_BOT_API
parameter.DriverBot
receives data fromTaxiService
viaREST API
too. TheDriverBot
is the server. See theport
parameter in theDriverBot
start command below.
To configure the bot, save the file env.template
with the name .env
in the taxi_bot
directory. Оpen file .env
in your favorite editor and set the actual values for all parameters:
TELEGRAM_DRIVER_API_KEY
— the token ofTelegram
bot, that will becomeDriverBot
.TELEGRAM_CUSTOMER_API_KEY
— the token ofTelegram
bot, that will becomeCustomerBot
inTelegram
.VIBER_CUSTOMER_API_KEY
— the token ofViber
bot, that will becomeCustomerBot
inViber
.TAXI_BOT_API
— address of theTaxiService
service. Specifyhttp://127.0.0.1:6010
, if you used this value when configuringTaxiService
.
All fields are required in the current version of the bot.
- In the pagekite control panel, add the
kite
to create another tunnel. For example,driver-taxiexample.pagekite.me
. To do this, click theadd kite
button. - Make sure you are in the
taxi_bot
directory. - Select the port for
DriverBot
, for example,6011
. - Run the bot with the command
In our particular example:
maxbot run --bot taxi_bot.bot:driver --updater=webhooks --host=localhost --port=<BIND_PORT> --public-url=<DRIVER_BOT_PUBLIC_URL>
maxbot run --bot taxi_bot.bot:driver --updater=webhooks --host=localhost --port=6011 --public-url=https://driver-taxiexample.pagekite.me
- Run
pagekite
. Set the port and the external address. For example,python3 pagekite.py 6011 driver-taxiexample.pagekite.me
- Make sure that
pagekite
has started successfully and created a tunnel.
The bot implements the interaction between the customer and TaxiService
. It provides the following features:
- customer registration,
- ordering a taxi,
- change the status of the trip.
- The bot supports the
Telegram
and theViber
channels. Interaction with these services is realized through theWebHooks
mechanism. You should have an external address to registerWebHook
. See the Launch section. CustomerBot
transfers data toTaxiService
viaREST API
as well as theDriverBot
. See the appropriate section.CustomerBot
receives data fromTaxiService
viaREST API
as well as theDriverBot
. See the appropriate section.
See the appropriate section for the DriverBot
.
- In the pagekite control panel, add the
kite
to create another tunnel. For example,customer-taxiexample.pagekite.me
. To do this, click the `add kite' button. - Select the port for
CustomerBot
, for example,6012
. - For
CustomerBot
it is important to create the tunnel first, and then run the bot (due to the features of Viber and pagekite). Run pagekite. Set the port and the external address, for example,python3 pagekite.py 6012 customer-taxiexample.pagekite.me
- Make sure that
pagekite
has started successfully and created a tunnel onbind_port
. - Make sure you are in the
taxi_bot
directory. - Run the bot with the command
In our particular example:
maxbot run --bot taxi_bot.bot:customer --updater=webhooks --host=localhost --port=<BIND_PORT>\ --public-url=<CUSTOMER_BOT_PUBLIC_URL>
maxbot run --bot taxi_bot.bot:customer --updater=webhooks --host=localhost --port=6012 --public-url=https://customer-taxiexample.pagekite.me
After successfully launching TaxiService
, DriverBot
and CustomerBot
you can test the example. Text DriverBot
and CustomerBot
via Telegram
and follow the hint.
This example shows the details of the implementation:
- using an external
HTTP
service, - interacting with an external
HTTP
service through theREST
command, - database schema,
- customer settings in the channel,
dialog
variable, - adding new commands to channels using
mixin
, - using of
RPC
requests to notify bots from externalHTTP
service, - working with several channels (
Telegram
andViber
) inCustomerBot
, - running tests,
- adding dependencies.
- An external
HTTP
service in python is used to implement business logic:TaxiService
. - The
taxi_bot/api_service
folder and the file are used for startingtaxi_bot/cli.py
. - It is accessed from bots using the REST external
HTTP
calls function (see below). - There is also a feature for external services to notify bots via
HTTP
queries:RPC
(see below).
The REST
command is used to interact with an external service; it performs HTTP
requests.
It is necessary to add the external service setting to the bot's scenario.
Example:
extensions:
rest:
services:
- name: taxi_service
base_url: http://taxi.service.net
timeout: 10
...
Or you can specify the address via the environment settings:
...
base_url: !ENV ${TAXI_BOT_API}
...
The TAXI_BOT_API
variable must be specified in the file taxi_bot/.env
You can specify more than one external service; each service must have a unique value of name
.
Example of using an external service with the REST
command:
response: |-
{% POST "taxi_service://{}/{}".format(dialog.channel_name, dialog.user_id) body {
"phone": slots.phone
} %}
This command sends a HTTP
request (POST) http://taxi.service.net/{channel_name}/{user_id}
with the JSON body.
See the dialog
variable below.
You can get the information about the customer from the bot scenario using dialog
variable.
The following parameters are available:
- customer channel name:
dialog.channel_name
it isTelegram
orViber
in this example - customer ID in the channel:
dialog.user_id
is a unique string value within the channel
Note
- The pair (
dialog.user_id
,dialog.channel_name
) is unique for each customer, it is used to store data about customers in this example - Only one channel (
Telegram
) was created for drivers, so it is enough to usedialog.user_id
to work with them
Sometimes you need the bot to respond to events, taking place in an external HTTP
service, not as a reaction to messages from customers.
The RPC
feature is made to receive and process such events. This is the ability of the bot scenario
to receive and process HTTP
requests. Each request must be described in the RPC
block.
Example of configuration:
rpc:
- method: arrival
params:
- name: order
required: true
Example of processing:
- condition: rpc.driver_arrived and slots.order_id == params.order_id
response: |-
<jump_to node="waiting_start_ride" transition="response" />
It is used to notify that a driver has arrived at the customer in CustomerBot
.
The event that happened in DriverBot
via an external HTTP
service and the RPC
feature comes to CustomerBot
.
mixin
is used to add features to communicate withTelegram
andViber
messengers- the
channels
folder contains the implementation of additional features of channels: work with buttons, locations and contacts
Telegram
documentationViber
documentation
Example of usage in scenario:
- condition: message.contact
response: |-
phone={{message.contact.phone_number}}
first_name={{message.contact.first_name}}
last_name={{message.contact.last_name|default('')}}
Note The Viber
does not have the first_name
and the last_name
parameters, but has the name
parameter.
It comes in the message.contact.first_name variable
, while message.contact.last_name
is always empty, equal to none
.
Note The last_name parameter can also be empty in Telegram
.
Telegram
documentation- The
request_contact
parameter is used in implementation forTelegram
Viber
documentation- The
ActionType=share-phone
parameter is used in implementation forViber
Example of using in bot scenario:
response: |-
<keyboard_button_contact title="Send your phone" text="Please, send me your contact." />
Telegram
documentationViber
documentation- The
horizontal_accuracy
parameter determines the map scale when displaying the map in messenger, its value is chosen empirically.
Example of using in bot scenario:
- condition: message.location
response: |-
latitude={{message.location.latitude}}
longitude={{message.location.longitude}}
<location latitude="{{ message.location.latitude }}"
longitude="{{ message.location.longitude }}"
horizontal_accuracy="5"/>
Telegram
documentation- The
request_location
parameter is used in implementation forTelegram
Viber
documentation- The
ActionType=location-picker
parameter is used in implementation forViber
Example of using in bot scenario:
response: |-
<keyboard_button_location text="Submit your location." title="My location" />
Used to select an option from the list or to prevent the customer from writing text.
Telegram
documentationViber
documentation
Example of using in bot scenario:
response: |-
<keyboard_button_list text="Click start or change route:" >
<buttons>start route</buttons>
<buttons>change route</buttons>
</keyboard_button_list>
Telegram
documentation- The
InputFieldState=hidden
parameter is used in implementation forTelegram
Viber
documentation- The
ActionType=none
parameters is used in implementation forViber
Note If you want to remove the chat buttons, you need to use the KeyboardButtonRemoveMixin
, otherwise
buttons will not disappear after sending a new message.
Note The message with the button removal command should contain text: it will be sent as a text message.
Example of using in bot scenario:
response: |-
<keyboard_button_remove text="Ride started" />
CustomerBot
uses two channels: Viber
and Telegram
. The example demonstrates
the ability to work with multiple messengers in the same scenario.
poetry shell
poetry install (if not previously installed)
poetry run pytest tests/
The external python service contains a number of dependencies that need to be installed.
To do this, add these dependencies to the pyproject.toml
file:
[tool.poetry.dependencies]
python = ">=3.9, <3.12"
maxbot = "^0.2.0b2"
click-config-file = {version = "^0.6.0"}
Flask-SQLAlchemy = {version = "^3.0.2"}
...
The maxbot
dependency should be in all examples.
The tool.poetry.group.dev.dependencies
section lists dependencies to run unit tests ,
in this example it is:
[tool.poetry.group.dev.dependencies]
pytest = "^7.2.0" (for pytest based unit tests)
httpretty = "^1.1.4" (mock library for `HTTP` modules)