Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/happy dialog 5 #127

Merged
merged 23 commits into from
Mar 12, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
2993035
add prompt for year slot
hyzhak Mar 9, 2018
0bc118c
when we get exactly matched templates they shouldn't have requirement…
hyzhak Mar 9, 2018
257a9b0
log separately exact valid acknowledges
hyzhak Mar 9, 2018
d014a70
should minimize false positive on prompting user
hyzhak Mar 9, 2018
1dffb62
pupulate filled and resolved slots to prompt message
hyzhak Mar 9, 2018
b9ae954
wrap debug in the single logger and user appropriate console functions
hyzhak Mar 9, 2018
d115c54
fetch years for suggestion list
hyzhak Mar 12, 2018
1c0bb1f
return plain list of suggested year withou duplications
hyzhak Mar 12, 2018
265f368
slice 3 suggestions right before putting to response
hyzhak Mar 12, 2018
8047587
conver fetch suggestions in a middleware style
hyzhak Mar 12, 2018
47a17ae
left comment about further migration after new node support
hyzhak Mar 12, 2018
a7dac32
slice suggestions right before send ask
hyzhak Mar 12, 2018
e7d791b
humanize resolver
hyzhak Mar 12, 2018
0f00093
convert suggestion humanize to resolver
hyzhak Mar 12, 2018
3d1d3c5
rename humanized to more specific shortoptions
hyzhak Mar 12, 2018
81b0835
start using resolver short options instead of suggestions.humanized
hyzhak Mar 12, 2018
a1114d5
drop values field from suggestions list
hyzhak Mar 12, 2018
19186a9
add yearinterval resolver
hyzhak Mar 12, 2018
ad74c7c
use console.<function>.bind only when we have this logger
hyzhak Mar 12, 2018
9aab8af
apply _.values only when suggestion item is object
hyzhak Mar 12, 2018
173b193
only strings are allowed as items of suggestion list
hyzhak Mar 12, 2018
ce2b7a5
don't log promise resolving and proxy logging
hyzhak Mar 12, 2018
a912f40
rename yearinterval to years-interval and shortoptions to short-options
hyzhak Mar 12, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 54 additions & 40 deletions functions/actions/music-query.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
const debug = require('debug')('ia:actions:music-query:debug');
const warning = require('debug')('ia:actions:music-query:warning');
const _ = require('lodash');
const math = require('mathjs');
const mustache = require('mustache');

