We had a discussion about it earlier: Parsing builtin slot values
I agree about this. But what is this ‘session state’? Shouldn’t the skill just know how to end or continue the current session? That could be delivered by methods of the base class for the skills.
With some tricks it is possible. I did this in SnipsKit to create translatable intent names, see an example.
Isn’t this session_state
the same thing as customData
In the dialogue manager session ? Does it provides a way to abstract the serialization/deserialization of this value (as it is supposed to be a string) and it’s propagation in dialogue management topics?
Yes that’s the purpose of customData
. And an app can find this in the intent object (see the NluIntent
class): https://github.com/rhasspy/rhasspy-hermes/blob/master/rhasspyhermes/nlu.py
I would actually drop that based on @fastjack answer, if it’s ok for you. One less problem to worry about. Using specific intents per-skill and intent “namespaces” would be enough (e.g. daniele-athome:myskillapp:myintent
).
The problem is that only the dialogue manager knows about customData. So it’s not being passed with the Hermes message that actually counts in this case, which is hermes/intent/<intent_name>
. That’s because the NluIntent
message is sent by the NLU module and not the Dialogue Manager. More details in the official Hermes documentation.
Anyway, as I understand it, fixing that in Rhasspy would allow customData to be passed to NluIntent
s too, making my “session state” idea obsolete (btw I’ve already started working on that, but it involves multiple modules I’m afraid).
Snips worked like that, so for snips users who have already process with customData (like me ) , it would be easier to migrate.
But it is not a reason to not ask ourselves if there is no better methods to do that
Ced
Now I think it’s the best method. I just didn’t connected all the dots before – I was missing some information.
I have never used the customData
yet because all my apps had a simple question - answer dialogue, but I agree: the changes you propose could make this work in Rhasspy the way that Snips worked. Thanks for taking the time to look into this!
So, I haven’t forgotten about this, just busy with some other stuff. Yesterday I started to convert my proof of concept code into a repository I can publish, but this also made me think more about the design of the API. I remember when I was creating SnipsKit one of the Snips developers suggested a Flask-like API to me.
This would mean that the standalone version of the example app that I showed in the beginning of this post would look something like this:
from rhasspyhermes_app import StandaloneApp
app = StandaloneApp("TimeApp")
@app.intent("GetTime")
def get_time(intent):
now = datetime.now().strftime("%H %M")
app.say(f"It's {now}", intent.site_id)
if __name__ == '__main__':
app.run()
Such a Flask-type API is more flat with no inheritance. You don’t need to define a class. A lot of Python web developers are familiar with it. I see the beauty in this approach, and I actually rewrote part of SnipsKit in this style before I stopped using Snips, but I never actually wrote voice apps this way, so I don’t know whether it’s a good choice.
An important reason to not choose this approach is that AppDaemon apps are class-based: they should subclass the mqtt.Mqtt
class for MQTT apps and AppDaemon creates an object of this class and initializes it. So choosing this approach for standalone Rhasspy apps breaks API similarity with Rhasspy apps for AppDaemon. So I’m inclined to just implement this library with the class-based approach.
I’m curious what others think, though.
I think too that it’s not a good idea to break API similarity. But i like the Flask-like style
Ced
I really like the Flask style implementation and would prefere it over the class based one just for simplicity. Another Pro-Flask point is that this kind of minimalistic API is easier to understand and implement. it just takes less code…
i understand however the more linear approach using classes. Would using this make it easier to adapt to changes at AppDaemon in future?
Well, I do like the Flask-style API too, it’s easier to reason about, especially for simple apps. And it saves an indentation level it’s just that using the class-based approach for both types of apps would make the standalone and AppDaemon versions of the API as good as identical. If I’m using the Flask-style approach for standalone apps and the class-based approach for AppDaemon apps, the APIs would still be very similar (just compare the Flask-style example to the first AppDaemon example in this post), but if you have been writing standalone apps in the Flask-style API and then for some reason switch to developing for AppDaemon (or the other way around), you suddenly have to change your programming style a bit.
This may seem like hair-splitting, but I want to start from a clean base now so I don’t have to rewrite the fundamentals of the API later. So I want to look at all the options now and hear other opinions.
@daniele_athome you’re probably the one with most experience in AppDaemon from the participants in this discussion, what do you think about it?
Can you use the alternative AppDaemon implementation approach outlined here? Using this approach based on ADBase might allow you to build something Flask-like as you are proposing. You would still need to sub-class ADBase for your app, but the MQTT or HASS plugins can then be used via “accessors” without resorting to sub-classing.
Nice find! I forgot about this alternative. But those plugin objects are still defined inside the class, no? And AppDaemon only initializes an app when it finds a subclass of ad.ADBase
defined in a file. As far as I know, other code in the file outside the class definition is not executed by AppDaemon. How should the code for the AppDaemon time app look like with this approach, according to you?
Errr, is there anything that needs to change? I guess the final stanza calling app.run()
is unnecessary as there is no “main” for AppDaemon apps. The meat of the question is what does StandaloneApp
do behind the scenes to “run” your Rhasspy app in an AppDaemon app and hide the details?
For example, how do named (Rhasspy) apps defined with StandaloneApp
be made known to AppDaemon? You still have to “configure” the AppDaemon app correct? Would the named app (e.g. “TimeApp”) map to an AppDaemon app by that name that would have to be configured via yaml in the standard AppDaemon manner? Does that mean that StandaloneApp
has to generate the AppDaemon class corresponding to the named app on the fly? I presume this is possible in Python, but this starts to sound complicated. It also seems like with this flat structure, one could run into (function) namespace collisions that would be less likely in a class based approach given that unlike simple Python scripts, multiple apps live in the same Python environment with AppDaemon. Might this result in obscure issues with user contributed apps intended to run in AppDaemon?
What is it that one expects to gain in a Flask-like API versus the traditional AppDaemon approach? The two simple examples so far look pretty similar sans the class declaration. I’ve not used Flask so maybe others can be more detailed in why they like that style? Are we really gaining anything by hiding AppDaemon or just making things obscure? Your original class-based approach does seem reasonable. It can easily take advantage of the existing AppDaemon infrastructure without resorting to lots of hair to hide things. It does feel like a Flask-like structure is sort of an impedance mismatch with AppDaemon.
Sorry, seems like I have more questions than answers!
I would say that using a class-based approach or a function-based approach (both using annotations) wouldn’t really be a huge difference since an app (either AppDaemon app or standalone app) could just instantiate the “app” object and use methods provided by it to speak/listen to Hermes (IOC can be achieved for both ways with a little more effort). But there is one problem: what will the framework use for communicating with the external world (MQTT/Hermes)?
A standalone app would need some connection infrastructure (by leveraging an event loop for example), a configuration file for connection parameters, and so on.
An AppDaemon app can leverage a running application server, so no need for anything.
This difference alone forces us to create two different entry points.
Personally I’d try to leverage existing framework(s) – I would not reinvent the wheel by writing another framework from scratch (i.e. if we want to use a “Flask approach”, we should use Flask for real) – that is, if that was your idea @koan and if I understood it correctly.
I also think that creating a framework that can be used in multiple environments (that is, that can be used interchangeably with another framework or application server without touching the app code) brings more complexity and effort than benefits. Think about the compromises we’ll have to make, the complexity we’ll have to handle to make the framework “behave well” in every environment. Also think about the conventions will have to impose on the app’s creator that wants to use e.g. Flask stuff or AppDaemon stuff correctly.
In conclusion IMHO this library should focus on integrating with 1 framework/application server/whatever environment it runs in, since the real “interface” for this library is the Hermes protocol.
As much as I am personally interested in using AppDaemon – as a Home Assistant user I find it extremely convenient – any framework/environment will do. If someone wants to use another framework/environment it will require a new library.
However…
Beware: brainstorming stuff that came out of my mind almost as it was. I was tempted to not include this paragraph, but I’ll leave it here for whatever discussion it might generate
If we really want to make a “general purpose” library, we could implement something like a core module that handles Hermes business logic, and then implement adapters that will make the library plug into other frameworks/application servers/environment. Those adapters would be in charge of publishing/subscribing to MQTT using the framework/environment they are supposed to interact with. An example workflow would be (I might be talking in AppDaemon terms, but I believe the same concepts can be applied to any framework/environment):
- The app would register to events using ways and methods provided by the library
- The library will use the adapter to implement that “listen to events” (the adapter will provide an interface for doing that) (*1)
- The adapter would listen to events by using the means provided by the enviroment
- The adapter will receive calls (service requests from the app or events from MQTT) that will be handled using business logic in the core module
- The core module will return something to the adapter (or signal it some how) that it handled the call
- The adapter will return the result to the app
A similar approach could be used for service requests (not event-based, think about service calls).
(*1) this part is where the library would scan for annotations and subscribes to topics accordingly, without actually knowing how, because the adapter would take care of it.
This seems an overengineered and overly complex design to me. Also I might have missed something because this is the result of a 30 minutes brainstorming. Or I might just be a bad designer but to me the simple fact that it seems to be very complex makes me think that maybe it’s not worth the effort (in relation to the benefits).
In my next iteration of the proof of concept of the standalone app I’m now subclassing the HermesClient class and using the cli module, both from the Rhasspy Hermes library, which gives me all this functionality for free.
No that was not my intention. We don’t need HTTP endpoints and all this web stuff in the apps, we are using MQTT. I was just referring to the style of the API: creating a Flask object, attaching decorators of this object to functions and then running the object. This in contrast to subclassing a class of the API, attaching decorators to methods of this class and then creating an object of this subclass. As you already said, it’s not a huge difference in practice, but I feel that for casual developers this style is easier to reason about.
After thinking about this for a while, I’m also sure now that trying to create a framework that can be used both standalone and in AppDaemon will be too complex with too many compromises. It would be like driving a square peg in a round hole.
The “brainstorming stuff” that you wrote is exactly what I came up with, but it gave me nightmares about the time when I was designing bloated Java programs with factories and visitors and strategies and so on
So for now I’m focusing on a library for standalone apps, because this is also the way to go if we want to deliver Rhasspy apps as Docker containers for better security. As you say, the real interface is the Hermes protocol. I see this library really as a “helper” library, a wrapper around the Rhasspy Hermes library to eliminate as much boilerplate code as possible and let developers create simple apps in a few lines of code.
Damn for a moment there I hoped you would go for AppDaemon eheh
I feel like an AppDaemon plugin for Hermes is really missing, I’d like to implement it anyway. After our POCs will be out (actually if you can share what you have even if incomplete would be great), we can discuss using similar approaches so developers may easily switch from one to the other (such as naming convention stuff, e.g. name of the annotations, anything that can be shared really). I believe this could benefit everyone especially because we’ll have two different point of views to compare and work. What do you think?
Thanks for your work on this.
I’m now in my third iteration of the proof of concept, each time I had to start from scratch. First class-based, then Flask-like, now Flask-like using HermesClient and cli. I think this time it will result in a good foundation for the library. I hope this is ready and tested in a few days (sadly not much free time these days to program) and then I’ll publish it. Don’t expect too much of it yet, it’s just one or two Hermes messages wrapped to prove the approach is viable.
After that, It’s a good idea indeed to discuss some conventions so the APIs can share as much as possible in their naming, philosophy, …
By the way, do you mean “AppDaemon plugin for Hermes” or “Hermes plugin for AppDaemon”? I didn’t want to suggest the latter to you in my previous message because I didn’t know whether you wanted to implement this, but I actually think this could be a good approach to develop Rhasspy/Hermes functionality for AppDaemon: a dedicated Hermes plugin (like the HASS and MQTT plugins) to hide the MQTT details from Rhasspy app developers.