This document details how to implement a new feature for Stampy, by creating a module - Stampy is organized into modules, which inherit from the Module
class.
To explain how to implement a new Stampy feature, let's work through an example: Choosing randomly between options. The goal is a feature that works like this:
Example 1
Example 2
We'll implement this by creating a new module, which we'll call the 'choose' module. Here's how you'd do that!
- Open a GitHub issue that describes the module that you're planning to make.
- Message the
#stampy-dev
channel on Discord, to get feedback and buy-in for your potential module. If the general idea seems well liked, assign it to yourself and start coding. - Create a new branch named similarly to your module.
$ git checkout -b choose-module
- Create a new file named after your module inside of the
modules
folder.$ <editor of your choice> choose.py
- Inside of
choose.py
, create a child class of theModule
class-
import re from random import choice from modules.module import Module, Response class ChooseModule(Module): def __str__(self): return "Choose Module"
-
- Implement the required functions from the
Module
parent classprocess_message(self, message)
- This method takes a message and processes it, optionally returning a reply for Stampy to say, and an integer representing its confidence that the reply is good and should be posted. This method usually contains the core functionality of our module.
-
def process_message(self, message): text = self.is_at_me(message) if text and text.startswith("choose ") and " or " in text: choices_string = text.partition(" ")[2].strip("?") options = [option.strip() for option in re.split(" or |,", choices_string) if option.strip()] return Response( confidence=8, text=choice(options), why="I was asked to make a choice" ) return Response()
self.is_at_me(message)
checks if the message addressed to Stampy, for example by starting or ending with "stampy". If so, it returns the text of the message stripped of the address to stampy, otherwise it returns None. Checking this is usually a good idea, since we generally don't want Stampy to butt in on existing conversations.- Examples of outputs from
self.is_at_me(message)
:"Hello"
->None
"Hello stampy"
->"Hello"
"What do you mean, stampy?"
->"What do you mean?"
- Examples of outputs from
- All modules return a response object. This object should contain a text response to send directly to the user in the
text
property. If a developer needs to utilize more complex functionality they can instead provide the response object with a function to run in the future via thecallback
property (see the response object doc string for more info). If the message isn't addressed to stampy or our module, we return an empty Response object which by default has 0 confidence. If the message is addressed to stampy and looks like a choice question addressed to stampy, we return 8 ("This is a valid command specifically for this module") - Multiple modules might think that they could respond to a message. The system will take whichever module reports the highest confidence from
process_message
via the response object. - The last thing we need to do is to write a few test cases to verify that the module we wrote works correctly. For this we will use stampy's test module which is built in to the module base class. Simply define test cases as a list property of the class:
-
@property def test_cases(self): return [ self.create_integration_test(question="choose A or B", expected_regex=r"(^A$)|(^B$)"), self.create_integration_test( question="choose one, two, or three", expected_regex=r"(^one$)|(^two$)|(^three$)" ), ]
- Notice that we are using regular expressions in this case to match the expected response. Fuzzy matching is also supported by
create_integration_test
using theexpected_response
(expected text) andminimum_allowed_similarity
(number between 0 and 1 representing the percentage of matching chars).
- The final
choose.py
module file might look like this:
import re from random import choice from modules.module import Module, Response class ChooseModule(Module): def __str__(self): return "Choose Module" def process_message(self, message): text = self.is_at_me(message) if text and text.startswith("choose ") and " or " in text: choices_string = text.partition(" ")[2].strip("?") options = [option.strip() for option in re.split(" or |,", choices_string) if option.strip()] return Response(confidence=8, text=choice(options), why="I was asked to make a choice") return Response() @property def test_cases(self): return [ self.create_integration_test(question="choose vim or emacs", expected_regex=r"(^vim$)|(^emacs$)"), self.create_integration_test( question="choose coffee, tea, or water", expected_regex=r"(^coffee$)|(^tea$)|(^water$)" ), ]
- Stampy will automatically load your module using the
get_stampy_modules
function, which imports everything frommodules
, detects classes that inherit fromModule
, instantiates them and appends tomodules_dict
.- Previously it was required to add modules manually to the dictionary. This method is admittedly hacky but it's also convenient and not very likely to backfire.
- Once your feature is implemented, test your changes on the test discord server. You can run your integration test by asking stampy
stampy test yourself
in a discord channel where stampy is listening. Your output should be visible on discord. - Add and commit your changes
git add modules/choose.py stam.py
git commit -m "Created a new stampy module that randomly chooses between options given by the user"
- Open a pull request on github
- Post a link to your pull request in the
#stampy-dev
Discord channel
Good job, thanks for helping make Stampy better!