[Back to Implement a Genie Skill Backend]
If you choose to use the loader provided by @org.thingpdedia.v2
in your manifest, a skill package is required for your skill. It contains the Javascript code describing the details about how your skill will be configured and how each function behaves. We will use The Cat API as an example skill to demonstrate how it works.
The Thingpedia API assumes a precise layout for a skill package. You should not assume any nodejs module beyond the 'thingpedia'
module illustrated here - if you need any, bundle them in your skill package.
The primary entry point (i.e., index.js
) should export a device class. You would instantiate the device class from the API and set it directly to module.exports
. For example, in the cat skill (com.thecatapi
), it exports a CatAPIDevice
class as follows:
const Tp = require('thingpedia');
module.exports = class CatAPIDevice extends Tp.BaseDevice {
constructor(engine, state) {
super(engine, state);
// constructor
}
// other methods of device class
};
Note that, the constructor of the class can be omitted if nothing needs to be done.
Then, for each query or action you want to expose, you would add async functions to your device class with prefix get_
or do_
respectively. So for example, if you want to expose the query cat
for the cat skill, you would modify your index.js
as follows:
const Tp = require('thingpedia');
module.exports = class YelpDevice extends Tp.BaseDevice {
constructor(engine, state) {
super(engine, state);
// constructor
}
async get_cat() {
// return a cat picture
}
};
When you create a device class, you declare a subclass of Tp.BaseDevice
, the base class of all device classes. The full reference of the BaseDevice
class is given in the Thingpedia SDK reference.
If your device can support multiple instances (for example mapping to multiple accounts) you must use the constructor to set the uniqueId
. Unlike the ID
in the metadata, uniqueId
uniquely identifies the device instance of a user. For example, a user may configure two different Twitter accounts, and they will need different IDs in Genie. A common way is to concatenate the device ID, a dash, and then a specific ID for the corresponding account. E.g., "com.twitter-" + this.state.userId
.
Unlike the Yelp skill, many will require some kind of authentication. Three ways to do authentication are supported, including basic
(traditional username and password), oauth2
(OAuth 1.0 and 2.0 style authentication), and discovery
(authentication by discovery and local interactive paring). You can learn more about authentication in the Authentication guide.
Our system provides a generic interface Tp.Helpers.Http
for HTTP requests. These are wrappers for nodejs http API with a Promise interface.
Two of the most useful interfaces are probably Tp.Helpers.Http.get()
and Tp.Helpers.Http.post()
, which deal with HTTP GET requests and POST requests, respectively. We will see an example in practice in the next section.
A full list of the available APIs can be found in the Thingpedia SDK reference.
Recall that we separate Thingpedia functions into two different types: query
and action
. A query
returns data and makes no side effects, while action
makes side effects to the world.
Both queries and actions take an object params
to get the value of input parameters, where the key of the object is the parameter name. Thus, the value of each input parameter can be accessed by params.<param-name>
. In addition, queries have another input containing the hints, it provides what projection
, filter
, count
, or sort
operation will be applied to the query to allow developers to further optimize its skill (optional). For example, the signature of the cat query should look like the following:
async get_cat(params, hints) {
// return a cat picture
}
It is expected that queries and actions will perform some I/O to return the result, so they should be declared as async
.
A query must always return an array of plain objects specifying the value of its output parameters. For example, the cat
function has 3 output parameters, id
, picture_url
and link
, thus, it will return as follows:
async get_cat(params, hints, env) {
...
return [{
id: ...,
picture_url: ...,
link: ...,
}];
}
In fact, you can return any Iterable object, not just Array.
The details of how different ThingTalk types are represented in Javascript can be found in the ThingTalk Reference.
Now let's implement the cat query for The Cat API for real with HTTP helpers. The function should look like this:
async get_cat(params, hints) {
// fetch 1 cat by default
const count = hints.limit || 1;
const url = URL + '&results_per_page=' + count;
const result = await Tp.Helpers.Http.get(url);
const parsed = await Tp.Helpers.Xml.parseString(result);
const array = parsed.response.data[0].images[0].image;
return array.map((image) => {
return {
id: image.id[0],
picture_url: image.url[0],
link: 'http://thecatapi.com/?id=' + image.id[0]
};
});
}
}
We get the number of cats we want to retrieve from hints
by reading hints.limit
. Then we call Tp.Helpers.Http.get()
to the URL of the API endpoint; this is an IO call so we await
the result. The Cat API returns the result in XML format, so we parse it with Tp.Helpers.Xml.parseString()
to extract a JS Object. Then we find the values we need and assign them to the corresponding parameters for return. Note that we used Array.prototype.map()
to create the returned Array.
Putting all the components together, we have The Cat API code as follows.
"use strict";
const Tp = require('thingpedia');
const URL = 'http://thecatapi.com/api/images/get?api_key=MTAxNzA1&format=xml&type=jpg,png';
module.exports = class CatAPIDevice extends Tp.BaseDevice {
async get_cat(params, hints) {
// fetch 1 cat by default
const count = hints.limit || 1;
const url = URL + '&results_per_page=' + count;
const result = await Tp.Helpers.Http.get(url);
const parsed = await Tp.Helpers.Xml.parseString(result);
const array = parsed.response.data[0].images[0].image;
return array.map((image) => {
return {
id: image.id[0],
picture_url: image.url[0],
link: 'http://thecatapi.com/?id=' + image.id[0]
};
});
}
};
[Back to Implement a Genie Skill Backend]