Helper library to develop Rhasspy apps in Python

@fastjack I try to understand your point.

You prefer having placeholder in the topic instead of the wildcard. The implementation should parse and create the regex for matching the topic. On an incoming event it extract the the data, map it to the right placeholder and provide this as properties or dictionary.
Then the topic as argument is unnecessary. Like this:

@app.on_topic("printer/{name}/state")
def test_topic(properties: dict, payload: bytes):
    _LOGGER.debug(f"name: {properties.get('name'), payload: {payload.decode('utf-8')}")

Is it right?

Actually, do you need the regex pattern or a template at all? In all your examples you just use a wildcard topic and this can be just used like this:

@app.on_topic("hermes/hotword/+/detected")
def test_topic(topic: str, payload: bytes):
    content = json.loads(payload.decode('utf-8'))
    _LOGGER.debug(f"topic: {topic}, model: {content['modelId']}")

Your code should just work like this. Because in on_raw_message the concrete topic that matches the wildcard (like "hermes/hotword/foobar/detected") is passed, and you pass this to the decorated function with function(topic, payload). So in the function the topic name is in the topic argument.

Right if the app creates a regex to match the topic to the right decorated function.
I have to look how complex wildcards can be and if I’m able to create the right regex on the fly. I’m not a specialist for regex.

It seems there are only + and #. So it should be possible for me to create a regex on the fly.
But the more I think about it, with placeholders could also be a nice solution, or a mix of both.

With placeholders would look nicer (and more easy to read for ppl without programming background) but I want to note that the placeholder should not be fixed in one place only.

For the example of printer/{name}/state it works but someone might come along that wants to filter by state also, or has more abstruse hermes topics because one IoT device sets a strange topic that switches the order around.

Yes, I had it in mind. Like: printer/{room}/{name}/state

1 Like

The generic decorator can then easily be provided multiple topics if required:

@on_topic("{device}/{name}/state", "light/{name}/on", "light/{name}/off")

I don’t know if this is really required but should not be hard to implement and allow more freedom to the library users. Maybe this is a bit too far :wink:

Step 2 or 3, I think :wink:

I think in this case a bit to far might be better. I have only started playing around with mqtt when I turned up in this forum but I have found some strange mqtt topics around the internet. I even came across one example where someone somehow had the state in the middle.

Most ppl would never do this but since the library is a way to make it easier for less experienced users some strange cases might turn up and depending on how long ago they formed the habit they might be unwilling to change. And like I said, some IoT devices set pretty strange topics, too.

I was mistaken here. I overlooked that the code checks for equality between the key and the topic. But I think with some minor changes this could be made to work.

So there are two separate ideas in the current discussion:

  1. I think we should drop the regex pattern, so the on_topic decorator only needs one argument: a string. With some modifications the code could interpret this string as a topic wildcard and create a regular expression that matches all topics matching this wildcard. In the on_raw_message function the incoming topic would then be matched with the regular expression to see which decorated function should be called. This way you can decorate a function with @app.on_topic("printer/+/+/state") and the function would be called for topics printer/office/brother/state, printer/livingroom/hp/state and so on. The same could be done for the # wildcard.
  2. I like @fastjack’s idea of the templates and named properties. But because there are very few restrictions on MQTT topic names and printer/{room}/{name}/state is a valid MQTT topic, I think we shouldn’t use a string for this argument, but define (or re-use? this looks like very general functionality, so maybe we could re-use some existing minimal templating language module for his) a specific class for this. Or maybe just define another decorator for this, such as on_topic_template.

I think having 1. first and later extending the code to have 2. would be good. 1. would be a nice feature for the first published version of rhasspy-hermes-app on PyPI, so people can already react to Hermes messages that don’t have their own decorator yet in the library.

I made a new version and a pull request, so everyone can try it. This 4 kinds of subscriptions are working so far:

@app.on_topic("hermes/hotword/{hotword}/detected")
def test_topic1(data: TopicData, payload: bytes):
    _LOGGER.debug(f"topic1: {data.topic}, hotword: {data.custom_data.get('hotword')}, payload: {payload.decode('utf-8')}")


@app.on_topic("hermes/dialogueManager/sessionStarted")
def test_topic2(data: TopicData, payload: bytes):
    _LOGGER.debug(f"topic2: {data.topic}, payload: {payload.decode('utf-8')}")


@app.on_topic("hermes/tts/+")
def test_topic3(data: TopicData, payload: bytes):
    _LOGGER.debug(f"topic3: {data.topic}, payload: {payload.decode('utf-8')}")


