What is Baxter? Baxter is a voice assistant that is supposed to help me here and there (currently chat based). It will probably be converted to an API later, or be accessible via a WebSocket. So it will be accessible via Arduinos, mobile apps and more. Currently, he only speaks german.
Please note that BaxterLite is a project designed for easy extension and has a lot of helper functions and classes. You should definitely read the README before implementing your own features, as they will probably save you a lot of work in the end.
Since Baxter is only a side project and I also only add features that I can use myself, it is relatively small. Therefore, also the Lite. His features (actions):
- Get current time
- Clear the chat
- Repeat the last action
- Give you a random number
- Tell you a joke
- Open a website
- Say hello
- Say bye
BaxterLite also has a few features that are not actions, but are still very useful:
- You can open a chat with BaxterLite from the taskbar or by pressing the key combination
(LEFT) CTRL + SHIFT + B
. - Create own, intern actions is a bit more complicated, so I programmed a small plugin system. You can find more
information about this in the README under
# Create own plugins
.
-
First you need to download or clone the project, then navigate to the downloaded folder:
cd <cloned-repo-dir>
BaxterLite comes with a few dependencies that we need to install first. For this we simply run the following command:
pip install -r requirements.txt
Good, now we just start the main.py and that's it, BaxterLite is ready to support you!
python main.py
So, what is a plugin? Well, a plugin in this case is simply your Python script that you throw into the plugins
folder (plugins/
). In this case, it MUST still follow a certain structure. But don't worry, it's extremely easy to
add your plugin. Let's get started.
-
First we need to create our Python file. To do this, first go to the folder where the main.py of BaxterLite is located. From there, navigate to the
plugins
folder. If none exists yet, you can simply create one, don't worry.Now we create the file. In this example we will call it
get_random_number.py
. You should move it into theplugins
directory, so at the end, the path of the file will look like this:<path-to-baxter-lite>/plugins/get_random_number.py
-
Everything running smoothly so far? Okay, then let's write a simple code. I'll explain it afterwards.
from utils.action_helper.action_helper import BaxterPlugin from utils.action_utils import ActionUtils, TriggerInfos import random class Plugin(BaxterPlugin): def __init__(self) -> None: super().__init__() self.name = 'get_random_number' @classmethod def get_response(cls, input_str: str, main_str: str, error_str: str, action_utils: ActionUtils, trigger_infos: TriggerInfos) -> str: try: return main_str.format(number=random.randint(0, 99999)) except (Exception,): return error_str
Okay, what do we see here? First, let's create a class. The class MUST be called
Plugin
, otherwise the manager won't find your plugin! And its base-class MUST beBaxterPlugin
!
Then we set the name of the plugin. Note that the default name for the plugin isuntitled
. This will be relevant for later. However, you should change the name URGENTLY.
Now comes the most important part. Theget_response
function. If BaxterLite thinks that the sentence that was entered should trigger your action/plugin, then it gets the response sentence from THIS function. The function is the place where your magic happens. We'll go through the parameters of the function in a moment.
So, let's summarize first. First we create the class, taking into account the following:- The class MUST be called
Plugin
- The class MUST inherit from
BaxterPlugin
- We should set the name of the plugin (otherwise errors may occur later)
Well, that's what's important for creating and initializing the class. Now let's look at the
get_response
function. The function takes 5 parameters:- input_str: str (the user input - the sentence that was entered)
- main_str: str (the response that the user gets when the action is executed from intents.json)
- error_str: str (the response that the user gets when the action went wrong - also from intents.json)
- action_utils: ActionUtils Class (a class that contains some useful functions, e.g. to find important parts in the user input - docs below)
- trigger_infos: TriggerInfos Class (a class that contains some useful information about the trigger, e.g. the confidence of the classifier - docs below)
NOTE: If you need it, yourget_response
function CAN also be asynchronous. Just add theasync
keyword before thedef
keyword. The function should then look like this:@classmethod async def get_response(cls, input_str: str, main_str: str, error_str: str, action_utils: ActionUtils, trigger_infos: TriggerInfos) -> str: try: return main_str.format(number=random.randint(0, 99999)) except (Exception,): return error_str
That was actually the most difficult step. And yet not too difficult, right?
- The class MUST be called
-
The manager will find the plugin on its own if you have done everything correctly. But you have not yet said exactly when your plugin should be executed. For this we need to edit the
intents.json
file, which is under<path-to-baxter-lite>/datasets/intents.json
. We need to navigate to the intents list and now add a new element. The element MUST have the following keys:- tag:
String
-> This MUST beplugin-<name-of-plugin>
. The name of the plugin is the name that you set in the__init__
function of your plugin class. In our case it isget_random_number
. So the tag would beplugin-get_random_number
. - patterns:
List of strings
-> a list of possible sentences how a user can call your action - responses:
List of strings
-> possible responses of your action - action:
String
-> This MUST beplugin-<name-of-plugin>-action
. The name of the plugin is again the name that you set in the__init__
function of your plugin class. In our case it isget_random_number
. So the action would beplugin-get_random_number-action
. - error_msg:
String
-> An error message that the user gets when your action went wrong
In this example, the new element would look like this:
{ "tag": "plugin-get_random_number", "patterns": [ "give me a random number", "give me a number" ], "responses": [ "Here is your number: {number}" ], "action": "plugin-get_random_number-action", "error_msg": "Unable to get random number, sorry" }
- tag:
Congratulations, you have successfully created your first plugin! Now you can just launch the application and call the action. BaxterLite trains the classifier automatically when the application is started, so you don't have to worry about it!
NOTE: I do not recommend adding actions on this way, because it is very complicated. If it is possible, you
should use the plugin system. You can find more information about this in the README
under # Create own plugins
.
All "commands" that Baxter can do are called action. All actions are located
in utils/action_helper/actions/<name-of-action>-action.py
. To add your own action, follow the instructions below:
-
Think about an action name. It should be meaningful and understandable. Action names use the snake case as a naming convention, for e.g.
weather_forecast
. -
Create a new Python file, under
utils/action_helper/actions/
. The file should have the name of your action to keep an overview. -
Write your code in the Python file you created (all inside a class that should be called
<Name-Of-Action>-Action
). The function returns the response to the user and is called whenever the action is to be executed. An example would be:class SomeAction: def get_response(self, input_str, main_response: str, error_str: str, action_utils: ActionUtils, trigger_infos: TriggerInfos) -> str: return main_response
Note that ALL actions must have the function
get_response
! And also note that you CAN make this function asynchronous like described in the plugin section. -
As you can see, the function must take 5 parameters:
- input_str: String (the user input)
- main_response: String (the response that the user gets when the action is executed from intents.json)
- error_str: String (the response that the user gets when the action went wrong)
- action_utils: ActionUtils Class (a class that contains some useful functions, e.g. to find important parts in the user input)
- trigger_infos: TriggerInfos Class (a class that contains some useful information about the trigger, e.g. the confidence of the classifier)
-
The code is ready and working? You have a get_response? Cool! Now we need to tell the action_helper that you created an action. For this we simply go to
utils/action_helper/action_helper.py
and first to the init method. There will be a dictionary that looks something like this:self.__actions: dict = { 'check_fightclub_room2': fightub_action.FightclubAction() }
At the top you had to come up with a name for your action, we will enter it here. And as key an instance of your action class. The dictionary should look like this (if your action is called
weather_forecast
):self.__actions: dict = { 'check_fightclub_room2': fightub_action.FightclubAction(), 'weather_forecast': weather_forecast_action.WeatherForecastAction() }
We go to the top of the
action_helper.py
and import the class created before. The import should look like this:from utils.action_helper.actions import fightub_action, weather_forecast_action
-
Great, that was almost everything we need to do in the code. Now there are only 2 steps missing. Let's start with the second last one, we need to edit the intents.json, which is under
datasets/intents.json
. We need to navigate to the intents list and now add a new element. The element MUST have the following key value pairs!- tag:
String
-> something that explain short your action name (snake_case) - patterns:
List of strings
-> a list of possible sentences how a user can call your action - responses:
List of strings
-> possible responses of your action - action:
String or null
-> the action name you've created in step 1 - error_msg:
String
-> An error message that the user gets when your action went wrong In this example, the new element would look like this:
{ "tag": "get_weather_forecast", "patterns": [ "how is the weather outside", "give me a weather forecast" ], "responses": [ "outside are {forecast} degrees" ], "action": "weather_forecast", "error_msg": "Unable to get weather forecast, sorry" }
Now save the
intents.json
file and do the last step. - tag:
-
Whenever the
intents.json
is changed, you have to train the classifier again. For this we first go tomain.py
. You will find a line that looks something like this:classifier: Classifier = Classifier(str_helper, 'datasets/intents.json', use_pretrained=True)
To train that thing, we just add a line, in the whole it should look like this:
classifier: Classifier = Classifier(str_helper, 'datasets/intents.json', use_pretrained=True) classifier.train(epochs=50)
Pouh, that was a long tutorial. Anyway, now you can just launch the application and call the action.
The TriggerInfos class is a class that contains some useful information about the trigger. You can find it under
utils/action_utils/
. The class is a data class and contains the following attributes:
- ui: webview.Window (the window from which the trigger was made)
- last_action: str | None (the last action that was executed)
- last_input: str | None (the last input that was executed)
The ActionUtils class is a class that contains some useful functions. You can find it under utils/action_utils.py
.
The class contains the following functions:
This functions sends the given string to the current chat and calls the given function when the user sends the next message. Need some example? Here you go:
class GetRandomNumberAction:
@classmethod
def get_response(cls, input_str: str, main_str: str, error_str: str, action_utils: ActionUtils, trigger_infos: TriggerInfos) -> str:
def callback(input_str: str):
print(input_str) # -> should be the user input (for e.g. if next message is "123" it should print "123")
action_utils.request_input('Please enter a number', callback) # -> displays "Please enter a number" in the chat and calls the callback function when the user sends the next message
I personally recommend using this function instead of the synchronous one, because its much easier to use. This function sends the given string to the current chat and returns the next message of the user. Here is an example:
class GetRandomNumberAction:
@classmethod
async def get_response(cls, input_str: str, main_str: str, error_str: str, action_utils: ActionUtils, trigger_infos: TriggerInfos) -> str:
user_input: str = await action_utils.request_input_async('Please enter a number') # -> displays "Please enter a number" in the chat and returns the next message of the user
print('User input: ' + user_input) # -> should be the user input (for e.g. if next message is "123" it should print "123")
Note that the function must be asynchronous like described in the plugin section.
If you want to send a message to the chat, without returning it as a response, you can use this function. Here is an example:
class GetRandomNumberAction:
@classmethod
def get_response(cls, input_str: str, main_str: str, error_str: str, action_utils: ActionUtils,
trigger_infos: TriggerInfos) -> str:
action_utils.send_message('Please wait a moment...') # -> displays "Please wait a moment..." in the chat
Returns PositionPrediction Class (contains for each important part start-idx and end-idx, if nothing found it will be minus value) Example:
input_str: str = 'open google.com'
token_detector: TokenDetector = action_utils.get_token_detector()
position_prediction: PositionPrediction = token_detector.get_important_parts(input_str)
website_start: int = round(position_prediction.part1_start)
website_end: int = round(position_prediction.part1_end)
website: str = action_utils.get_part_by_indexes(input_str, website_start, website_end)
print(website) # -> is google.com
-
get_config_helper() -> ConfigHelper
Returns an instance of the ConfigHelper class Example:
class OpenWebsiteAction: @classmethod def get_response(cls, input_str: str, main_str: str, error_str: str, action_utils: ActionUtils, trigger_infos: TriggerInfos) -> str: config_helper: ConfigHelper = action_utils.get_config_helper()
Returns the location in a string that is between the given indexes. Example:
class OpenWebsiteAction:
@classmethod
def get_response(cls, input_str: str, main_str: str, error_str: str, action_utils: ActionUtils, trigger_infos: TriggerInfos) -> str:
test_str: str = 'This is an example sentence' # goal is to get "example sentence"
start_idx: 3
end_idx: 4
result: str = action_utils.get_part_by_indexes(test_str, start_idx, end_idx) # should be "example sentence"
Executes an if statement in the string, checking the config for values. Returns the executed string. The sample config looks like this:
{
"name": "Fido"
}
Let's say the main_str looks like this:
Hello%if_name%, {name}%if_name_end%!
As you can see the if statement starts with %if_name%A. The _ says that now comes the key to search for in the config. It does not check the value of this key, but whether the key exists at all.
Then comes {name}. Here the value of the key name is fetched from the Config and inserted. After that the if statement is closed with an %if_name_end%. Here the key MUST be the same as when opening.
A simple action that handles these if statements looks like this:
import webview
class GreetAction:
@classmethod
def get_response(cls, input_str: str, main_str: str, error_str: str, action_utils: ActionUtils, trigger_infos: TriggerInfos) -> str:
test_str: str = 'Hello%if_name%, {name}%if_name_end%!'
result: str = action_utils.handle_if_statements(main_str) # since name exists in Config it will result in "Hello, Fido!" otherwhise it would end in "Hello!"
return result
Returns an instance of the TokenDetector class Example:
class OpenWebsiteAction:
@classmethod
def get_response(cls, input_str: str, main_str: str, error_str: str, action_utils: ActionUtils, trigger_infos: TriggerInfos) -> str:
token_detector: TokenDetector = action_utils.get_token_detector()
BaxterLite was programmed by Fidode07.