[Back to Implement a Genie Skill Backend]
WORK IN PROGRESS!
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 Cooking skill 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 cooking skill (org.agent.cooking
), it exports a CookingSkillDevice
class as follows:
const Tp = require('thingpedia');
module.exports = class CookingSkillDevice 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 recipe
for the cooking 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_recipe() {
// return a recipe
}
};
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
.
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 recipe
query should look like the following:
async get_recipe(params, hints) {
// return a recipe
}
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 recipe
function has 3 output parameters, id
, ingredients
and instructions
, thus, it will return as follows:
async get_recipe(params, hints, env) {
...
return [{
id: ...,
ingredients: ...,
instructions: ...,
}];
}
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 recipe query for the Cooking skill that performs a search query against a local database. The function should look like this:
const generatedRecipes = require("./generated_recipe.json");
async get_recipe(params, hints) {
// fetch a list of recipes by default
let recipes = generatedRecipes.recipes; //
const newRecipes = [];
for (let recipe of recipes) {
const newRecipe = {
id: new Value.Entity(recipe.id, recipe.id),
ingredients: recipe.ingredients.map((ingredientId) => new Value.Entity(ingredientId, ingredientId)),
instructions: recipe.instructions.map((instructionId) => new Value.Entity(instructionId, instructionId))
};
newRecipes.push(newRecipe);
}
return newRecipes;
}
Note that you can also choose to let Genie handle the projection
, filter
, count
, or sort
operations to simplify the query operation.
[Back to Implement a Genie Skill Backend]