const dialog = require('../dialog');
const feeders = require('../extensions/feeders');
const {getSuggestionProviderForSlots} = require('../extensions/suggestions');
const humanize = require('../humanize');
const {
extractRequrements,
getMatchedTemplates,
Expand All @@ -18,6 +15,7 @@ const {
const playlist = require('../state/playlist');
const query = require('../state/query');
const availableSchemes = require('../strings').intents.musicQuery;
const {debug, warning} = require('../utils/logger')('ia:actions:music-query');

/**
* Handle music query action
Expand Down Expand Up @@ -80,10 +78,10 @@ function handler (app) {
}
}

return generateAcknowledge(app, slotScheme, newValues)
return generateAcknowledge({app, slotScheme, newValues})
.then(res => {
answer.push(res);
return generatePrompt(app, slotScheme);
return generatePrompt({app, slotScheme});
})
.then(res => {
answer.push(res);
Expand All @@ -92,7 +90,7 @@ function handler (app) {
if (groupedAnswers.speech && groupedAnswers.speech.length > 0) {
dialog.ask(app, {
speech: groupedAnswers.speech.join(' '),
suggestions: groupedAnswers.suggestions,
suggestions: groupedAnswers.suggestions.slice(0, 3),
});
} else {
// TODO: we don't have anything to say should warn about it
Expand Down Expand Up @@ -263,7 +261,7 @@ function fillSlots (app, slotScheme) {
* @param newValues
* @returns {*}
*/
function generateAcknowledge (app, slotScheme, newValues) {
function generateAcknowledge ({app, slotScheme, newValues}) {
debug('we had slots:', Object.keys(query.getSlots(app)));

const newNames = Object.keys(newValues);
Expand All @@ -283,26 +281,27 @@ function generateAcknowledge (app, slotScheme, newValues) {
newNames
);

if (validAcknowledges && validAcknowledges.length === 0) {
if (!validAcknowledges || validAcknowledges.length === 0) {
validAcknowledges = getMatchedTemplates(
acknowledgeRequirements,
newNames
);
}

if (!validAcknowledges || validAcknowledges.length === 0) {
warning(`there is no valid acknowledges for ${newNames}. Maybe we should write few?`);
return Promise.resolve(null);
}
if (!validAcknowledges || validAcknowledges.length === 0) {
warning(`there is no valid acknowledges for ${newNames}. Maybe we should write few?`);
return Promise.resolve(null);
}

debug('we have few valid acknowledges', validAcknowledges);
debug('we have partly matched acknowledges', validAcknowledges);
} else {
debug('we have exactly matched acknowledges', validAcknowledges);
}

const template = _.sample(validAcknowledges);
const context = query.getSlots(app);

// mustachejs doesn't support promises on-fly
// so we should solve all them before and fetch needed data
return resolveSlots(context, template)
return resolveSlots(app, query.getSlots(app), template)
.then(resolvedSlots => ({
speech: mustache.render(
template,
Expand All @@ -317,11 +316,10 @@ function generateAcknowledge (app, slotScheme, newValues) {
* some slots could be resolved in more friendly look
* for example we could convert creatorId to {title: <band-name>}
*
* @param context
* @param template
* @returns {Promise.<TResult>}
*/
function resolveSlots (context, template) {
function resolveSlots (app, context, template) {
debug(`resolve slots for "${template}"`);
const extensions = getRequiredExtensionHandlers(template);
debug('we get extensions:', extensions);
Expand All @@ -343,57 +341,69 @@ function resolveSlots (context, template) {
.reduce((acc, extension) => {
debug(`we get result extension.result: ${extension.result} to bake for ${extension.name}`);
return Object.assign({}, acc, {
['__' + extension.extType]: {
['__' + extension.extType]: Object.assign({}, acc['__' + extension.extType], {
[extension.name]: extension.result,
},
}),
});
}, {});
});
}

/**
* Middleware
* Fetch suggestions for slots
*
* @param app
* @param promptScheme
* @returns {Promise}
*/
function fetchSuggestions (app, promptScheme) {
function fetchSuggestions (args) {
// TODO: migrate to the `...rest` style
// once Google Firebase migrates to modern Nodej.s
const {app, promptScheme} = args;
let suggestions = promptScheme.suggestions;

if (suggestions) {
debug('have static suggestions', suggestions);
return Promise.resolve(suggestions);
return Promise.resolve(Object.assign({}, args, {suggestions}));
}

const provider = getSuggestionProviderForSlots(promptScheme.requirements);
if (!provider) {
warning(`don't have any suggestions for: ${promptScheme.requirements}. Maybe we should add them.`);
return Promise.resolve(null);
return Promise.resolve(args);
}

return provider(query.getSlots(app))
.then(res => {
const suggestions = res.items.slice(0, 3);
let suggestions;
if (promptScheme.suggestionTemplate) {
return suggestions.map(
suggestions = res.items.map(
item => mustache.render(promptScheme.suggestionTemplate, item)
);
} else {
return suggestions.map(
item => _.values(item).join(' ')
suggestions = res.items.map(
item => {
if (typeof item === 'object') {
return _.values(item).join(' ');
} else {
return item;
}
}
);
}
return Object.assign({}, args, {suggestions});
});
}

/**
* Generate prompt for missed slots
*
* @param app
* @param slotScheme
* @returns {*}
*/
function generatePrompt (app, slotScheme) {
function generatePrompt ({app, slotScheme}) {
const missedSlots =
slotScheme.slots
.filter(slotName => !query.hasSlot(app, slotName));
Expand All @@ -414,23 +424,27 @@ function generatePrompt (app, slotScheme) {
return Promise.resolve(null);
}

const prompt = _.sample(promptScheme.prompts);

debug('we randombly choice prompt:', prompt);
return fetchSuggestions(app, promptScheme)
.then((suggestions) => {
const speech = mustache.render(prompt, {
// TODO: pass all slots and suggestions as context
suggestions: {
humanized: humanize.list.toFriendlyString(suggestions, {ends: ' or '}),
values: suggestions,
},
});
const context = query.getSlots(app);
const template = _.sample(promptScheme.prompts);

debug('we randomly choice prompt:', template);
let suggestions;
return fetchSuggestions({app, promptScheme})
.then((res) => {
suggestions = res.suggestions;
return resolveSlots(app, Object.assign({}, context, {suggestions}), template);
})
.then(resolvedValues => {
const speech = mustache.render(template,
Object.assign({}, context, resolvedValues, {suggestions})
);

return {speech, suggestions};
});
}

module.exports = {
handler,
fetchSuggestions,
resolveSlots,
};
4 changes: 4 additions & 0 deletions functions/dialog/ask.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ module.exports = function (app, {speech, reprompt = null, suggestions = null}) {
if (!suggestions) {
app.ask(speech);
} else {
if (Array.isArray(suggestions)) {
suggestions = suggestions.map(s => s.toString());
}

app.ask(app.buildRichResponse()
.addSimpleResponse(speech)
.addSuggestions(suggestions));
Expand Down
1 change: 1 addition & 0 deletions functions/extensions/suggestions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const _ = require('lodash');
const providers = _([
require('./coverage-year'),
require('./creators'),
require('./years'),

// TODO: should implement suggestions feeders
// {slots: ['coverage'], handler: () => Promise.resolve({items: []});},
Expand Down
39 changes: 39 additions & 0 deletions functions/extensions/suggestions/years.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
const _ = require('lodash');

const albumsProvider = require('../../provider/albums');
const {debug, warning} = require('../../utils/logger')('ia:suggestions:years');

const MAX_YEARS = 1000;

/**
* Fetch year suggestions for the artist
* TODO: actually it should work for any query set
*
* @param context
*/
function handle (context) {
debug(`handle years suggestions for creator:${context.creatorId}`);
return albumsProvider
.fetchAlbumsByQuery(Object.assign({}, context, {
limit: MAX_YEARS,
fields: 'year',
order: 'year',
}))
.then(res => {
if (res.total === 0) {
warning('it seems we defined very strict requirements for years and got nothing');
}
if (res.total >= MAX_YEARS) {
warning('it seems we have asked years with the broad search scope. We should make it more precise to get a more relevant result.');
}

return Object.assign({}, res, {
items: _.uniq(res.items.map(i => i.year))
});
});
}

module.exports = {
handle,
slots: ['year'],
};
13 changes: 5 additions & 8 deletions functions/provider/albums.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ function fetchAlbums (id, {
limit = 3,
page = 0,
order = 'downloads+desc',
fields = 'identifier,coverage,title,year',
} = {}) {
debug(`fetch albums of ${id}`);
return fetch(
Expand All @@ -69,7 +70,7 @@ function fetchAlbums (id, {
limit,
page,
order,
fields: 'identifier,coverage,title,year',
fields,
}
)
)
Expand Down Expand Up @@ -110,6 +111,7 @@ function fetchAlbums (id, {
*/
function fetchAlbumsByQuery (query) {
const {
fields = 'identifier,coverage,title,year',
limit = 3,
page = 0,
order = 'downloads+desc'
Expand All @@ -121,7 +123,6 @@ function fetchAlbumsByQuery (query) {
const condition = buildQueryCondition(query);
debug(`condition ${condition}`);

const fields = 'identifier,coverage,title,year';
debug(`Fetch albums by ${JSON.stringify(query)}`);

return fetch(
Expand All @@ -138,13 +139,9 @@ function fetchAlbumsByQuery (query) {
)
.then(res => res.json())
.then(json => ({
items: json.response.docs.map(a => ({
identifier: a.identifier,
coverage: a.coverage,
subject: a.subject,
title: a.title,
items: json.response.docs.map(a => (Object.assign({}, a, {
year: parseInt(a.year),
})),
}))),
total: json.response.numFound,
}))
.catch(e => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
const _ = require('lodash');

const {debug, error, warning} = require('../../../../utils/logger')('ia:resolver:hor:context-proxy');

module.exports = (processing) => {
/**
* Wrap to proxy context to preprocess each requested property
*
* @param context
* @returns {Promise}
*/
function handler (context) {
debug('start handling');

// Actually we could use it without Promise
// but for consistency we wrap Proxy in Promise
// becaus all the result resolver turns Promise.
return Promise.resolve(new Proxy({}, {
get: function (object, name) {
if (_.includes(['toString', 'valueOf'], name)) {
return () => `<Proxy of [context]>`;
}

if (_.includes(['inspect', 'then'], name) || (typeof name === 'symbol')) {
// those message usually is not important
// because are fired on Promise resolving and logging
return undefined;
}

if (!(name in context)) {
warning(`we don't have "${String(name)}" in context`);
return undefined;
}

const value = context[name];
if (!Array.isArray(value)) {
error('is not implemented yet!');
return undefined;
}

return processing(value);
}
}));
}

return handler;
};
15 changes: 15 additions & 0 deletions functions/slots/extensions/resolvers/short-options.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const humanize = require('../../../humanize');

const contextProxy = require('./high-order-resolvers/context-proxy');

module.exports = {
/**
* Humanize short (maximum 3) list of options
*
* @param context
* @returns {Promise}
*/
handler: contextProxy((value) =>
humanize.list.toFriendlyString(value.slice(0, 3), {ends: ' or '})
)
};
Loading