@app.on_topic("hermes/+/{site_id}/playBytes/#")
def test_topic4(data: TopicData, payload: bytes):
    _LOGGER.debug(f"topic4: {data.topic}, site_id: {data.custom_data.get('site_id')}")

The data contains the matched topic and custom data with the values of the named placeholder.
As you can see, you can mix it.

All topics are only examples and do not have to make sense. :wink:

2 Likes

This is very nice. If you’re ambitious, you could try adding @fastjack’s suggestion to add multiple topics. I think this is doable by changing the decorator’s signature to:

def on_topic(self, *topic_names: str):

And then put almost all the code inside the wrapper function in a loop for topic_name in topic_names: and then probably some minor changes elsewhere.

I’ll have a closer look tomorrow and test it before merging the PR.

It wasn’t as easy as you think, but I refactored it. Maybe some parts could/should be better implemented, because I don’t know all python features.

So, thanks to @H3adcra5h you can do now something like this in the library:

@app.on_topic("hermes/+/{site_id}/playBytes/#")
def test_topic4(data: TopicData, payload: bytes):
    _LOGGER.debug(f"topic4: {data.topic}, site_id: {data.data.get('site_id')}")

And even:

@app.on_topic(
    "hermes/dialogueManager/sessionStarted",
    "hermes/hotword/{hotword}/detected",
    "hermes/tts/+",
    "hermes/+/{site_id}/playBytes/#",
)
def test_topic1(data: TopicData, payload: bytes):
    if "hotword" in data.topic:
        _LOGGER.debug(f"topic: {data.topic}, hotword: {data.data.get('hotword')}")
    elif "playBytes" in data.topic:
        _LOGGER.debug(f"topic: {data.topic}, site_id: {data.data.get('site_id')}")
    else:
        _LOGGER.debug(f"topic: {data.topic}, payload: {payload.decode('utf-8')}")

The examples are purely fictional, of course. The goal is to have specific decorators for Hermes messages that work on Rhasspy Hermes classes, but in the mean time, you can use this on_topic decorator for Hermes messages that don’t have their own decorator yet. This also makes it easy to react to both Hermes messages and non-Hermes MQTT messages.

Thanks again, @H3adcra5h!

2 Likes

I just created a first rough API documentation. You can generate it with:

make docs

Afterwards you can find the generated HTML pages in docs/build/html. The plan is to extend this documentation and publish it for easier discovery of the library’s functionality.

With the new “on_topic” decorator I am trying to handle the “intent not recognized” event like this:

@app.on_topic("hermes/dialogueManager/intentNotRecognized")
def intent_not_recognized(data: TopicData, payload: bytes):
    _LOGGER.debug("not recognized: " + str(data) + str(payload))

    payload_json = json.loads(payload.decode("utf-8"))
    session_id = payload_json.get("sessionId")
    site_id = payload_json.get("siteId")
    if session_id in akinator_sessions:
        text = 'Das habe ich nicht verstanden'
        app.publish(
            DialogueContinueSession(
                session_id=session_id,
                site_id=site_id,
                text=text,
                intent_filter=intent_filter,
                custom_data=None,
                send_intent_not_recognized=True,
                slot=None
                )
        )

The problem is: It only kind of works. Rhasspy seems to push some or most of the TTS message into the STT system. Causing an infinite loop of intent_not_recognized events. It should wait until the TTS output is finished before recording. It does that normally, why not when the ContinueSession is published from the intent not recognized event. Is that a Rhasspy bug?

Regarding the api:

It would be great if there were a parameter in the decorator to say the system “payload is json” and it doing the json-load and decode for you. Even better in my case a on_intent_not_recognized decorator´

And what if you subscribe to hermes/nlu/intentNotRecognized instead of hermes/dialogueManager/intentNotRecognized?

I have just added an on_intent_not_recognized decorator in https://github.com/rhasspy/rhasspy-hermes-app/commit/485cfceeda63f66b478fee1b7fb5db9651170231. Note that this is for hermes/nlu/intentNotRecognized. Let me know if you need the other one too.

I think hermes/dialogueManager/intentNotRecognized is the correct one to use. It’s behavior is depending on the send_intent_not_recognized parameter of the DialogueContinueSession message. (True will issue the intentNotRecognized event False will end the session. I looked at the Rhasspy source).

Sadly with both it will capture part of the TTS response as input.

That seems like a bug in Rhasspy then. Can you open an issue in the rhasspy repository?

Thank you. I would need the other one. But there are probably use case to have both. The nlu one will react to every not recognized intent the dialogueManager only in an active dialog session (at least that’s my understanding)