Helper library to develop Rhasspy apps in Python

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)

I will do it later. I want to have a second look at the Rhasspy source before that. Yesterday it confused me, as it seems to handle the dialogue continue session message always in the same way.

I have added your suggestions as issues in the repository. You can subscribe to an issue to follow progress.

It seems I got confused while testing. The issue with intentNotRecognized triggering a loop because it started to listen to it’s output is triggered by a 10 second TTS timeout in DialogueHermesMqtt.

My help text is pretty long (13 seconds). I thought I could also reproduce it with a shorter text but I can’t anymore. I have opened an issue: https://github.com/rhasspy/rhasspy/issues/62

I wrote a small countdown timer app using your library:

I think everything is there for that use case.

Some things that could help:

  • An easy way to access the slot values
  • An easy way to start a session (with type notitfication)
  • (in Rhasspy: a builtin slot type for duration)

That’s looking nice!

What would you suggest?

Yes, it makes sense to add a method to start a session.

Built-in slot types are indeed a feature that could help a lot of use cases. But that should be embedded in Rhasspy itself indeed. There have been some discussions about it already.

I’m now working on tests (current code coverage: 44%) and there’s autogenerated API documentation. I want to expand both before I create the first release on PyPI.

I just published the first release of Rhasspy Hermes App on PyPI: 0.1.0.

You can install it with:

pip3 install rhasspy-hermes-app

If the API of this library changes, your app possibly stops working when it updates to the newest release of Rhasspy Hermes App. Therefore, it’s best to define a specific version in your requirements.txt file:

rhasspy-hermes-app==0.1.0

This way your app keeps working when the Rhasspy Hermes App library adds incompatible changes in a new version.

I have expanded the documentation, including some information for people who want to contribute.

All contributions are welcome! New features, bug fixes, documentation, tests, …

@H3adcra5h I have added a lot of documentation, mypy annotations and tests to the code, but for now I have left your raw topic additions more or less alone because you know that part of the code better. If you could take a look at it, that would be awesome.

4 Likes

I am no real Python developer and don’t really know what is best practice for such things.

One idea would be: add a new parameter “supported_slots” to the decorator.

Could look like this:

@app.on_intent("StartTimer", supported_slots=["minutes", "seconds"])
def handle_start_timer(intent, minutes, seconds):
  or
def handle_start_timer(intent, slot_minutes, slot_seconds):

If the Intent has slot values for minutes or seconds handle_start_timer will get called with that otherwise it will provide “None”. One variant of this would be to also put the expected type into the decorator and let the decorator do the validation.

Another option: Just provide a helper function like mine in your module to get the slot values out of an intent. As you can see in my source (maybe I am doing it wrong) it is not that simple.

Ok, I have created an issue about easier access to slot values to welcome a discussion and follow progress on this.

I have released version 0.2.0 with two additions:

Added

  • Method HermesApp.notify to send a dialogue notification. See #10.
  • Decorator HermesApp.on_dialogue_intent_not_recognized to act when the dialogue manager failed to recognize an intent. See #9.

The latest version can be installed from PyPI:

pip3 install --upgrade rhasspy-hermes-app
1 Like