diff --git a/lib/dialogue-agent/conversation.ts b/lib/dialogue-agent/conversation.ts index 82bea66e3..4d6f661c3 100644 --- a/lib/dialogue-agent/conversation.ts +++ b/lib/dialogue-agent/conversation.ts @@ -22,14 +22,12 @@ import path from "path"; import fs from "fs"; import * as events from 'events'; -import interpolate from 'string-interp'; import type * as Tp from 'thingpedia'; import * as ThingTalk from 'thingtalk'; import * as I18n from '../i18n'; -import * as ParserClient from '../prediction/parserclient'; -import UserInput, { PlatformData } from './user-input'; +import { PlatformData } from './user-input'; import ValueCategory from './value-category'; import DialogueLoop from './dialogue-loop'; import { MessageType, Message, RDL } from './protocol'; @@ -79,19 +77,10 @@ export interface ConversationDelegate { addMessage(msg : Message) : Promise; } -interface SetContextOptions { - explicitStrings ?: boolean; -} - interface ResultLike { toLocaleString(locale ?: string) : string; } -interface PredictionCandidate { - target : UserInput; - score : number|'Infinity'; -} - class DialogueLog { private readonly _turns : DialogueTurn[]; private _done : boolean; @@ -118,6 +107,15 @@ class DialogueLog { } } +/** + * A single session of conversation in Almond. + * + * This object is responsible for maintaining the history of the conversation + * to support clients reconnecting to the same conversation later, as well + * as tracking connected clients and inactivity timeouts. + * + * The actual conversation logic is in {@link DialogueLoop}. + */ export default class Conversation extends events.EventEmitter { private _engine : Engine; private _user : AssistantUser; @@ -129,18 +127,10 @@ export default class Conversation extends events.EventEmitter { private _options : ConversationOptions; private _debug : boolean; rng : () => number; - private _prefs : Tp.Preferences; - private _nlu : ParserClient.ParserClient; - private _nlg : ParserClient.ParserClient; - - private _raw : boolean; - private _lastCommand : ParserClient.PredictionResult|null; - private _lastCandidates : PredictionCandidate[]|null; private _loop : DialogueLoop; private _expecting : ValueCategory|null; private _context : Context; - private _choices : string[]; private _delegates : Set; private _history : Message[]; @@ -170,30 +160,18 @@ export default class Conversation extends events.EventEmitter { else this._stats = stats; - this._raw = false; this._options = options; this._debug = !!this._options.debug; this.rng = options.rng || Math.random; - this._prefs = engine.platform.getSharedPreferences(); - this._nlu = ParserClient.get(this._options.nluServerUrl, engine.platform.locale, engine.platform, - undefined, engine.thingpedia); - if (this._options.nlgServerUrl) - this._nlg = ParserClient.get(this._options.nlgServerUrl, engine.platform.locale, engine.platform); - else - this._nlg = this._nlu; - this._lastCommand = null; - this._lastCandidates = null; - - this._loop = new DialogueLoop(this, this._engine, this._debug); - this._choices = []; + this._loop = new DialogueLoop(this, this._engine, { + nluServerUrl: options.nluServerUrl, + nlgServerUrl: options.nlgServerUrl, + debug: this._debug + }); this._expecting = null; - this._context = { - code: [], - entities: {} - }; - this.setContext(null); + this._context = { code: ['null'], entities: {} }; this._delegates = new Set; this._history = []; this._nextMsgId = 0; @@ -270,157 +248,18 @@ export default class Conversation extends events.EventEmitter { return this._loop.dispatchNotifyError(appId, icon, error); } - setExpected(expecting : ValueCategory|null, raw : boolean) : void { + setExpected(expecting : ValueCategory|null, context : Context) : void { this._expecting = expecting; - this._choices = []; - this._raw = raw; + this._context = context; } async start() : Promise { - await this._nlu.start(); - if (this._nlu !== this._nlg) - await this._nlg.start(); this._resetInactivityTimeout(); return this._loop.start(!!this._options.showWelcome); } async stop() : Promise { - await this._nlu.stop(); - if (this._nlu !== this._nlg) - await this._nlg.stop(); - } - - private _isUnsupportedError(e : Error) : boolean { - // FIXME there should be a better way to do this - - // 'xxx has no actions yyy' or 'xxx has no queries yyy' - // quite likely means that the NN worked but it produced a device that - // was not approved yet (otherwise the NN itself would catch the invalid function and - // skip this result) and we don't have the necessary developer key - // in that case, we reply to the user that the command is unsupported - return /(invalid kind| has no (quer(ies|y)|actions?)) /i.test(e.message); - } - - // set confident = true only if - // 1) we are not dealing with natural language (code, gui, etc), or - // 2) we find an exact match - private _doHandleCommand(intent : UserInput, - analyzed : ParserClient.PredictionResult|null, - candidates : PredictionCandidate[], - confident=false) { - this._lastCommand = analyzed; - this._lastCandidates = candidates; - return this._loop.handle(intent, confident); - } - - private _getContext(currentCommand : string|null, platformData : PlatformData) { - return { - command: currentCommand, - previousCommand: this._lastCommand, - previousCandidates: this._lastCandidates, - platformData: platformData - }; - } - - setContext(context : ThingTalk.Ast.DialogueState|null, options : SetContextOptions = {}) { - if (context === null) { - this._context = { - code: ['null'], - entities: {} - }; - } else { - const [code, entities] = ThingTalkUtils.serializeNormalized(context); - this._context = { code, entities }; - } - } - - async generateAnswer(policyPrediction : ThingTalk.Ast.DialogueState) : Promise { - const [targetAct,] = ThingTalkUtils.serializeNormalized(policyPrediction, this._context.entities); - const result = await this._nlg.generateUtterance(this._context.code, this._context.entities, targetAct); - return result[0].answer; - } - - private async _continueHandleCommand(command : string, - analyzed : ParserClient.PredictionResult, - platformData : PlatformData) : Promise { - // parse all code sequences into an Intent - // this will correctly filter out anything that does not parse - if (analyzed.candidates.length > 0) - console.log('Analyzed message into ' + analyzed.candidates[0].code.join(' ')); - else - console.log('Failed to analyze message'); - const candidates = await Promise.all(analyzed.candidates.map(async (candidate, beamposition) => { - let parsed; - try { - parsed = await UserInput.parse({ code: candidate.code, entities: analyzed.entities }, - this.thingpedia, this.schemas, this._getContext(command, platformData)); - } catch(e) { - // Likely, a type error in the ThingTalk code; not a big deal, but we still log it - console.log(`Failed to parse beam ${beamposition}: ${e.message}`); - - if (this._isUnsupportedError(e)) - parsed = new UserInput.Unsupported(command, platformData); - else - return null; - } - return { target: parsed, score: candidate.score }; - })).then((candidates) => candidates.filter((c : T) : c is Exclude => c !== null)); - - // here we used to do a complex heuristic dance of probabilities and confidence scores - // we do none of that, because Almond-NNParser does not give us useful scores - - if (candidates.length > 0) { - let i = 0; - let choice = candidates[i]; - while (i < candidates.length-1 && choice.target instanceof UserInput.Unsupported && choice.score === 'Infinity') { - i++; - choice = candidates[i]; - } - - this.stats.hit('sabrina-command-good'); - const confident = choice.score === 'Infinity'; - return this._doHandleCommand(choice.target, analyzed, candidates, confident); - } else { - this._lastCommand = analyzed; - this._lastCandidates = candidates; - - this.stats.hit('sabrina-failure'); - return this._loop.handle(new UserInput.Failed(command, platformData)); - } - } - - private async _errorWrap(fn : () => Promise, command : string|null, platformData : PlatformData) : Promise { - try { - try { - await fn(); - } catch(e) { - if (this._isUnsupportedError(e)) - await this._doHandleCommand(new UserInput.Unsupported(command, platformData), null, [], true); - else - throw e; - } - } catch(e) { - if (e.code === 'EHOSTUNREACH' || e.code === 'ETIMEDOUT') { - await this.sendReply('Sorry, I cannot contact the Almond service. Please check your Internet connection and try again later.', null); - } else if (typeof e.code === 'number' && (e.code === 404 || e.code >= 500)) { - await this.sendReply('Sorry, there seems to be a problem with the Almond service at the moment. Please try again later.', null); - } else { - await this.sendReply(interpolate(this._("Sorry, I had an error processing your command: ${error}"), { - error: e.message - }, { locale: this.platform.locale, timezone: this.platform.timezone })||'', null); - console.error(e); - } - await this._loop.reset(); - await this.sendAskSpecial(); - } - } - - private _sendUtterance(utterance : string) { - return this._nlu.sendUtterance(utterance, this._context.code, this._context.entities, { - expect: this._expecting ? String(this._expecting) : undefined, - choices: this._choices, - store: this._prefs.get('sabrina-store-log') as string || 'no' - }); + return this._loop.stop(); } private _resetInactivityTimeout() { @@ -464,8 +303,7 @@ export default class Conversation extends events.EventEmitter { await Promise.all(Array.from(this._delegates).map((out) => out.addMessage(msg))); } - async handleCommand(command : string, platformData : PlatformData = {}, - postprocess ?: (analysis : ParserClient.PredictionResult) => void) : Promise { + async handleCommand(command : string, platformData : PlatformData = {}) : Promise { this.stats.hit('sabrina-command'); this.emit('active'); this._resetInactivityTimeout(); @@ -473,22 +311,7 @@ export default class Conversation extends events.EventEmitter { if (this._debug) console.log('Received assistant command ' + command); - return this._errorWrap(async () => { - if (this._raw && command !== null) { - let value; - if (this._expecting === ValueCategory.Location) - value = new ThingTalk.Ast.LocationValue(new ThingTalk.Ast.UnresolvedLocation(command)); - else - value = new ThingTalk.Ast.Value.String(command); - const intent = new UserInput.Answer(value, command, platformData); - return this._doHandleCommand(intent, null, [], true); - } - - const analyzed = await this._sendUtterance(command); - if (postprocess) - postprocess(analyzed); - return this._continueHandleCommand(command, analyzed, platformData); - }, command, platformData); + return this._loop.handleCommand({ type: 'command', utterance: command, platformData }); } async handleParsedCommand(root : any, title ?: string, platformData : PlatformData = {}) : Promise { @@ -498,7 +321,8 @@ export default class Conversation extends events.EventEmitter { this._resetInactivityTimeout(); if (typeof root === 'string') root = JSON.parse(root); - await this._addMessage({ type: MessageType.COMMAND, command: title || '\\r ' + JSON.stringify(root), json: root }); + await this._addMessage({ type: MessageType.COMMAND, command: title || command, json: root }); + if (this._debug) console.log('Received pre-parsed assistant command'); if (root.example_id) { @@ -506,11 +330,26 @@ export default class Conversation extends events.EventEmitter { console.error('Failed to record example click: ' + e.message); }); } - return this._errorWrap(async () => { - const intent = await UserInput.parse(root, this.thingpedia, this.schemas, - this._getContext(command, platformData)); - return this._doHandleCommand(intent, null, [], true); - }, command, platformData); + + if ('program' in root) + return this.handleThingTalk(root.program, platformData); + + const { code, entities } = root; + for (const name in entities) { + if (name.startsWith('SLOT_')) { + const slotname = root.slots![parseInt(name.substring('SLOT_'.length))]; + const slotType = ThingTalk.Type.fromString(root.slotTypes![slotname]); + const value = ThingTalk.Ast.Value.fromJSON(slotType, entities[name]); + entities[name] = value; + } + } + + const parsed = await ThingTalkUtils.parsePrediction(code, entities, { + thingpediaClient: this._engine.thingpedia, + schemaRetriever: this._engine.schemas, + loadMetadata: true + }, true); + return this._loop.handleCommand({ type: 'thingtalk', parsed, platformData }); } async handleThingTalk(program : string, platformData : PlatformData = {}) : Promise { @@ -522,10 +361,12 @@ export default class Conversation extends events.EventEmitter { if (this._debug) console.log('Received ThingTalk program'); - return this._errorWrap(async () => { - const intent = await UserInput.parse({ program }, this.thingpedia, this.schemas, this._getContext(command, platformData)); - return this._doHandleCommand(intent, null, [], true); - }, command, platformData); + const parsed = await ThingTalkUtils.parse(program, { + thingpediaClient: this._engine.thingpedia, + schemaRetriever: this._engine.schemas, + loadMetadata: true + }); + return this._loop.handleCommand({ type: 'thingtalk', parsed, platformData }); } async setHypothesis(hypothesis : string) : Promise { @@ -570,21 +411,11 @@ export default class Conversation extends events.EventEmitter { sendChoice(idx : number, title : string) { if (this._expecting !== ValueCategory.MultipleChoice) console.log('UNEXPECTED: sendChoice while not expecting a MultipleChoice'); - - this._choices[idx] = title; if (this._debug) console.log('Genie sends multiple choice button: '+ title); return this._addMessage({ type: MessageType.CHOICE, idx, title }); } - async resendChoices() { - if (this._expecting !== ValueCategory.MultipleChoice) - console.log('UNEXPECTED: sendChoice while not expecting a MultipleChoice'); - - for (let idx = 0; idx < this._choices.length; idx++) - await this._addMessage({ type: MessageType.CHOICE, idx, title: this._choices[idx] }); - } - sendButton(title : string, json : string) { if (this._debug) console.log('Genie sends generic button: '+ title); @@ -597,14 +428,14 @@ export default class Conversation extends events.EventEmitter { return this._addMessage({ type: MessageType.LINK, url, title }); } - lastDialogue() { + private get _lastDialogue() { if (this._log.length === 0) return null; return this._log[this._log.length - 1]; } lastTurn() { - const lastDialogue = this.lastDialogue(); + const lastDialogue = this._lastDialogue; if (!lastDialogue || lastDialogue.turns.length === 0) throw new Error('No dialogue is logged'); return lastDialogue.turns[lastDialogue.turns.length - 1]; @@ -614,10 +445,17 @@ export default class Conversation extends events.EventEmitter { this._log.push(new DialogueLog()); } + dialogueFinished() { + const last = this._lastDialogue; + if (last) + last.finish(); + } + appendNewTurn(turn : DialogueTurn) { - if (!this.lastDialogue() || this.lastDialogue()!.done) + const last = this._lastDialogue; + if (!last || last.done) this.appendNewDialogue(); - const dialogue = this.lastDialogue()!; + const dialogue = this._lastDialogue!; dialogue.append(turn); } diff --git a/lib/dialogue-agent/dialogue-loop.ts b/lib/dialogue-agent/dialogue-loop.ts index a3206cdce..194abe844 100644 --- a/lib/dialogue-agent/dialogue-loop.ts +++ b/lib/dialogue-agent/dialogue-loop.ts @@ -23,18 +23,23 @@ import assert from 'assert'; import * as Tp from 'thingpedia'; import * as ThingTalk from 'thingtalk'; -const Ast = ThingTalk.Ast; +import { Ast, Syntax } from 'thingtalk'; import interpolate from 'string-interp'; import AsyncQueue from 'consumer-queue'; import { getProgramIcon } from '../utils/icons'; -import { computePrediction, computeNewState, prepareContextForPrediction } from '../utils/thingtalk'; +import * as ThingTalkUtils from '../utils/thingtalk'; +import { EntityMap } from '../utils/entity-utils'; import type Engine from '../engine'; +import * as ParserClient from '../prediction/parserclient'; import * as I18n from '../i18n'; import ValueCategory from './value-category'; import QueueItem from './dialogue_queue'; -import UserInput, { PlatformData } from './user-input'; +import { + UserInput, + PlatformData +} from './user-input'; import { CancellationError } from './errors'; import * as Helpers from './helpers'; @@ -46,17 +51,89 @@ import CardFormatter, { FormattedChunk } from './card-output/card-formatter'; import ExecutionDialogueAgent from './execution_dialogue_agent'; import { DialogueTurn } from "../dataset-tools/parsers"; -const ENABLE_SUGGESTIONS = false; - // TODO: load the policy.yaml file instead const POLICY_NAME = 'org.thingpedia.dialogue.transaction'; const TERMINAL_STATES = [ 'sys_end', 'sys_action_success' ]; +// Confidence thresholds: +// +// The API returns two global scores associated with +// the utterance (the "command" score and the "ignore" score), +// and a confidence score on each candidate parse. +// (There is a third score, the "other" score, which is exactly +// 1-command-ignore) +// +// (See LocalParserClient for how these scores are computed +// from the raw confidence scores produced by genienlp) +// +// - If the "ignore" score is greater than IGNORE_THRESHOLD +// we ignore this command entirely, do nothing and say +// nothing. Typically, this means a spurious wakeword activation, +// or a command that was truncated midway by the microphone. +// In Alexa, the ring would light up, turn off, and nothing would happen. +// +// - If the "command" score is less than IN_DOMAIN_THRESHOLD, +// we ship this command out to other backends silently, or tell +// the user that the command is not supported. +// +// - If we have a best valid parse, and the confidence of that parse +// is greater than CONFIDENCE_CONFIRM_THRESHOLD, we run the command +// without further confirmation. +// +// - If we have a best valid parse, and the confidence of that parse +// is greater than CONFIDENCE_FAILURE_THRESHOLD, we ask the user +// for additional confirmation before executing. +// +// - In all other cases, we tell the user we did not understand. +const CONFIDENCE_CONFIRM_THRESHOLD = 0.5; +const CONFIDENCE_FAILURE_THRESHOLD = 0.25; +const IN_DOMAIN_THRESHOLD = 0.5; +const IGNORE_THRESHOLD = 0.5; + +enum CommandAnalysisType { + // special commands - these are generated by the exact matcher, or + // by UI buttons like the "X" button + STOP, + NEVERMIND, + WAKEUP, + DEBUG, + + // some sort of command + CONFIDENT_IN_DOMAIN_COMMAND, + NONCONFIDENT_IN_DOMAIN_COMMAND, + OUT_OF_DOMAIN_COMMAND, + PARSE_FAILURE, + + // ignore this command and do nothing + IGNORE +} + +interface CommandAnalysisResult { + type : CommandAnalysisType; + utterance : string; + + // not null if this command was generated as a ThingTalk $answer() + // only used by legacy ask() methods + answer : Ast.Value|number|null; + + // the user target + parsed : Ast.Input; +} + +interface DialogueLoopOptions { + nluServerUrl : string|undefined; + nlgServerUrl : string|undefined; + debug : boolean; +} + export default class DialogueLoop { conversation : Conversation; engine : Engine; + + private _nlu : ParserClient.ParserClient; + private _nlg : ParserClient.ParserClient; private _textFormatter : TextFormatter; private _cardFormatter : CardFormatter; private _langPack : I18n.LanguagePack; @@ -71,35 +148,43 @@ export default class DialogueLoop { icon : string|null; expecting : ValueCategory|null; platformData : PlatformData; + private _raw = false; + private _choices : string[]; private _dialogueState : ThingTalk.Ast.DialogueState|null; private _executorState : undefined; private _lastNotificationApp : string|undefined; private _currentTurn : DialogueTurn; + private _stopped = false; private _mgrResolve : (() => void)|null; private _mgrPromise : Promise|null; constructor(conversation : Conversation, engine : Engine, - debug : boolean) { + options : DialogueLoopOptions) { this._userInputQueue = new AsyncQueue(); this._notifyQueue = new AsyncQueue(); - this._debug = debug; + this._debug = options.debug; this.conversation = conversation; this.engine = engine; this._prefs = engine.platform.getSharedPreferences(); + this._nlu = ParserClient.get(options.nluServerUrl || undefined, engine.platform.locale, engine.platform, + undefined, engine.thingpedia); + this._nlg = ParserClient.get(options.nlgServerUrl || undefined, engine.platform.locale, engine.platform); + this._langPack = I18n.get(engine.platform.locale); this._textFormatter = new TextFormatter(engine.platform.locale, engine.platform.timezone, engine.schemas); this._cardFormatter = new CardFormatter(engine.platform.locale, engine.platform.timezone, engine.schemas); this.icon = null; this.expecting = null; + this._choices = []; this.platformData = {}; this._mgrResolve = null; this._mgrPromise = null; - this._agent = new ExecutionDialogueAgent(engine, this, debug); + this._agent = new ExecutionDialogueAgent(engine, this, options.debug); this._policy = new DialoguePolicy({ thingpedia: conversation.thingpedia, schemas: conversation.schemas, @@ -144,7 +229,7 @@ export default class DialogueLoop { })||''; } - async nextIntent() : Promise { + async nextCommand() : Promise { await this.conversation.sendAskSpecial(); this._mgrPromise = null; this._mgrResolve!(); @@ -160,103 +245,178 @@ export default class DialogueLoop { } } - private async _handleUICommand(intent : UserInput.UICommand) { - switch (intent.type) { - case 'stop': - // stop means cancel, but without a failure message - throw new CancellationError(); - - case 'nevermind': - await this.reply(this._("Sorry I couldn't help on that.")); - throw new CancellationError(); - - case 'debug': - await this.reply("Current State:\n" + (this._dialogueState ? this._dialogueState.prettyprint() : "null")); - break; + private _getSpecialThingTalkType(input : Ast.Input) : CommandAnalysisType { + if (input instanceof Ast.ControlCommand) { + if (input.intent instanceof Ast.SpecialControlIntent) { + switch (input.intent.type) { + case 'stop': + return CommandAnalysisType.STOP; + case 'nevermind': + return CommandAnalysisType.NEVERMIND; + case 'wakeup': + return CommandAnalysisType.WAKEUP; + case 'debug': + return CommandAnalysisType.DEBUG; + case 'failed': + return CommandAnalysisType.PARSE_FAILURE; + } + } + } - case 'wakeup': - // nothing to do - break; + // anything else is automatically in-domain + return CommandAnalysisType.CONFIDENT_IN_DOMAIN_COMMAND; + } - default: - await this.fail(); + private _maybeGetThingTalkAnswer(input : Ast.Input) : Ast.Value|number|null { + if (input instanceof Ast.ControlCommand) { + if (input.intent instanceof Ast.SpecialControlIntent) { + switch (input.intent.type) { + case 'yes': + case 'no': + return new Ast.Value.Boolean(input.intent.type === 'yes'); + } + } else if (input.intent instanceof Ast.AnswerControlIntent + || input.intent instanceof Ast.ChoiceControlIntent) { + return input.intent.value; + } } + return null; } - private async _getFallbackExamples(command : string) { - const dataset = await this.conversation.thingpedia.getExamplesByKey(command); - const examples = ENABLE_SUGGESTIONS ? await Helpers.loadExamples(dataset, this.conversation.schemas, 5) : []; + private _prepareContextForPrediction(state : Ast.DialogueState|null, forSide : 'user'|'agent') : [string[], EntityMap] { + const prepared = ThingTalkUtils.prepareContextForPrediction(state, forSide); + return ThingTalkUtils.serializeNormalized(prepared); + } - if (examples.length === 0) { - await this.reply(this._("Sorry, I did not understand that.")); - return; + private async _analyzeCommand(command : UserInput) : Promise { + if (command.type === 'thingtalk') { + const type = this._getSpecialThingTalkType(command.parsed); + return { + type, + utterance: `\\t ${command.parsed.prettyprint()}`, + answer: this._maybeGetThingTalkAnswer(command.parsed), + parsed: command.parsed + }; } - this.conversation.stats.hit('sabrina-fallback-buttons'); + // ok so this was a natural language - // don't sort the examples, they come already sorted from Thingpedia - - await this.reply(this._("Sorry, I did not understand that. Try the following instead:")); - for (const ex of examples) - this.replyButton(Helpers.presentExample(this, ex.utterance), JSON.stringify(ex.target)); - } - - private async _computePrediction(intent : UserInput) : Promise { - // handle all intents generated internally and by the UI: - // - // - Failed when parsing fails - // - Answer when the user clicks a button, or when the agent is in "raw mode" - // - NeverMind when the user clicks the X button - // - Debug when the user clicks/types "debug" - // - WakeUp when the user says the wake word and nothing else - if (intent instanceof UserInput.Failed) { - await this._getFallbackExamples(intent.utterance!); - return null; - } - if (intent instanceof UserInput.Unsupported) { - this.icon = null; - await this.reply(this._("Sorry, I don't know how to do that yet.")); - throw new CancellationError(); + if (this._raw) { + // in "raw mode", all natural language becomes an answer + let value; + if (this.expecting === ValueCategory.Location) + value = new Ast.LocationValue(new Ast.UnresolvedLocation(command.utterance)); + else + value = new Ast.Value.String(command.utterance); + return { + type: CommandAnalysisType.CONFIDENT_IN_DOMAIN_COMMAND, + utterance: command.utterance, + answer: value, + parsed: new Ast.ControlCommand(null, new Ast.AnswerControlIntent(null, value)) + }; } - if (intent instanceof UserInput.Answer) { - const handled = await this._policy.handleAnswer(this._dialogueState, intent.value); - if (!handled) { - await this.fail(); - return null; + + // alright, let's ask parser first then + let nluResult : ParserClient.PredictionResult; + try { + const [contextCode, contextEntities] = this._prepareContextForPrediction(this._dialogueState, 'user'); + + nluResult = await this._nlu.sendUtterance(command.utterance, contextCode, contextEntities, { + expect: this.expecting ? String(this.expecting) : undefined, + choices: this._choices, + store: this._prefs.get('sabrina-store-log') as string || 'no' + }); + } catch(e) { + if (e.code === 'EHOSTUNREACH' || e.code === 'ETIMEDOUT') { + await this.reply(this._("Sorry, I cannot contact the Almond service. Please check your Internet connection and try again later."), null); + throw new CancellationError(); + } else if (typeof e.code === 'number' && (e.code === 404 || e.code >= 500)) { + await this.reply(this._("Sorry, there seems to be a problem with the Almond service at the moment. Please try again later."), null); + throw new CancellationError(); + } else { + throw e; } - return computePrediction(this._dialogueState, handled, 'user'); } - if (intent instanceof UserInput.MultipleChoiceAnswer) { - await this.fail(); - return null; + if (nluResult.intent.ignore >= IGNORE_THRESHOLD) { + this.debug('Ignored likely spurious command'); + return { + type: CommandAnalysisType.IGNORE, + utterance: command.utterance, + answer: null, + parsed: new Ast.ControlCommand(null, new Ast.SpecialControlIntent(null, 'failed')) + }; } - if (intent instanceof UserInput.Program) { - // convert thingtalk programs to dialogue states so we can use "\t" without too much typing - const prediction = new Ast.DialogueState(null, 'org.thingpedia.dialogue.transaction', 'execute', null, []); - for (const stmt of intent.program.statements) { - if (stmt instanceof Ast.Assignment) - throw new Error(`Unsupported: assignment statement`); - prediction.history.push(new Ast.DialogueHistoryItem(null, stmt, null, 'accepted')); - } - return prediction; + if (nluResult.intent.command < IN_DOMAIN_THRESHOLD) { + this.debug('Analyzed as out-of-domain command'); + return { + type: CommandAnalysisType.OUT_OF_DOMAIN_COMMAND, + utterance: command.utterance, + answer: null, + parsed: new Ast.ControlCommand(null, new Ast.SpecialControlIntent(null, 'failed')) + }; } - if (intent instanceof UserInput.UICommand) { - await this._handleUICommand(intent); - return null; + // parse all code sequences into an Intent + // this will correctly filter out anything that does not parse + const candidates = await Promise.all(nluResult.candidates.map(async (candidate, beamposition) => { + let parsed; + try { + parsed = await ThingTalkUtils.parsePrediction(candidate.code, nluResult.entities, { + thingpediaClient: this.engine.thingpedia, + schemaRetriever: this.engine.schemas, + loadMetadata: true, + }, true); + } catch(e) { + // Likely, a type error in the ThingTalk code; not a big deal, but we still log it + console.log(`Failed to parse beam ${beamposition}: ${e.message}`); + parsed = new Ast.ControlCommand(null, new Ast.SpecialControlIntent(null, 'failed')); + } + return { parsed, score: candidate.score }; + })); + // ensure that we always have at least one candidate by pushing $failed at the end + candidates.push({ parsed: new Ast.ControlCommand(null, new Ast.SpecialControlIntent(null, 'failed')), score: 0 }); + + // ignore all candidates with score==Infinity that we failed to parse + // (these are exact matches that correspond to skills not available for + // this user) + let i = 0; + let choice = candidates[i]; + let type = this._getSpecialThingTalkType(choice.parsed); + while (i < candidates.length-1 && type === CommandAnalysisType.PARSE_FAILURE && choice.score === 'Infinity') { + i++; + choice = candidates[i]; + type = this._getSpecialThingTalkType(choice.parsed); } - assert(intent instanceof UserInput.DialogueState); - return intent.prediction; + if (type === CommandAnalysisType.PARSE_FAILURE || + choice.score < CONFIDENCE_FAILURE_THRESHOLD) { + type = CommandAnalysisType.PARSE_FAILURE; + this.debug('Failed to analyze message'); + this.conversation.stats.hit('sabrina-failure'); + } else if (choice.score < CONFIDENCE_CONFIRM_THRESHOLD) { + type = CommandAnalysisType.NONCONFIDENT_IN_DOMAIN_COMMAND; + this.debug('Dubiously analyzed message into ' + choice.parsed.prettyprint()); + this.conversation.stats.hit('sabrina-command-maybe'); + } else { + this.debug('Confidently analyzed message into ' + choice.parsed.prettyprint()); + this.conversation.stats.hit('sabrina-command-good'); + } + return { + type, + utterance: command.utterance, + answer: this._maybeGetThingTalkAnswer(choice.parsed), + parsed: choice.parsed + }; } private _useNeuralNLG() : boolean { return this._prefs.get('experimental-use-neural-nlg') as boolean; } - private async _doAgentReply() : Promise<[ValueCategory|null, string, number]> { + private async _doAgentReply() : Promise<[ValueCategory|null, number]> { const oldState = this._dialogueState; + this._currentTurn.context = oldState ? oldState.prettyprint() : null; const policyResult = await this._policy.chooseAction(this._dialogueState); if (!policyResult) { @@ -265,93 +425,191 @@ export default class DialogueLoop { } let expect, utterance, entities, numResults; - if (this._useNeuralNLG()) { - [this._dialogueState, expect, , entities, numResults] = policyResult; + [this._dialogueState, expect, utterance, entities, numResults] = policyResult; - const policyPrediction = computeNewState(oldState, this._dialogueState, 'agent'); - this.debug(`Agent act:`); - this.debug(policyPrediction.prettyprint()); + const policyPrediction = ThingTalkUtils.computePrediction(oldState, this._dialogueState, 'agent'); + this._currentTurn.agent_target = policyPrediction.prettyprint(); + this.debug(`Agent act:`); + this.debug(this._currentTurn.agent_target); - const context = prepareContextForPrediction(oldState, 'agent'); - await this.conversation.setContext(context); + if (this._useNeuralNLG()) { + const [contextCode, contextEntities] = this._prepareContextForPrediction(this._dialogueState, 'agent'); - utterance = await this.conversation.generateAnswer(policyPrediction); - } else { - [this._dialogueState, expect, utterance, entities, numResults] = policyResult; + const [targetAct,] = ThingTalkUtils.serializeNormalized(policyPrediction, contextEntities); + const result = await this._nlg.generateUtterance(contextCode, contextEntities, targetAct); + utterance = result[0].answer; } utterance = this._langPack.postprocessNLG(utterance, entities, this._agent); this.icon = getProgramIcon(this._dialogueState!); await this.reply(utterance); + if (expect === null && TERMINAL_STATES.includes(this._dialogueState!.dialogueAct)) + throw new CancellationError(); - await this.setExpected(expect); - return [expect, utterance, numResults]; + return [expect, numResults]; } private _updateLog() { - if (this.conversation.inRecordingMode) { - this.conversation.appendNewTurn(this._currentTurn); - this._currentTurn = { - context: null, - agent: null, - agent_target: null, - intermediate_context: null, - user: '', - user_target: '' - }; - } + this.conversation.appendNewTurn(this._currentTurn); + this._currentTurn = { + context: null, + agent: null, + agent_target: null, + intermediate_context: null, + user: '', + user_target: '' + }; } - private async _handleUserInput(intent : UserInput) { - for (;;) { - const prediction = await this._computePrediction(intent); - if (prediction === null) { - intent = await this.nextIntent(); - continue; - } + private async _handleUICommand(type : CommandAnalysisType) { + switch (type) { + case CommandAnalysisType.STOP: + // stop means cancel, but without a failure message + throw new CancellationError(); - this._currentTurn.user = intent.utterance!; - this._currentTurn.user_target = prediction.prettyprint(); - this._updateLog(); + case CommandAnalysisType.NEVERMIND: + await this.reply(this._("Sorry I couldn't help on that.")); + throw new CancellationError(); - this._dialogueState = computeNewState(this._dialogueState, prediction, 'user'); + case CommandAnalysisType.DEBUG: + await this.reply("Current State:\n" + (this._dialogueState ? this._dialogueState.prettyprint() : "null")); + break; - this._checkPolicy(this._dialogueState.policy); - this.icon = getProgramIcon(this._dialogueState); + case CommandAnalysisType.WAKEUP: + // "wakeup" means the user said "hey almond" without anything else, + // or said "hey almond wake up", or triggered one of the LaunchIntents + // in Google Assistant or Alexa, or similar "opening" statements + // we show the welcome message if the current state is null, + // and do nothing otherwise + if (this._dialogueState === null) { + this._showWelcome(); + // keep the microphone open for a while + await this.setExpected(ValueCategory.Command); + } + + case CommandAnalysisType.IGNORE: + // do exactly nothing + break; + } + } - //this.debug(`Before execution:`); - //this.debug(this._dialogueState.prettyprint()); + private async _describeProgram(program : Ast.Input) { + const allocator = new Syntax.SequentialEntityAllocator({}); + const describer = new ThingTalkUtils.Describer(this.conversation.locale, + this.conversation.timezone, + allocator); + // retrieve the relevant primitive templates + const kinds = new Set(); + for (const [, prim] of program.iteratePrimitives(false)) + kinds.add(prim.selector.kind); + for (const kind of kinds) + describer.setDataset(kind, await this.engine.schemas.getExamplesByKind(kind)); + return this._langPack.postprocessNLG(describer.describe(program), allocator.entities, this._agent); + } - const { newDialogueState, newExecutorState, newResults } = await this._agent.execute(this._dialogueState, this._executorState); - this._dialogueState = newDialogueState; - this._executorState = newExecutorState; - this.debug(`Execution state:`); - this.debug(this._dialogueState!.prettyprint()); + private async _handleUserInput(command : UserInput) { + for (;;) { + const analyzed = await this._analyzeCommand(command); + if (analyzed.type !== CommandAnalysisType.IGNORE) { + // save the utterance and complete the turn + // skip the log if the command was ignored + this._currentTurn.user = analyzed.utterance; + this._currentTurn.user_target = analyzed.parsed.prettyprint(); + this._updateLog(); + } - const [expect, utterance, numResults] = await this._doAgentReply(); + switch (analyzed.type) { + case CommandAnalysisType.STOP: + case CommandAnalysisType.NEVERMIND: + case CommandAnalysisType.DEBUG: + case CommandAnalysisType.WAKEUP: + case CommandAnalysisType.IGNORE: + await this._handleUICommand(analyzed.type); + break; - this._currentTurn.context = this._dialogueState.prettyprint(); - this._currentTurn.agent = utterance; - this._currentTurn.agent_target = computePrediction(newDialogueState, this._dialogueState, "agent").prettyprint(); + case CommandAnalysisType.PARSE_FAILURE: + await this.fail(); + break; - if (expect === null && TERMINAL_STATES.includes(this._dialogueState!.dialogueAct)) { - this._updateLog(); + case CommandAnalysisType.OUT_OF_DOMAIN_COMMAND: + // TODO dispatch this out + await this.reply(this._("Sorry, I don't know how to do that yet.")); throw new CancellationError(); + + case CommandAnalysisType.NONCONFIDENT_IN_DOMAIN_COMMAND: { + // TODO move this to the state machine, not here + const confirmation = await this._describeProgram(analyzed.parsed!); + const question = this.interpolate(this._("Did you mean ${command}?"), { command: confirmation }); + const yesNo = await this.ask(ValueCategory.YesNo, question); + assert(yesNo instanceof Ast.BooleanValue); + if (!yesNo.value) { + // preserve the dialogue state and the expecting state here + await this.reply(this._("Sorry I couldn't help on that. Would you like to try again?")); + continue; + } + + // fallthrough to the confident case } - for (const [outputType, outputValue] of newResults.slice(0, numResults)) { - const formatted = await this._cardFormatter.formatForType(outputType, outputValue, { removeText: true }); + case CommandAnalysisType.CONFIDENT_IN_DOMAIN_COMMAND: + default: { + // everything else is an in-domain command + const prediction = await ThingTalkUtils.inputToDialogueState(this._policy, this._dialogueState, analyzed.parsed!); + if (prediction === null) { + // the command does not make sense in the current state + // do nothing and keep the current state + // (this can only occur with commands caught by the exact + // matcher like "yes" or "no") + await this.fail(); + break; + } - for (const card of formatted) - await this.replyCard(card); + await this._handleNormalDialogueCommand(prediction); + } } - if (expect === null) + // if we're not expecting any more answer from the user, + // exit this loop + // note: this does not mean the dialogue is terminated! + // state is preserved until we call reset() due to context reset + // timeout, or some command causes a CancellationError + // (typically, "never mind", or a "no" in sys_anything_else) + // + // exiting this loop means that we close the microphone + // (requiring a wakeword again to continue) and start + // processing notifications again + + if (this.expecting === null) return; + command = await this.nextCommand(); + } + } + + private async _handleNormalDialogueCommand(prediction : Ast.DialogueState) : Promise { + this._dialogueState = ThingTalkUtils.computeNewState(this._dialogueState, prediction, 'user'); + this._checkPolicy(this._dialogueState.policy); + this.icon = getProgramIcon(this._dialogueState); + + //this.debug(`Before execution:`); + //this.debug(this._dialogueState.prettyprint()); + + const { newDialogueState, newExecutorState, newResults } = await this._agent.execute(this._dialogueState, this._executorState); + this._dialogueState = newDialogueState; + this._executorState = newExecutorState; + this.debug(`Execution state:`); + this.debug(this._dialogueState!.prettyprint()); + + const [expect, numResults] = await this._doAgentReply(); - intent = await this.nextIntent(); + for (const [outputType, outputValue] of newResults.slice(0, numResults)) { + const formatted = await this._cardFormatter.formatForType(outputType, outputValue, { removeText: true }); + + for (const card of formatted) + await this.replyCard(card); } + + await this.setExpected(expect); } private async _showNotification(appId : string, @@ -407,33 +665,43 @@ export default class DialogueLoop { } } + private async _showWelcome() { + await this._doAgentReply(); + // reset the dialogue state here; if we don't, we we'll see sys_greet as an agent + // dialogue act; this is never seen in training, because in training the user speaks + // first, so it confuses the neural network + this._dialogueState = null; + // the utterance ends with "what can i do for you?", which is expect = 'generic' + // but we don't want to keep the microphone open here, we want to go back to wake-word mode + // so we unconditionally close the round here + await this.setExpected(null); + } + private async _loop(showWelcome : boolean) { // if we want to show the welcome message, we run the policy on the `null` state, which will return the sys_greet intent - if (showWelcome) { - await this._doAgentReply(); - // reset the dialogue state here; if we don't, we we'll see sys_greet as an agent - // dialogue act; this is never seen in training, because in training the user speaks - // first, so it confuses the neural network - this._dialogueState = null; - // the utterance ends with "what can i do for you?", which is expect = 'generic' - // but we don't want to keep the microphone open here, we want to go back to wake-word mode - // so we unconditionally close the round here - await this.setExpected(null); - } + if (showWelcome) + await this._showWelcome(); - for (;;) { + while (!this._stopped) { const item = await this.nextQueueItem(); try { if (item instanceof QueueItem.UserInput) { this._lastNotificationApp = undefined; - await this._handleUserInput(item.intent); + await this._handleUserInput(item.command); } else { await this._handleAPICall(item); + this.conversation.dialogueFinished(); this._dialogueState = null; } } catch(e) { if (e.code === 'ECANCELLED') { - await this.reset(); + this.icon = null; + this._dialogueState = null; + await this.setExpected(null); + // if the dialogue terminated, save the last utterance from the agent + // in a new turn with an empty utterance from the user + this._updateLog(); + this.conversation.dialogueFinished(); } else { if (item instanceof QueueItem.UserInput) { await this.replyInterp(this._("Sorry, I had an error processing your command: ${error}."), {//" @@ -465,7 +733,7 @@ export default class DialogueLoop { this._mgrResolve!(); const queueItem = await this._notifyQueue.pop(); if (queueItem instanceof QueueItem.UserInput) - this.platformData = queueItem.intent.platformData; + this.platformData = queueItem.command.platformData; else this.platformData = {}; return queueItem; @@ -478,7 +746,7 @@ export default class DialogueLoop { await this.reply(this._("Sorry, I need you to confirm the last question first.")); } else if (this.expecting === ValueCategory.MultipleChoice) { await this.reply(this._("Could you choose one of the following?")); - this.conversation.resendChoices(); + await this._resendChoices(); } else if (this.expecting === ValueCategory.Measure) { await this.reply(this._("Could you give me a measurement?")); } else if (this.expecting === ValueCategory.Number) { @@ -530,9 +798,9 @@ export default class DialogueLoop { if (expected === undefined) throw new TypeError(); this.expecting = expected; - const context = prepareContextForPrediction(this._dialogueState, 'user'); - this.conversation.setContext(context); - this.conversation.setExpected(expected, raw); + this._raw = raw; + const [contextCode, contextEntities] = this._prepareContextForPrediction(this._dialogueState, 'user'); + this.conversation.setExpected(expected, { code: contextCode, entities: contextEntities }); } /** @@ -540,7 +808,7 @@ export default class DialogueLoop { * * This is a legacy method used for certain scripted interactions. */ - async ask(expected : ValueCategory.PhoneNumber|ValueCategory.EmailAddress|ValueCategory.Location|ValueCategory.Time, + async ask(expected : ValueCategory.YesNo|ValueCategory.PhoneNumber|ValueCategory.EmailAddress|ValueCategory.Location|ValueCategory.Time, question : string, args ?: Record) : Promise { await this.replyInterp(question, args); @@ -548,39 +816,63 @@ export default class DialogueLoop { // because otherwise we send it to the parser and the parser will // likely misbehave as it's a state that we've never seen in training await this.setExpected(expected, expected === ValueCategory.Location); - let intent = await this.nextIntent(); - while (!(intent instanceof UserInput.Answer) || intent.category !== expected) { - if (intent instanceof UserInput.UICommand) - await this._handleUICommand(intent); - else + + let analyzed = await this._analyzeCommand(await this.nextCommand()); + while (analyzed.answer === null || typeof analyzed.answer === 'number' || + ValueCategory.fromType(analyzed.answer.getType()) !== expected) { + switch (analyzed.type) { + case CommandAnalysisType.STOP: + case CommandAnalysisType.NEVERMIND: + case CommandAnalysisType.DEBUG: + case CommandAnalysisType.WAKEUP: + case CommandAnalysisType.IGNORE: + await this._handleUICommand(analyzed.type); + break; + + default: await this.fail(); - intent = await this.nextIntent(); + await this.lookingFor(); + } + + analyzed = await this._analyzeCommand(await this.nextCommand()); } - return intent.value; + return analyzed.answer; } + async askChoices(question : string, choices : string[]) : Promise { await this.reply(question); this.setExpected(ValueCategory.MultipleChoice); + this._choices = choices; for (let i = 0; i < choices.length; i++) - await this.replyChoice(i, choices[i]); - let intent = await this.nextIntent(); - while (!(intent instanceof UserInput.MultipleChoiceAnswer)) { - if (intent instanceof UserInput.UICommand) - await this._handleUICommand(intent); - else + await this.conversation.sendChoice(i, choices[i]); + + let analyzed = await this._analyzeCommand(await this.nextCommand()); + while (analyzed.answer === null || typeof analyzed.answer !== 'number' + || analyzed.answer < 0 || analyzed.answer >= choices.length) { + switch (analyzed.type) { + case CommandAnalysisType.STOP: + case CommandAnalysisType.NEVERMIND: + case CommandAnalysisType.DEBUG: + case CommandAnalysisType.WAKEUP: + case CommandAnalysisType.IGNORE: + await this._handleUICommand(analyzed.type); + break; + + default: await this.fail(); - intent = await this.nextIntent(); + await this.lookingFor(); + } + + analyzed = await this._analyzeCommand(await this.nextCommand()); } - return intent.value; + return analyzed.answer; } + private async _resendChoices() { + if (this.expecting !== ValueCategory.MultipleChoice) + console.log('UNEXPECTED: sendChoice while not expecting a MultipleChoice'); - async reset() { - this.icon = null; - this._dialogueState = null; - await this.setExpected(null); - const last = this.conversation.lastDialogue(); - if (last) - last.finish(); + for (let idx = 0; idx < this._choices.length; idx++) + await this.conversation.sendChoice(idx, this._choices[idx]); } async replyInterp(msg : string, args ?: Record, icon : string|null = null) { @@ -591,6 +883,8 @@ export default class DialogueLoop { } async reply(msg : string, icon ?: string|null) { + this._currentTurn.agent = this._currentTurn.agent ? + (this._currentTurn.agent + '\n' + msg) : msg; await this.conversation.sendReply(msg, icon || this.icon); } @@ -611,10 +905,6 @@ export default class DialogueLoop { } } - async replyChoice(idx : number, title : string) { - await this.conversation.sendChoice(idx, title); - } - async replyButton(text : string, json : string) { await this.conversation.sendButton(text, json); } @@ -636,8 +926,11 @@ export default class DialogueLoop { this._pushQueueItem(item); } - start(showWelcome : boolean) { - const promise = this._waitNextIntent(); + async start(showWelcome : boolean) { + await this._nlu.start(); + await this._nlg.start(); + + const promise = this._waitNextCommand(); this._loop(showWelcome).then(() => { throw new Error('Unexpected end of dialog loop'); }, (err) => { @@ -647,16 +940,48 @@ export default class DialogueLoop { return promise; } + async stop() { + this._stopped = true; + + // wait until the dialog is ready to accept commands, then inject + // a cancellation error + await this._mgrPromise; + assert(this._mgrPromise === null); + + if (this._isInDefaultState()) + this._notifyQueue.cancelWait(new CancellationError()); + else + this._userInputQueue.cancelWait(new CancellationError()); + + await this._nlu.stop(); + await this._nlg.stop(); + } + + async reset() { + // wait until the dialog is ready to accept commands + await this._mgrPromise; + assert(this._mgrPromise === null); + + if (this._isInDefaultState()) + this._notifyQueue.cancelWait(new CancellationError()); + else + this._userInputQueue.cancelWait(new CancellationError()); + } + private _pushQueueItem(item : QueueItem) { // ensure that we have something to wait on before the next // command is handled if (!this._mgrPromise) - this._waitNextIntent(); + this._waitNextCommand(); this._notifyQueue.push(item); } - private _waitNextIntent() : Promise { + /** + * Returns a promise that will resolve when the dialogue loop is + * ready to accept the next command from the user. + */ + private _waitNextCommand() : Promise { const promise = new Promise((callback, errback) => { this._mgrResolve = callback; }); @@ -664,21 +989,21 @@ export default class DialogueLoop { return promise; } - pushIntent(intent : UserInput, confident = false) { - this._pushQueueItem(new QueueItem.UserInput(intent, confident)); + pushCommand(command : UserInput) { + this._pushQueueItem(new QueueItem.UserInput(command)); } - async handle(intent : UserInput, confident = false) : Promise { + async handleCommand(command : UserInput) : Promise { // wait until the dialog is ready to accept commands await this._mgrPromise; assert(this._mgrPromise === null); - const promise = this._waitNextIntent(); + const promise = this._waitNextCommand(); if (this._isInDefaultState()) - this.pushIntent(intent, confident); + this.pushCommand(command); else - this._userInputQueue.push(intent); + this._userInputQueue.push(command); return promise; } diff --git a/lib/dialogue-agent/dialogue_queue.ts b/lib/dialogue-agent/dialogue_queue.ts index c8983f0c3..d1308a832 100644 --- a/lib/dialogue-agent/dialogue_queue.ts +++ b/lib/dialogue-agent/dialogue_queue.ts @@ -18,7 +18,7 @@ // // Author: Giovanni Campagna -import type UserInputIntent from './user-input'; +import type { UserInput as Command } from './user-input'; class QueueItem { } @@ -27,13 +27,12 @@ type JSError = Error; namespace QueueItem { export class UserInput extends QueueItem { - constructor(public intent : UserInputIntent, - public confident : boolean) { + constructor(public command : Command) { super(); } toString() { - return `UserInput(${this.intent})`; + return `UserInput(${this.command})`; } } diff --git a/lib/dialogue-agent/user-input.ts b/lib/dialogue-agent/user-input.ts index b58620e56..4fcab903c 100644 --- a/lib/dialogue-agent/user-input.ts +++ b/lib/dialogue-agent/user-input.ts @@ -18,12 +18,9 @@ // // Author: Giovanni Campagna -import * as Tp from 'thingpedia'; -import { Ast, Type, SchemaRetriever } from 'thingtalk'; +import * as ThingTalk from 'thingtalk'; -import ValueCategory from './value-category'; import { EntityMap } from '../utils/entity-utils'; -import * as ThingTalkUtils from '../utils/thingtalk'; export interface PlatformData { contacts ?: Array<{ @@ -40,158 +37,14 @@ export interface PreparsedCommand { slotTypes ?: Record; } -function parseSpecial(intent : Ast.SpecialControlIntent, - context : { command : string|null, platformData : PlatformData }) { - switch (intent.type) { - case 'yes': - return new UserInput.Answer(new Ast.Value.Boolean(true), context.command, context.platformData); - case 'no': - return new UserInput.Answer(new Ast.Value.Boolean(false), context.command, context.platformData); - default: - return new UserInput.UICommand(intent.type, context.platformData); - } -} - -/** - * Base class for the interpretation of the input from the user, which could be - * a UI action (a button) or a natural language command. - */ -class UserInput { - utterance : string|null; +interface NaturalLanguageUserInput { + type : 'command'; + utterance : string; platformData : PlatformData; - - constructor(utterance : string|null, platformData : PlatformData) { - this.utterance = utterance; - this.platformData = platformData; - } - - static fromThingTalk(thingtalk : Ast.Input, - context : { command : string|null, platformData : PlatformData }) : UserInput { - if (thingtalk instanceof Ast.ControlCommand) { - if (thingtalk.intent instanceof Ast.SpecialControlIntent) - return parseSpecial(thingtalk.intent, context); - else if (thingtalk.intent instanceof Ast.AnswerControlIntent) - return new UserInput.Answer(thingtalk.intent.value, context.command, context.platformData); - else if (thingtalk.intent instanceof Ast.ChoiceControlIntent) - return new UserInput.MultipleChoiceAnswer(thingtalk.intent.value, context.command, context.platformData); - else - throw new TypeError(`Unrecognized bookkeeping intent`); - } else if (thingtalk instanceof Ast.Program) { - return new UserInput.Program(thingtalk, context.command, context.platformData); - } else if (thingtalk instanceof Ast.DialogueState) { - return new UserInput.DialogueState(thingtalk, context.command, context.platformData); - } else { - throw new TypeError(`Unrecognized ThingTalk command: ${thingtalk.prettyprint()}`); - } - } - - static async parse(json : { program : string }|PreparsedCommand, - thingpediaClient : Tp.BaseClient, - schemaRetriever : SchemaRetriever, - context : { command : string|null, platformData : PlatformData }) : Promise { - if ('program' in json) { - return UserInput.fromThingTalk(await ThingTalkUtils.parse(json.program, { - thingpediaClient, - schemaRetriever, - loadMetadata: true - }), context); - } - - const { code, entities } = json; - for (const name in entities) { - if (name.startsWith('SLOT_')) { - const slotname = json.slots![parseInt(name.substring('SLOT_'.length))]; - const slotType = Type.fromString(json.slotTypes![slotname]); - const value = Ast.Value.fromJSON(slotType, entities[name]); - entities[name] = value; - } - } - - const thingtalk = await ThingTalkUtils.parsePrediction(code, entities, { - thingpediaClient, - schemaRetriever, - loadMetadata: true - }, true); - return UserInput.fromThingTalk(thingtalk, context); - } } - -namespace UserInput { - /** - * A natural language command that was parsed correctly but is not supported in - * Thingpedia (it uses Thingpedia classes that are not available). - */ - export class Unsupported extends UserInput {} - - /** - * A natural language command that failed to parse entirely. - */ - export class Failed extends UserInput {} - - /** - * A special command that bypasses the neural network, or a button on the UI. - */ - export class UICommand extends UserInput { - constructor(public type : string, - platformData : PlatformData) { - super(null, platformData); - } - } - - /** - * A multiple choice answer. This can be generated by the UI button, - * or by the parser in multiple choice mode. It is only used to disambiguate - * entities and device names - */ - export class MultipleChoiceAnswer extends UserInput { - category : ValueCategory.MultipleChoice = ValueCategory.MultipleChoice; - - constructor(public value : number, - utterance : string|null, - platformData : PlatformData) { - super(utterance, platformData); - } - } - - /** - * A single, naked ThingTalk value. This can be generated by the UI pickers - * (file pickers, location pickers, contact pickers, etc.), in certain uses - * of the exact matcher, and when the agent is in raw mode. - */ - export class Answer extends UserInput { - category : ValueCategory; - - constructor(public value : Ast.Value, - utterance : string|null, - platformData : PlatformData) { - super(utterance, platformData); - this.category = ValueCategory.fromValue(value); - } - } - - /** - * A single ThingTalk program. This can come from a single-command neural network, - * or from the user typing "\t". - */ - export class Program extends UserInput { - constructor(public program : Ast.Program, - utterance : string|null, - platformData : PlatformData) { - super(utterance, platformData); - } - } - - /** - * A prediction ThingTalk dialogue state (policy, dialogue act, statements), which - * is generated by the neural network after parsing the user's input. - */ - export class DialogueState extends UserInput { - constructor(public prediction : Ast.DialogueState, - utterance : string|null, - platformData : PlatformData) { - super(utterance, platformData); - } - } +interface ThingTalkUserInput { + type : 'thingtalk'; + parsed : ThingTalk.Ast.Input; + platformData : PlatformData; } - -export default UserInput; +export type UserInput = NaturalLanguageUserInput|ThingTalkUserInput; diff --git a/lib/prediction/localparserclient.ts b/lib/prediction/localparserclient.ts index e4460fd67..c12b4e9e2 100644 --- a/lib/prediction/localparserclient.ts +++ b/lib/prediction/localparserclient.ts @@ -18,6 +18,7 @@ // // Author: Giovanni Campagna +import assert from 'assert'; import * as Tp from 'thingpedia'; import * as ThingTalk from 'thingtalk'; @@ -179,6 +180,11 @@ export default class LocalParserClient { let result : PredictionCandidate[]|null = null; let exact : string[][]|null = null; + const intent = { + command: 1, + other: 0, + ignore: 0 + }; if (tokens.length === 0) { result = [{ @@ -206,27 +212,37 @@ export default class LocalParserClient { } if (result === null) { - if (options.expect === 'Location') { - result = [{ - code: ['$answer', '(', 'new', 'Location', '(', '"', ...tokens, '"', ')', ')', ';'], - score: 1 - }]; - } else { - if (contextCode) - contextCode = this._applyPreHeuristics(contextCode); + if (contextCode) + contextCode = this._applyPreHeuristics(contextCode); - let candidates; - if (contextCode) - candidates = await this._predictor.predict(contextCode.join(' '), tokens.join(' '), answer, NLU_TASK, options.example_id); - else - candidates = await this._predictor.predict(tokens.join(' '), undefined, answer, SEMANTIC_PARSING_TASK, options.example_id); - result = candidates.map((c) => { - return { - code: c.answer.split(' '), - score: c.score - }; - }); - } + let candidates; + if (contextCode) + candidates = await this._predictor.predict(contextCode.join(' '), tokens.join(' '), answer, NLU_TASK, options.example_id); + else + candidates = await this._predictor.predict(tokens.join(' '), undefined, answer, SEMANTIC_PARSING_TASK, options.example_id); + assert(candidates.length > 0); + + result = candidates.map((c) => { + // convert is_correct and is_probably_correct scores into + // a single scale such that >0.5 is correct and >0.25 is + // probably correct + const score = (c.score.is_correct ?? 1) >= 0.5 ? 1 : + (c.score.is_probably_correct ?? 1) >= 0.5 ? 0.35 : 0.15; + return { + code: c.answer.split(' '), + score: score + }; + }); + + if (candidates[0].score.is_junk >= 0.5) + intent.ignore = 1; + else + intent.ignore = 0; + if (intent.ignore < 0.5 && candidates[0].score.is_ood >= 0.5) + intent.other = 1; + else + intent.other = 0; + intent.command = 1 - intent.ignore - intent.other; } let result2 = result!; // guaranteed not null @@ -263,11 +279,18 @@ export default class LocalParserClient { result: 'ok', tokens: tokens, candidates: result2, - entities: entities + entities: entities, + intent }; } async generateUtterance(contextCode : string[], contextEntities : EntityMap, targetAct : string[]) : Promise { - return this._predictor.predict(contextCode.join(' ') + ' ' + targetAct.join(' '), NLG_QUESTION, undefined, NLG_TASK); + const candidates = await this._predictor.predict(contextCode.join(' ') + ' ' + targetAct.join(' '), NLG_QUESTION, undefined, NLG_TASK); + return candidates.map((cand) => { + return { + answer: cand.answer, + score: cand.score.confidence ?? 1 + }; + }); } } diff --git a/lib/prediction/predictor.ts b/lib/prediction/predictor.ts index c30c155ca..ff5d71dc0 100644 --- a/lib/prediction/predictor.ts +++ b/lib/prediction/predictor.ts @@ -27,13 +27,13 @@ import JsonDatagramSocket from '../utils/json_datagram_socket'; const DEFAULT_QUESTION = 'translate from english to thingtalk'; -interface PredictionCandidate { +export interface RawPredictionCandidate { answer : string; - score : number; + score : Record; } interface Request { - resolve(data : PredictionCandidate[][]) : void; + resolve(data : RawPredictionCandidate[][]) : void; reject(err : Error) : void; } @@ -46,7 +46,7 @@ interface Example { answer ?: string; example_id ?: string; - resolve(data : PredictionCandidate[]) : void; + resolve(data : RawPredictionCandidate[]) : void; reject(err : Error) : void; } @@ -114,14 +114,16 @@ class LocalWorker extends events.EventEmitter { if (msg.error) { req.reject(new Error(msg.error)); } else { - req.resolve(msg.instances.map((instance : any) : PredictionCandidate[] => { + req.resolve(msg.instances.map((instance : any) : RawPredictionCandidate[] => { if (instance.candidates) { return instance.candidates; } else { - // no beam search, hence only one candidate, and fixed score + // no beam search, hence only one candidate + // the score might present or not, depending on whether + // we calibrate or not return [{ answer: instance.answer, - score: 1 + score: instance.score || {} }]; } })); @@ -136,7 +138,7 @@ class LocalWorker extends events.EventEmitter { this._requests.clear(); } - request(task : string, minibatch : Example[]) : Promise { + request(task : string, minibatch : Example[]) : Promise { const id = this._nextId ++; return new Promise((resolve, reject) => { @@ -164,23 +166,21 @@ class RemoteWorker extends events.EventEmitter { start() {} stop() {} - async request(task : string, minibatch : Example[]) : Promise { + async request(task : string, minibatch : Example[]) : Promise { const response = await Tp.Helpers.Http.post(this._url, JSON.stringify({ - id: 0, // should be ignored task, instances: minibatch }), { dataContentType: 'application/json', accept: 'application/json' }); - const parsed = JSON.parse(response); - // TODO: this needs to be updated when genienlp kfserver is fixed to avoid - // double wrapping in JSON - return JSON.parse(parsed.predictions).instances.map((instance : any) : PredictionCandidate[] => { + return JSON.parse(response).predictions.map((instance : any) : RawPredictionCandidate[] => { if (instance.candidates) { return instance.candidates; } else { - // no beam search, hence only one candidate, and fixed score + // no beam search, hence only one candidate + // the score might present or not, depending on whether + // we calibrate or not return [{ answer: instance.answer, - score: 1 + score: instance.score || {} }]; } }); @@ -254,7 +254,7 @@ export default class Predictor { } } - predict(context : string, question = DEFAULT_QUESTION, answer ?: string, task = 'almond', example_id ?: string) : Promise { + predict(context : string, question = DEFAULT_QUESTION, answer ?: string, task = 'almond', example_id ?: string) : Promise { assert(typeof context === 'string'); assert(typeof question === 'string'); @@ -262,9 +262,9 @@ export default class Predictor { if (!this._worker) this.start(); - let resolve ! : (data : PredictionCandidate[]) => void, + let resolve ! : (data : RawPredictionCandidate[]) => void, reject ! : (err : Error) => void; - const promise = new Promise((_resolve, _reject) => { + const promise = new Promise((_resolve, _reject) => { resolve = _resolve; reject = _reject; }); diff --git a/lib/prediction/types.ts b/lib/prediction/types.ts index 8f13259d1..2d2c3bc9e 100644 --- a/lib/prediction/types.ts +++ b/lib/prediction/types.ts @@ -40,11 +40,22 @@ export interface PredictionCandidate { score : number|'Infinity'; } +// this type matches the NLP web API exactly, including some +// odd aspects around "intent" export interface PredictionResult { result : 'ok'; tokens : string[]; entities : EntityMap; candidates : PredictionCandidate[]; + + // the server's best guess of whether this is a command (in-domain), + // an out of domain command (could be a new function, web question, or + // chatty sentence), or should be ignored altogether + intent : { + command : number; + other : number; + ignore : number; + } } export interface GenerationResult { diff --git a/lib/utils/thingtalk/index.ts b/lib/utils/thingtalk/index.ts index ef7398d2a..de170cbbb 100644 --- a/lib/utils/thingtalk/index.ts +++ b/lib/utils/thingtalk/index.ts @@ -31,6 +31,7 @@ import { extractConstants, createConstants } from './constants'; export * from './describe'; export * from './syntax'; export * from './dialogue_state_utils'; +import { computePrediction } from './dialogue_state_utils'; export * from './example-utils'; // reexport clean from misc-utils import { clean } from '../misc-utils'; @@ -130,3 +131,54 @@ class StateValidator { export function createStateValidator(policyManifest ?: string) : StateValidator { return new StateValidator(policyManifest); } + +interface DialoguePolicy { + handleAnswer(state : Ast.DialogueState, value : Ast.Value) : Promise; +} + +export async function inputToDialogueState(policy : DialoguePolicy, + context : Ast.DialogueState|null, + input : Ast.Input) : Promise { + if (input instanceof Ast.ControlCommand) { + if (context === null) + return null; + + if (input.intent instanceof Ast.SpecialControlIntent) { + switch (input.intent.type) { + case 'yes': + case 'no': { + const value = new Ast.BooleanValue(input.intent.type === 'yes'); + const handled = await policy.handleAnswer(context, value); + if (!handled) + return null; + return computePrediction(context, handled, 'user'); + } + default: + return null; + } + } + if (input.intent instanceof Ast.ChoiceControlIntent) + return null; + + if (input.intent instanceof Ast.AnswerControlIntent) { + const handled = await policy.handleAnswer(context, input.intent.value); + if (!handled) + return null; + return computePrediction(context, handled, 'user'); + } + + throw new TypeError(`Unrecognized bookkeeping intent`); + } else if (input instanceof Ast.Program) { + // convert thingtalk programs to dialogue states so we can use "\t" without too much typing + const prediction = new Ast.DialogueState(null, 'org.thingpedia.dialogue.transaction', 'execute', null, []); + for (const stmt of input.statements) { + if (stmt instanceof Ast.Assignment) + throw new Error(`Unsupported: assignment statement`); + prediction.history.push(new Ast.DialogueHistoryItem(null, stmt, null, 'accepted')); + } + return prediction; + } + + assert(input instanceof Ast.DialogueState); + return input; +} diff --git a/test/agent/expected-log.txt b/test/agent/expected-log.txt index ed3852817..c08862b48 100644 --- a/test/agent/expected-log.txt +++ b/test/agent/expected-log.txt @@ -1,29 +1,30 @@ ==== # test U: \t $dialogue @org.thingpedia.dialogue.transaction.execute; -U: now => @com.facebook.post(); +U: @com.facebook.post(); UT: $dialogue @org.thingpedia.dialogue.transaction.execute; UT: @com.facebook.post(); -C: $dialogue @org.thingpedia.dialogue.transaction.sys_slot_fill(status); +C: $dialogue @org.thingpedia.dialogue.transaction.execute; C: @com.facebook(id="com.facebook-6"^^tt:device_id("Some Device 6")).post(); A: What do you want to post? AT: $dialogue @org.thingpedia.dialogue.transaction.sys_slot_fill(status); AT: @com.facebook(id="com.facebook-6"^^tt:device_id("Some Device 6")).post(); U: \t $dialogue @org.thingpedia.dialogue.transaction.execute; -U: now => @com.facebook.post(status="hello world"); +U: @com.facebook.post(status="hello world"); UT: $dialogue @org.thingpedia.dialogue.transaction.execute; UT: @com.facebook.post(status="hello world"); -C: $dialogue @org.thingpedia.dialogue.transaction.sys_confirm_action; +C: $dialogue @org.thingpedia.dialogue.transaction.execute; C: @com.facebook(id="com.facebook-6"^^tt:device_id("Some Device 6")).post(status="hello world"); A: Ok, do you want me to post hello world on Facebook? AT: $dialogue @org.thingpedia.dialogue.transaction.sys_confirm_action; AT: @com.facebook(id="com.facebook-6"^^tt:device_id("Some Device 6")).post(status="hello world"); U: \t $dialogue @org.thingpedia.dialogue.transaction.execute; -U: now => @com.facebook.post(status="hello world") #[confirm=enum(confirmed)]; +U: @com.facebook.post(status="hello world") +U: #[confirm=enum confirmed]; UT: $dialogue @org.thingpedia.dialogue.transaction.execute; UT: @com.facebook.post(status="hello world") UT: #[confirm=enum confirmed]; -C: $dialogue @org.thingpedia.dialogue.transaction.sys_action_success; +C: $dialogue @org.thingpedia.dialogue.transaction.execute; C: @com.facebook(id="com.facebook-6"^^tt:device_id("Some Device 6")).post(status="hello world") C: #[results=[]]; A: I posted hello world on Facebook. @@ -34,29 +35,33 @@ AT: $dialogue @org.thingpedia.dialogue.transaction.sys_action_success; #! lines ==== # test +U: \t $stop; +UT: $stop; +==== +# test U: \t $dialogue @org.thingpedia.dialogue.transaction.execute; -U: now => @com.facebook.post(); +U: @com.facebook.post(); UT: $dialogue @org.thingpedia.dialogue.transaction.execute; UT: @com.facebook.post(); -C: $dialogue @org.thingpedia.dialogue.transaction.sys_slot_fill(status); +C: $dialogue @org.thingpedia.dialogue.transaction.execute; C: @com.facebook(id="com.facebook-6"^^tt:device_id("Some Device 6")).post(); A: What do you want to post? AT: $dialogue @org.thingpedia.dialogue.transaction.sys_slot_fill(status); AT: @com.facebook(id="com.facebook-6"^^tt:device_id("Some Device 6")).post(); U: hello world -UT: $dialogue @org.thingpedia.dialogue.transaction.execute; -UT: @com.facebook(id="com.facebook-6"^^tt:device_id("Some Device 6")).post(status="hello world"); -C: $dialogue @org.thingpedia.dialogue.transaction.sys_confirm_action; +UT: $answer("hello world"); +C: $dialogue @org.thingpedia.dialogue.transaction.execute; C: @com.facebook(id="com.facebook-6"^^tt:device_id("Some Device 6")).post(status="hello world"); A: Ok, do you want me to post hello world on Facebook? AT: $dialogue @org.thingpedia.dialogue.transaction.sys_confirm_action; AT: @com.facebook(id="com.facebook-6"^^tt:device_id("Some Device 6")).post(status="hello world"); U: \t $dialogue @org.thingpedia.dialogue.transaction.execute; -U: now => @com.facebook.post(status="hello world") #[confirm=enum(confirmed)]; +U: @com.facebook.post(status="hello world") +U: #[confirm=enum confirmed]; UT: $dialogue @org.thingpedia.dialogue.transaction.execute; UT: @com.facebook.post(status="hello world") UT: #[confirm=enum confirmed]; -C: $dialogue @org.thingpedia.dialogue.transaction.sys_action_success; +C: $dialogue @org.thingpedia.dialogue.transaction.execute; C: @com.facebook(id="com.facebook-6"^^tt:device_id("Some Device 6")).post(status="hello world") C: #[results=[]]; A: I posted hello world on Facebook. @@ -65,7 +70,7 @@ AT: $dialogue @org.thingpedia.dialogue.transaction.sys_action_success; # test U: \t $dialogue @org.thingpedia.dialogue.transaction.end; UT: $dialogue @org.thingpedia.dialogue.transaction.end; -C: $dialogue @org.thingpedia.dialogue.transaction.sys_end; +C: $dialogue @org.thingpedia.dialogue.transaction.end; A: Alright, bye! AT: $dialogue @org.thingpedia.dialogue.transaction.sys_end; #! vote: up @@ -74,30 +79,35 @@ AT: $dialogue @org.thingpedia.dialogue.transaction.sys_end; #! lines ==== # test +U: \t $stop; +UT: $stop; +==== +# test U: \t $dialogue @org.thingpedia.dialogue.transaction.execute; -U: now => @com.twitter.post(); +U: @com.twitter.post(); UT: $dialogue @org.thingpedia.dialogue.transaction.execute; UT: @com.twitter.post(); -C: $dialogue @org.thingpedia.dialogue.transaction.sys_slot_fill(status); +C: $dialogue @org.thingpedia.dialogue.transaction.execute; C: @com.twitter(id="twitter-foo"^^tt:device_id("Twitter Account foo")).post(); A: What do you want to tweet? AT: $dialogue @org.thingpedia.dialogue.transaction.sys_slot_fill(status); AT: @com.twitter(id="twitter-foo"^^tt:device_id("Twitter Account foo")).post(); U: \t $dialogue @org.thingpedia.dialogue.transaction.execute; -U: now => @com.twitter.post(status="hello world"); +U: @com.twitter.post(status="hello world"); UT: $dialogue @org.thingpedia.dialogue.transaction.execute; UT: @com.twitter.post(status="hello world"); -C: $dialogue @org.thingpedia.dialogue.transaction.sys_confirm_action; +C: $dialogue @org.thingpedia.dialogue.transaction.execute; C: @com.twitter(id="twitter-foo"^^tt:device_id("Twitter Account foo")).post(status="hello world"); A: Ok, do you want me to tweet hello world? AT: $dialogue @org.thingpedia.dialogue.transaction.sys_confirm_action; AT: @com.twitter(id="twitter-foo"^^tt:device_id("Twitter Account foo")).post(status="hello world"); U: \t $dialogue @org.thingpedia.dialogue.transaction.execute; -U: now => @com.twitter.post(status="hello world") #[confirm=enum(confirmed)]; +U: @com.twitter.post(status="hello world") +U: #[confirm=enum confirmed]; UT: $dialogue @org.thingpedia.dialogue.transaction.execute; UT: @com.twitter.post(status="hello world") UT: #[confirm=enum confirmed]; -C: $dialogue @org.thingpedia.dialogue.transaction.sys_action_success; +C: $dialogue @org.thingpedia.dialogue.transaction.execute; C: @com.twitter(id="twitter-foo"^^tt:device_id("Twitter Account foo")).post(status="hello world") C: #[results=[]]; A: I tweeted hello world. @@ -108,11 +118,15 @@ AT: $dialogue @org.thingpedia.dialogue.transaction.sys_action_success; #! lines ==== # test +U: \t $stop; +UT: $stop; +==== +# test U: \t $dialogue @org.thingpedia.dialogue.transaction.execute; -U: now => @com.yelp.restaurant() => notify; +U: @com.yelp.restaurant(); UT: $dialogue @org.thingpedia.dialogue.transaction.execute; UT: @com.yelp.restaurant(); -C: $dialogue @org.thingpedia.dialogue.transaction.sys_recommend_two; +C: $dialogue @org.thingpedia.dialogue.transaction.execute; C: @com.yelp.restaurant() C: #[results=[ C: { id="vhfPni9pci29SEHrN1OtRg"^^com.yelp:restaurant("Ramen Nagi"), image_url="https://s3-media3.fl.yelpcdn.com/bphoto/OKCXWIEFIkNdvkqETl0Bqw/o.jpg"^^tt:picture, link="https://www.yelp.com/biz/ramen-nagi-palo-alto?adjust_creative=hejPBQRox5iXtqGPiDw4dg&utm_campaign=yelp_api_v3&utm_medium=api_v3_business_search&utm_source=hejPBQRox5iXtqGPiDw4dg"^^tt:url, cuisines=["ramen"^^com.yelp:restaurant_cuisine("Ramen"), "noodles"^^com.yelp:restaurant_cuisine("Noodles")], price=enum moderate, rating=4.5, reviewCount=1625, geo=new Location(37.445523, -122.1607073261) }, @@ -131,10 +145,10 @@ C: #[more=true]; A: I have Ramen Nagi or Evvia Estiatorio. They're restaurants rated 4.5 star. AT: $dialogue @org.thingpedia.dialogue.transaction.sys_recommend_two; U: \t $dialogue @org.thingpedia.dialogue.transaction.execute; -U: now => [price] of @com.yelp.restaurant(), id == "vhfPni9pci29SEHrN1OtRg"^^com.yelp:restaurant => notify; +U: [price] of @com.yelp.restaurant() filter id == "vhfPni9pci29SEHrN1OtRg"^^com.yelp:restaurant; UT: $dialogue @org.thingpedia.dialogue.transaction.execute; UT: [price] of @com.yelp.restaurant() filter id == "vhfPni9pci29SEHrN1OtRg"^^com.yelp:restaurant; -C: $dialogue @org.thingpedia.dialogue.transaction.sys_recommend_one; +C: $dialogue @org.thingpedia.dialogue.transaction.execute; C: @com.yelp.restaurant() C: #[results=[ C: { id="vhfPni9pci29SEHrN1OtRg"^^com.yelp:restaurant("Ramen Nagi"), image_url="https://s3-media3.fl.yelpcdn.com/bphoto/OKCXWIEFIkNdvkqETl0Bqw/o.jpg"^^tt:picture, link="https://www.yelp.com/biz/ramen-nagi-palo-alto?adjust_creative=hejPBQRox5iXtqGPiDw4dg&utm_campaign=yelp_api_v3&utm_medium=api_v3_business_search&utm_source=hejPBQRox5iXtqGPiDw4dg"^^tt:url, cuisines=["ramen"^^com.yelp:restaurant_cuisine("Ramen"), "noodles"^^com.yelp:restaurant_cuisine("Noodles")], price=enum moderate, rating=4.5, reviewCount=1625, geo=new Location(37.445523, -122.1607073261) }, @@ -157,10 +171,10 @@ C: ]]; A: Ramen Nagi is a moderate restaurant. AT: $dialogue @org.thingpedia.dialogue.transaction.sys_recommend_one; U: \t $dialogue @org.thingpedia.dialogue.transaction.execute; -U: now => @com.yelp.restaurant(), price == enum(expensive) => notify; +U: @com.yelp.restaurant() filter price == enum expensive; UT: $dialogue @org.thingpedia.dialogue.transaction.execute; UT: @com.yelp.restaurant() filter price == enum expensive; -C: $dialogue @org.thingpedia.dialogue.transaction.sys_recommend_three; +C: $dialogue @org.thingpedia.dialogue.transaction.execute; C: @com.yelp.restaurant() C: #[results=[ C: { id="vhfPni9pci29SEHrN1OtRg"^^com.yelp:restaurant("Ramen Nagi"), image_url="https://s3-media3.fl.yelpcdn.com/bphoto/OKCXWIEFIkNdvkqETl0Bqw/o.jpg"^^tt:picture, link="https://www.yelp.com/biz/ramen-nagi-palo-alto?adjust_creative=hejPBQRox5iXtqGPiDw4dg&utm_campaign=yelp_api_v3&utm_medium=api_v3_business_search&utm_source=hejPBQRox5iXtqGPiDw4dg"^^tt:url, cuisines=["ramen"^^com.yelp:restaurant_cuisine("Ramen"), "noodles"^^com.yelp:restaurant_cuisine("Noodles")], price=enum moderate, rating=4.5, reviewCount=1625, geo=new Location(37.445523, -122.1607073261) }, @@ -197,7 +211,7 @@ A: I see Evvia Estiatorio, NOLA Restaurant & Bar and Tamarine Restaurant. They'r AT: $dialogue @org.thingpedia.dialogue.transaction.sys_recommend_three; U: \t $dialogue @org.thingpedia.dialogue.transaction.cancel; UT: $dialogue @org.thingpedia.dialogue.transaction.cancel; -C: $dialogue @org.thingpedia.dialogue.transaction.sys_end; +C: $dialogue @org.thingpedia.dialogue.transaction.cancel; C: @com.yelp.restaurant() C: #[results=[ C: { id="vhfPni9pci29SEHrN1OtRg"^^com.yelp:restaurant("Ramen Nagi"), image_url="https://s3-media3.fl.yelpcdn.com/bphoto/OKCXWIEFIkNdvkqETl0Bqw/o.jpg"^^tt:picture, link="https://www.yelp.com/biz/ramen-nagi-palo-alto?adjust_creative=hejPBQRox5iXtqGPiDw4dg&utm_campaign=yelp_api_v3&utm_medium=api_v3_business_search&utm_source=hejPBQRox5iXtqGPiDw4dg"^^tt:url, cuisines=["ramen"^^com.yelp:restaurant_cuisine("Ramen"), "noodles"^^com.yelp:restaurant_cuisine("Noodles")], price=enum moderate, rating=4.5, reviewCount=1625, geo=new Location(37.445523, -122.1607073261) }, @@ -238,11 +252,15 @@ AT: $dialogue @org.thingpedia.dialogue.transaction.sys_end; #! lines ==== # test +U: \t $stop; +UT: $stop; +==== +# test U: \t $dialogue @org.thingpedia.dialogue.transaction.execute; -U: now => @org.thingpedia.weather.current(location=new Location(37.442156, -122.1634471, "Palo Alto, California")) => notify; +U: @org.thingpedia.weather.current(location=new Location(37.442156, -122.1634471, "Palo Alto, California")); UT: $dialogue @org.thingpedia.dialogue.transaction.execute; UT: @org.thingpedia.weather.current(location=new Location(37.442156, -122.1634471, "Palo Alto, California")); -C: $dialogue @org.thingpedia.dialogue.transaction.sys_display_result; +C: $dialogue @org.thingpedia.dialogue.transaction.execute; C: @org.thingpedia.weather.current(location=new Location(37.442156, -122.1634471, "Palo Alto, California")) C: #[results=[ C: { location=new Location(37.442156, -122.1634471, "Palo Alto, California"), temperature=15.6C, wind_speed=3.4mps, humidity=91.3, cloudiness=4.7, fog=0, status=enum sunny, icon="http://api.met.no/weatherapi/weathericon/1.1/?symbol=1;content_type=image/png"^^tt:picture } @@ -254,16 +272,29 @@ AT: $dialogue @org.thingpedia.dialogue.transaction.sys_display_result; #! additional #! lines U: \t $dialogue @org.thingpedia.dialogue.transaction.execute; -U: now => [temperature] of @org.thingpedia.weather.current(location=new Location(37.442156, -122.1634471, "Palo Alto, California")) => notify; +U: [temperature] of @org.thingpedia.weather.current(location=new Location(37.442156, -122.1634471, "Palo Alto, California")); UT: $dialogue @org.thingpedia.dialogue.transaction.execute; UT: [temperature] of @org.thingpedia.weather.current(location=new Location(37.442156, -122.1634471, "Palo Alto, California")); +C: $dialogue @org.thingpedia.dialogue.transaction.execute; +C: @org.thingpedia.weather.current(location=new Location(37.442156, -122.1634471, "Palo Alto, California")) +C: #[results=[ +C: { location=new Location(37.442156, -122.1634471, "Palo Alto, California"), temperature=15.6C, wind_speed=3.4mps, humidity=91.3, cloudiness=4.7, fog=0, status=enum sunny, icon="http://api.met.no/weatherapi/weathericon/1.1/?symbol=1;content_type=image/png"^^tt:picture } +C: ]]; +C: [temperature] of @org.thingpedia.weather.current(location=new Location(37.442156, -122.1634471, "Palo Alto, California")) +C: #[results=[ +C: { location=new Location(37.442156, -122.1634471, "Palo Alto, California"), temperature=15.6C, status=enum sunny } +C: ]]; +A: The temperature is 60.1 F. +AT: $dialogue @org.thingpedia.dialogue.transaction.sys_display_result; +U: \t $stop; +UT: $stop; ==== # test U: \t $dialogue @org.thingpedia.dialogue.transaction.execute; -U: now => @org.thingpedia.weather.current(location=$?) => notify; +U: @org.thingpedia.weather.current(location=$?); UT: $dialogue @org.thingpedia.dialogue.transaction.execute; UT: @org.thingpedia.weather.current(location=$?); -C: $dialogue @org.thingpedia.dialogue.transaction.sys_slot_fill(location); +C: $dialogue @org.thingpedia.dialogue.transaction.execute; C: @org.thingpedia.weather.current(location=$?); A: What location do you want the current weather for? AT: $dialogue @org.thingpedia.dialogue.transaction.sys_slot_fill(location); @@ -272,16 +303,24 @@ AT: @org.thingpedia.weather.current(location=$?); #! comment: test comment for dialogue turns #! additional #! lines -U: \t bookkeeping(answer(new Location(37.442156, -122.1634471, "Palo Alto, California"))); -UT: $dialogue @org.thingpedia.dialogue.transaction.execute; -UT: @org.thingpedia.weather.current(location=new Location(37.442156, -122.1634471, "Palo Alto, California")); +U: \t $answer(new Location(37.442156, -122.1634471, "Palo Alto, California")); +UT: $answer(new Location(37.442156, -122.1634471, "Palo Alto, California")); +C: $dialogue @org.thingpedia.dialogue.transaction.execute; +C: @org.thingpedia.weather.current(location=new Location(37.442156, -122.1634471, "Palo Alto, California")) +C: #[results=[ +C: { location=new Location(37.442156, -122.1634471, "Palo Alto, California"), temperature=15.6C, wind_speed=3.4mps, humidity=91.3, cloudiness=4.7, fog=0, status=enum sunny, icon="http://api.met.no/weatherapi/weathericon/1.1/?symbol=1;content_type=image/png"^^tt:picture } +C: ]]; +A: The current weather in Palo Alto, California is sunny. The temperature is 60.1 F and the humidity is 91.3 %. +AT: $dialogue @org.thingpedia.dialogue.transaction.sys_display_result; +U: \t $stop; +UT: $stop; ==== # test U: \t $dialogue @org.thingpedia.dialogue.transaction.execute; -U: now => (@com.spotify.song(), id =~ ("despacito")) => @com.spotify.play_song(song=id); +U: @com.spotify.song() filter id =~ "despacito" => @com.spotify.play_song(song=id); UT: $dialogue @org.thingpedia.dialogue.transaction.execute; UT: @com.spotify.song() filter id =~ "despacito" => @com.spotify.play_song(song=id); -C: $dialogue @org.thingpedia.dialogue.transaction.sys_action_success; +C: $dialogue @org.thingpedia.dialogue.transaction.execute; C: (@com.spotify(id="com.spotify-7"^^tt:device_id("Some Device 7")).song() filter id =~ "despacito")[1] => @com.spotify(id="com.spotify-7"^^tt:device_id("Some Device 7")).play_song(song=id) C: #[results=[ C: { song="spotify:track:6habFhsOp2NvshLv26DqMb"^^com.spotify:song("Despacito"), device="str:ENTITY_com.spotify:device::36:"^^com.spotify:device } @@ -294,11 +333,15 @@ AT: $dialogue @org.thingpedia.dialogue.transaction.sys_action_success; #! lines ==== # test +U: \t $stop; +UT: $stop; +==== +# test U: \t $dialogue @org.thingpedia.dialogue.transaction.execute; -U: now => (@com.spotify.song(), contains(artists, null^^com.spotify:artist("ariana grande"))) => @com.spotify.play_song(song=id); +U: @com.spotify.song() filter contains(artists, null^^com.spotify:artist("ariana grande")) => @com.spotify.play_song(song=id); UT: $dialogue @org.thingpedia.dialogue.transaction.execute; UT: @com.spotify.song() filter contains(artists, null^^com.spotify:artist("ariana grande")) => @com.spotify.play_song(song=id); -C: $dialogue @org.thingpedia.dialogue.transaction.sys_action_success; +C: $dialogue @org.thingpedia.dialogue.transaction.execute; C: @com.spotify(id="com.spotify-7"^^tt:device_id("Some Device 7")).song() filter contains(artists, "spotify:artist:66CXWjxzNUsdJxJ2JdwvnR"^^com.spotify:artist("Ariana Grande")) => @com.spotify(id="com.spotify-7"^^tt:device_id("Some Device 7")).play_song(song=id) C: #[results=[ C: { song="spotify:track:6ocbgoVGwYJhOv1GgI9NsF"^^com.spotify:song("7 rings"), device="str:ENTITY_com.spotify:device::36:"^^com.spotify:device }, @@ -321,11 +364,15 @@ AT: $dialogue @org.thingpedia.dialogue.transaction.sys_action_success; #! lines ==== # test +U: \t $stop; +UT: $stop; +==== +# test U: \t $dialogue @org.thingpedia.dialogue.transaction.execute; -U: now => (@com.spotify.song(), album == null^^com.spotify:album("folklore")) => @com.spotify.play_song(song=id); +U: @com.spotify.song() filter album == null^^com.spotify:album("folklore") => @com.spotify.play_song(song=id); UT: $dialogue @org.thingpedia.dialogue.transaction.execute; UT: @com.spotify.song() filter album == null^^com.spotify:album("folklore") => @com.spotify.play_song(song=id); -C: $dialogue @org.thingpedia.dialogue.transaction.sys_action_success; +C: $dialogue @org.thingpedia.dialogue.transaction.execute; C: @com.spotify(id="com.spotify-7"^^tt:device_id("Some Device 7")).song() filter album == "spotify:album:2fenSS68JI1h4Fo296JfGr"^^com.spotify:album("folklore") => @com.spotify(id="com.spotify-7"^^tt:device_id("Some Device 7")).play_song(song=id) C: #[results=[ C: { song="spotify:track:4R2kfaDFhslZEMJqAFNpdd"^^com.spotify:song("cardigan"), device="str:ENTITY_com.spotify:device::36:"^^com.spotify:device }, @@ -348,11 +395,15 @@ AT: $dialogue @org.thingpedia.dialogue.transaction.sys_action_success; #! lines ==== # test +U: \t $stop; +UT: $stop; +==== +# test U: \t $dialogue @org.thingpedia.dialogue.transaction.execute; -U: now => @com.icanhazdadjoke.get() => notify; +U: @com.icanhazdadjoke.get(); UT: $dialogue @org.thingpedia.dialogue.transaction.execute; UT: @com.icanhazdadjoke.get(); -C: $dialogue @org.thingpedia.dialogue.transaction.sys_display_result; +C: $dialogue @org.thingpedia.dialogue.transaction.execute; C: @com.icanhazdadjoke(id="com.icanhazdadjoke-8"^^tt:device_id("Some Device 8")).get() C: #[results=[ C: { id="A5ozsH6pWnb"^^com.icanhazdadjoke:id, text="What do you call a troublesome Canadian high schooler? A poutine" } @@ -360,10 +411,10 @@ C: ]]; A: What do you call a troublesome Canadian high schooler? A poutine. AT: $dialogue @org.thingpedia.dialogue.transaction.sys_display_result; U: \t $dialogue @org.thingpedia.dialogue.transaction.execute; -U: now => @com.icanhazdadjoke.get() => notify; +U: @com.icanhazdadjoke.get(); UT: $dialogue @org.thingpedia.dialogue.transaction.execute; UT: @com.icanhazdadjoke.get(); -C: $dialogue @org.thingpedia.dialogue.transaction.sys_display_result; +C: $dialogue @org.thingpedia.dialogue.transaction.execute; C: @com.icanhazdadjoke(id="com.icanhazdadjoke-8"^^tt:device_id("Some Device 8")).get() C: #[results=[ C: { id="A5ozsH6pWnb"^^com.icanhazdadjoke:id, text="What do you call a troublesome Canadian high schooler? A poutine" } @@ -375,10 +426,10 @@ C: ]]; A: Why are fish so smart? Because they live in schools! AT: $dialogue @org.thingpedia.dialogue.transaction.sys_display_result; U: \t $dialogue @org.thingpedia.dialogue.transaction.execute; -U: now => @com.icanhazdadjoke.get() => notify; +U: @com.icanhazdadjoke.get(); UT: $dialogue @org.thingpedia.dialogue.transaction.execute; UT: @com.icanhazdadjoke.get(); -C: $dialogue @org.thingpedia.dialogue.transaction.sys_display_result; +C: $dialogue @org.thingpedia.dialogue.transaction.execute; C: @com.icanhazdadjoke(id="com.icanhazdadjoke-8"^^tt:device_id("Some Device 8")).get() C: #[results=[ C: { id="A5ozsH6pWnb"^^com.icanhazdadjoke:id, text="What do you call a troublesome Canadian high schooler? A poutine" } @@ -398,19 +449,52 @@ AT: $dialogue @org.thingpedia.dialogue.transaction.sys_display_result; #! additional #! lines U: \t $dialogue @org.thingpedia.dialogue.transaction.execute; -U: now => @com.icanhazdadjoke.get() => notify; +U: @com.icanhazdadjoke.get(); UT: $dialogue @org.thingpedia.dialogue.transaction.execute; UT: @com.icanhazdadjoke.get(); +C: $dialogue @org.thingpedia.dialogue.transaction.execute; +C: @com.icanhazdadjoke(id="com.icanhazdadjoke-8"^^tt:device_id("Some Device 8")).get() +C: #[results=[ +C: { id="A5ozsH6pWnb"^^com.icanhazdadjoke:id, text="What do you call a troublesome Canadian high schooler? A poutine" } +C: ]]; +C: @com.icanhazdadjoke(id="com.icanhazdadjoke-8"^^tt:device_id("Some Device 8")).get() +C: #[results=[ +C: { id="lGe2Tvskyd"^^com.icanhazdadjoke:id, text="Why are fish so smart?\nBecause they live in schools!" } +C: ]]; +C: @com.icanhazdadjoke(id="com.icanhazdadjoke-8"^^tt:device_id("Some Device 8")).get() +C: #[results=[ +C: { id="K6UKmykqrjb"^^com.icanhazdadjoke:id, text="What is a witch's favorite subject in school? Spelling! " } +C: ]]; +C: @com.icanhazdadjoke(id="com.icanhazdadjoke-8"^^tt:device_id("Some Device 8")).get() +C: #[results=[ +C: { id="K6UKmykqrjb"^^com.icanhazdadjoke:id, text="What is a witch's favorite subject in school? Spelling! " } +C: ]]; +A: What is a witch's favorite subject in school? Spelling! +AT: $dialogue @org.thingpedia.dialogue.transaction.sys_display_result; +U: \t $stop; +UT: $stop; ==== # test +U: \t $dialogue @org.thingpedia.dialogue.transaction.execute; +U: @com.twitter.post(); +UT: $dialogue @org.thingpedia.dialogue.transaction.execute; +UT: @com.twitter.post(); +C: $dialogue @org.thingpedia.dialogue.transaction.execute; +C: @com.twitter(id="twitter-foo"^^tt:device_id("Twitter Account foo")).post(); +A: What do you want to tweet? +AT: $dialogue @org.thingpedia.dialogue.transaction.sys_slot_fill(status); +AT: @com.twitter(id="twitter-foo"^^tt:device_id("Twitter Account foo")).post(); +U: \t $nevermind; +UT: $nevermind; +A: Sorry I couldn't help on that. #! vote: down #! comment: test comment for dialogue turns #! additional #! lines -U: \t $dialogue @org.thingpedia.dialogue.transaction.execute; -U: now => @com.twitter.post(); -UT: $dialogue @org.thingpedia.dialogue.transaction.execute; -UT: @com.twitter.post(); +==== +# test +U: \t $stop; +UT: $stop; ==== # test #! vote: up @@ -418,26 +502,57 @@ UT: @com.twitter.post(); #! additional #! lines U: \t $dialogue @org.thingpedia.dialogue.transaction.execute; -U: now => @com.yelp.restaurant(), contains(cuisines, null^^com.yelp:restaurant_cuisine("italian")) => notify; +U: @com.yelp.restaurant() filter contains(cuisines, null^^com.yelp:restaurant_cuisine("italian")); UT: $dialogue @org.thingpedia.dialogue.transaction.execute; UT: @com.yelp.restaurant() filter contains(cuisines, null^^com.yelp:restaurant_cuisine("italian")); +C: $dialogue @org.thingpedia.dialogue.transaction.execute; +C: @com.yelp.restaurant() filter contains(cuisines, "italian"^^com.yelp:restaurant_cuisine("Italian")) +C: #[results=[ +C: { id="OLa29RISTT2raUNPLo-9xw"^^com.yelp:restaurant("Patxi's Pizza"), image_url="https://s3-media3.fl.yelpcdn.com/bphoto/Jvow48SJ-SfCcxLt0a7uWg/o.jpg"^^tt:picture, link="https://www.yelp.com/biz/patxis-pizza-palo-alto?adjust_creative=hejPBQRox5iXtqGPiDw4dg&utm_campaign=yelp_api_v3&utm_medium=api_v3_business_search&utm_source=hejPBQRox5iXtqGPiDw4dg"^^tt:url, cuisines=["pizza"^^com.yelp:restaurant_cuisine("Pizza"), "salad"^^com.yelp:restaurant_cuisine("Salad"), "italian"^^com.yelp:restaurant_cuisine("Italian")], price=enum moderate, rating=3.5, reviewCount=1700, geo=new Location(37.445153, -122.163337), phone="+16504739999"^^tt:phone_number }, +C: { id="Tuq1Ht5QoISVViAlAnyrZg"^^com.yelp:restaurant("Cafe Borrone"), image_url="https://s3-media3.fl.yelpcdn.com/bphoto/jKOuPFIcqdtG47gRHqv1WQ/o.jpg"^^tt:picture, link="https://www.yelp.com/biz/cafe-borrone-menlo-park?adjust_creative=hejPBQRox5iXtqGPiDw4dg&utm_campaign=yelp_api_v3&utm_medium=api_v3_business_search&utm_source=hejPBQRox5iXtqGPiDw4dg"^^tt:url, cuisines=["breakfast_brunch"^^com.yelp:restaurant_cuisine("Breakfast & Brunch"), "italian"^^com.yelp:restaurant_cuisine("Italian"), "cafes"^^com.yelp:restaurant_cuisine("Cafes")], price=enum moderate, rating=4, reviewCount=1727, geo=new Location(37.45368, -122.18205), phone="+16503270830"^^tt:phone_number }, +C: { id="d62LElGhYd65kXXCimgPpg"^^com.yelp:restaurant("Osteria Toscana"), image_url="https://s3-media2.fl.yelpcdn.com/bphoto/eNDUayX7OPzh1MZFWB9Xpg/o.jpg"^^tt:picture, link="https://www.yelp.com/biz/osteria-toscana-palo-alto?adjust_creative=hejPBQRox5iXtqGPiDw4dg&utm_campaign=yelp_api_v3&utm_medium=api_v3_business_search&utm_source=hejPBQRox5iXtqGPiDw4dg"^^tt:url, cuisines=["italian"^^com.yelp:restaurant_cuisine("Italian")], price=enum moderate, rating=3.5, reviewCount=938, geo=new Location(37.4445, -122.16118), phone="+16503285700"^^tt:phone_number }, +C: { id="pLqiFFz1JScp8wMMyXcx-w"^^com.yelp:restaurant("Terún"), image_url="https://s3-media2.fl.yelpcdn.com/bphoto/w1yuXOCOAFcL_TVkoFed4Q/o.jpg"^^tt:picture, link="https://www.yelp.com/biz/ter%C3%BAn-palo-alto-3?adjust_creative=hejPBQRox5iXtqGPiDw4dg&utm_campaign=yelp_api_v3&utm_medium=api_v3_business_search&utm_source=hejPBQRox5iXtqGPiDw4dg"^^tt:url, cuisines=["pizza"^^com.yelp:restaurant_cuisine("Pizza"), "italian"^^com.yelp:restaurant_cuisine("Italian")], price=enum moderate, rating=4, reviewCount=1493, geo=new Location(37.425993, -122.145453), phone="+16506008310"^^tt:phone_number }, +C: { id="OZI4rK9s0xTwg2PdTRgmcw"^^com.yelp:restaurant("Aromi - Cambridge"), image_url="https://s3-media1.fl.yelpcdn.com/bphoto/eS-F_RSo1UyOumExtszsGA/o.jpg"^^tt:picture, link="https://www.yelp.com/biz/aromi-cambridge-cambridge?adjust_creative=hejPBQRox5iXtqGPiDw4dg&utm_campaign=yelp_api_v3&utm_medium=api_v3_business_search&utm_source=hejPBQRox5iXtqGPiDw4dg"^^tt:url, cuisines=["italian"^^com.yelp:restaurant_cuisine("Italian"), "cafes"^^com.yelp:restaurant_cuisine("Cafes"), "delis"^^com.yelp:restaurant_cuisine("Delis")], price=enum cheap, rating=4, reviewCount=30, geo=new Location(52.2042405, 0.1187084), phone="+441223300117"^^tt:phone_number } +C: ]]; +A: I see Patxi's Pizza, Cafe Borrone and Osteria Toscana. All of them are moderate restaurants having Italian food. +AT: $dialogue @org.thingpedia.dialogue.transaction.sys_recommend_three; +U: \t $stop; +UT: $stop; ==== # test +U: \t $dialogue @org.thingpedia.dialogue.transaction.execute; +U: @com.yelp.restaurant() filter contains(cuisines, null^^com.yelp:restaurant_cuisine("invalid")); +UT: $dialogue @org.thingpedia.dialogue.transaction.execute; +UT: @com.yelp.restaurant() filter contains(cuisines, null^^com.yelp:restaurant_cuisine("invalid")); +A: Sorry, I cannot find any Yelp Cuisine matching “invalid”. +#! vote: down +#! comment: test comment for dialogue turns +#! additional +#! lines +==== +# test +U: \t $stop; +UT: $stop; +==== +# test +U: !! test command always failed !! +UT: $failed; +A: Sorry, I did not understand that. Can you rephrase it? #! vote: up #! comment: test comment for dialogue turns #! additional #! lines -U: \t $dialogue @org.thingpedia.dialogue.transaction.execute; -U: now => @com.yelp.restaurant(), contains(cuisines, null^^com.yelp:restaurant_cuisine("invalid")) => notify; -UT: $dialogue @org.thingpedia.dialogue.transaction.execute; -UT: @com.yelp.restaurant() filter contains(cuisines, null^^com.yelp:restaurant_cuisine("invalid")); +==== +# test +U: \t $stop; +UT: $stop; ==== # test U: \t $dialogue @org.thingpedia.dialogue.transaction.execute; -U: now => @com.yelp.restaurant(), contains(cuisines, null^^com.yelp:restaurant_cuisine("italian")) => notify; +U: @com.yelp.restaurant() filter contains(cuisines, null^^com.yelp:restaurant_cuisine("italian")); UT: $dialogue @org.thingpedia.dialogue.transaction.execute; UT: @com.yelp.restaurant() filter contains(cuisines, null^^com.yelp:restaurant_cuisine("italian")); -C: $dialogue @org.thingpedia.dialogue.transaction.sys_recommend_three; +C: $dialogue @org.thingpedia.dialogue.transaction.execute; C: @com.yelp.restaurant() filter contains(cuisines, "italian"^^com.yelp:restaurant_cuisine("Italian")) C: #[results=[ C: { id="OLa29RISTT2raUNPLo-9xw"^^com.yelp:restaurant("Patxi's Pizza"), image_url="https://s3-media3.fl.yelpcdn.com/bphoto/Jvow48SJ-SfCcxLt0a7uWg/o.jpg"^^tt:picture, link="https://www.yelp.com/biz/patxis-pizza-palo-alto?adjust_creative=hejPBQRox5iXtqGPiDw4dg&utm_campaign=yelp_api_v3&utm_medium=api_v3_business_search&utm_source=hejPBQRox5iXtqGPiDw4dg"^^tt:url, cuisines=["pizza"^^com.yelp:restaurant_cuisine("Pizza"), "salad"^^com.yelp:restaurant_cuisine("Salad"), "italian"^^com.yelp:restaurant_cuisine("Italian")], price=enum moderate, rating=3.5, reviewCount=1700, geo=new Location(37.445153, -122.163337), phone="+16504739999"^^tt:phone_number }, @@ -450,7 +565,7 @@ A: I see Patxi's Pizza, Cafe Borrone and Osteria Toscana. All of them are modera AT: $dialogue @org.thingpedia.dialogue.transaction.sys_recommend_three; U: \t $dialogue @org.thingpedia.dialogue.transaction.cancel; UT: $dialogue @org.thingpedia.dialogue.transaction.cancel; -C: $dialogue @org.thingpedia.dialogue.transaction.sys_end; +C: $dialogue @org.thingpedia.dialogue.transaction.cancel; C: @com.yelp.restaurant() filter contains(cuisines, "italian"^^com.yelp:restaurant_cuisine("Italian")) C: #[results=[ C: { id="OLa29RISTT2raUNPLo-9xw"^^com.yelp:restaurant("Patxi's Pizza"), image_url="https://s3-media3.fl.yelpcdn.com/bphoto/Jvow48SJ-SfCcxLt0a7uWg/o.jpg"^^tt:picture, link="https://www.yelp.com/biz/patxis-pizza-palo-alto?adjust_creative=hejPBQRox5iXtqGPiDw4dg&utm_campaign=yelp_api_v3&utm_medium=api_v3_business_search&utm_source=hejPBQRox5iXtqGPiDw4dg"^^tt:url, cuisines=["pizza"^^com.yelp:restaurant_cuisine("Pizza"), "salad"^^com.yelp:restaurant_cuisine("Salad"), "italian"^^com.yelp:restaurant_cuisine("Italian")], price=enum moderate, rating=3.5, reviewCount=1700, geo=new Location(37.445153, -122.163337), phone="+16504739999"^^tt:phone_number }, @@ -467,11 +582,15 @@ AT: $dialogue @org.thingpedia.dialogue.transaction.sys_end; #! lines ==== # test +U: \t $stop; +UT: $stop; +==== +# test U: \t $dialogue @org.thingpedia.dialogue.transaction.execute; -U: now => @com.spotify.player_repeat(repeat=enum(track)); +U: @com.spotify.player_repeat(repeat=enum track); UT: $dialogue @org.thingpedia.dialogue.transaction.execute; UT: @com.spotify.player_repeat(repeat=enum track); -C: $dialogue @org.thingpedia.dialogue.transaction.sys_action_success; +C: $dialogue @org.thingpedia.dialogue.transaction.execute; C: @com.spotify(id="com.spotify-7"^^tt:device_id("Some Device 7")).player_repeat(repeat=enum track) C: #[results=[]]; A: I repeated this song. @@ -482,14 +601,25 @@ AT: $dialogue @org.thingpedia.dialogue.transaction.sys_action_success; #! lines ==== # test +U: \t $stop; +UT: $stop; +==== +# test #! vote: down #! comment: test comment for dialogue turns #! additional #! lines U: \t $dialogue @org.thingpedia.dialogue.transaction.execute; -U: now => @com.yelp.restaurant(), contains(cuisines, "peruvian"^^com.yelp:restaurant_cuisine("Peruvian")) => notify; +U: @com.yelp.restaurant() filter contains(cuisines, "peruvian"^^com.yelp:restaurant_cuisine("Peruvian")); UT: $dialogue @org.thingpedia.dialogue.transaction.execute; UT: @com.yelp.restaurant() filter contains(cuisines, "peruvian"^^com.yelp:restaurant_cuisine("Peruvian")); +C: $dialogue @org.thingpedia.dialogue.transaction.execute; +C: @com.yelp.restaurant() filter contains(cuisines, "peruvian"^^com.yelp:restaurant_cuisine("Peruvian")) +C: #[results=[]]; +A: Sorry, I cannot find any restaurant like that. +AT: $dialogue @org.thingpedia.dialogue.transaction.sys_empty_search; +U: \t $stop; +UT: $stop; ==== # test #! vote: up @@ -497,16 +627,23 @@ UT: @com.yelp.restaurant() filter contains(cuisines, "peruvian"^^com.yelp:restau #! additional #! lines U: \t $dialogue @org.thingpedia.dialogue.transaction.execute; -U: now => @com.spotify.song(), contains(artists, "invalid"^^com.spotify:artist("Invalid")) => @com.spotify.play_song(song=id); +U: @com.spotify.song() filter contains(artists, "invalid"^^com.spotify:artist("Invalid")) => @com.spotify.play_song(song=id); UT: $dialogue @org.thingpedia.dialogue.transaction.execute; UT: @com.spotify.song() filter contains(artists, "invalid"^^com.spotify:artist("Invalid")) => @com.spotify.play_song(song=id); +C: $dialogue @org.thingpedia.dialogue.transaction.execute; +C: @com.spotify(id="com.spotify-7"^^tt:device_id("Some Device 7")).song() filter contains(artists, "invalid"^^com.spotify:artist("Invalid")) => @com.spotify(id="com.spotify-7"^^tt:device_id("Some Device 7")).play_song(song=id) +C: #[results=[]]; +A: Sorry, I cannot find any song like that. +AT: $dialogue @org.thingpedia.dialogue.transaction.sys_empty_search; +U: \t $stop; +UT: $stop; ==== # test U: \t $dialogue @org.thingpedia.dialogue.transaction.execute; -U: now => @com.spotify.player_shuffle(shuffle=enum(on)); +U: @com.spotify.player_shuffle(shuffle=enum on); UT: $dialogue @org.thingpedia.dialogue.transaction.execute; UT: @com.spotify.player_shuffle(shuffle=enum on); -C: $dialogue @org.thingpedia.dialogue.transaction.sys_action_success; +C: $dialogue @org.thingpedia.dialogue.transaction.execute; C: @com.spotify(id="com.spotify-7"^^tt:device_id("Some Device 7")).player_shuffle(shuffle=enum on) C: #[results=[]]; A: I shuffled your Spotify. @@ -517,14 +654,27 @@ AT: $dialogue @org.thingpedia.dialogue.transaction.sys_action_success; #! lines ==== # test +U: \t $stop; +UT: $stop; +==== +# test #! vote: up #! comment: test comment for dialogue turns #! additional #! lines U: \t $dialogue @org.thingpedia.dialogue.transaction.execute; -U: now => count(@com.yelp.restaurant()) => notify; +U: count(@com.yelp.restaurant()); UT: $dialogue @org.thingpedia.dialogue.transaction.execute; UT: count(@com.yelp.restaurant()); +C: $dialogue @org.thingpedia.dialogue.transaction.execute; +C: count(@com.yelp.restaurant()) +C: #[results=[ +C: { count=60 } +C: ]]; +A: I have 60 restaurant with those characteristics. +AT: $dialogue @org.thingpedia.dialogue.transaction.sys_display_result; +U: \t $stop; +UT: $stop; ==== # test #! vote: down @@ -532,9 +682,18 @@ UT: count(@com.yelp.restaurant()); #! additional #! lines U: \t $dialogue @org.thingpedia.dialogue.transaction.execute; -U: now => count(@com.yelp.restaurant(), contains(cuisines, "italian"^^com.yelp:restaurant_cuisine("Italian"))) => notify; +U: count(@com.yelp.restaurant() filter contains(cuisines, "italian"^^com.yelp:restaurant_cuisine("Italian"))); UT: $dialogue @org.thingpedia.dialogue.transaction.execute; UT: count(@com.yelp.restaurant() filter contains(cuisines, "italian"^^com.yelp:restaurant_cuisine("Italian"))); +C: $dialogue @org.thingpedia.dialogue.transaction.execute; +C: count(@com.yelp.restaurant() filter contains(cuisines, "italian"^^com.yelp:restaurant_cuisine("Italian"))) +C: #[results=[ +C: { count=5 } +C: ]]; +A: There are 5 restaurant with those characteristics. +AT: $dialogue @org.thingpedia.dialogue.transaction.sys_display_result; +U: \t $stop; +UT: $stop; ==== # test #! vote: up @@ -542,9 +701,18 @@ UT: count(@com.yelp.restaurant() filter contains(cuisines, "italian"^^com.yelp:r #! additional #! lines U: \t $dialogue @org.thingpedia.dialogue.transaction.execute; -U: now => min(rating of (@com.yelp.restaurant())) => notify; +U: min(rating of @com.yelp.restaurant()); UT: $dialogue @org.thingpedia.dialogue.transaction.execute; UT: min(rating of @com.yelp.restaurant()); +C: $dialogue @org.thingpedia.dialogue.transaction.execute; +C: min(rating of @com.yelp.restaurant()) +C: #[results=[ +C: { rating=3.5 } +C: ]]; +A: The minimum rating is 3.5. +AT: $dialogue @org.thingpedia.dialogue.transaction.sys_display_result; +U: \t $stop; +UT: $stop; ==== # test #! vote: down @@ -552,26 +720,65 @@ UT: min(rating of @com.yelp.restaurant()); #! additional #! lines U: \t $dialogue @org.thingpedia.dialogue.transaction.execute; -U: now => avg(rating of (@com.yelp.restaurant())) => notify; +U: avg(rating of @com.yelp.restaurant()); UT: $dialogue @org.thingpedia.dialogue.transaction.execute; UT: avg(rating of @com.yelp.restaurant()); +C: $dialogue @org.thingpedia.dialogue.transaction.execute; +C: avg(rating of @com.yelp.restaurant()) +C: #[results=[ +C: { rating=4.058333333333334 } +C: ]]; +A: The average rating is 4.1. +AT: $dialogue @org.thingpedia.dialogue.transaction.sys_display_result; +U: \t $stop; +UT: $stop; ==== # test -#! vote: down +#! vote: up #! comment: test comment for dialogue turns #! additional #! lines U: \t $dialogue @org.thingpedia.dialogue.transaction.execute; -U: now => [distance(geo, $location.current_location)] of (@com.yelp.restaurant()) => notify; +U: [distance(geo, $location.current_location)] of @com.yelp.restaurant(); UT: $dialogue @org.thingpedia.dialogue.transaction.execute; UT: [distance(geo, $location.current_location)] of @com.yelp.restaurant(); +C: $dialogue @org.thingpedia.dialogue.transaction.execute; +C: [distance(geo, new Location(37.4275, -122.1697))] of @com.yelp.restaurant() +C: #[results=[ +C: { id="vhfPni9pci29SEHrN1OtRg"^^com.yelp:restaurant("Ramen Nagi"), distance=2004.0661629148217m }, +C: { id="1vMgajRAI3lYwuCeGX58oQ"^^com.yelp:restaurant("Evvia Estiatorio"), distance=1954.4732256309346m }, +C: { id="KyU7XWDPclmZ3BpdIsd_0Q"^^com.yelp:restaurant("Oren's Hummus"), distance=2025.637978683918m }, +C: { id="jwaXc3VVGDFQu1aCoiXwdw"^^com.yelp:restaurant("NOLA Restaurant & Bar"), distance=1948.4223321751008m }, +C: { id="0WDM3JlNBvk2MeOTlu03mg"^^com.yelp:restaurant("Coconuts Caribbean Restaurant & Bar"), distance=1813.5892535724372m }, +C: { id="_mkNOTiQ-kbc1kJHo9RoVA"^^com.yelp:restaurant("Tamarine Restaurant"), distance=2369.5638867955154m }, +C: { id="TtYbsOqn7vLKhva0AuKQQQ"^^com.yelp:restaurant("Philz Coffee"), distance=1631.6743535821893m }, +C: { id="pi9MMjHWtVxC9eTnMk7e_w"^^com.yelp:restaurant("Crepevine Restaurant"), distance=2166.077171036171m }, +C: { id="vStWaPrdP4ysnKpj0WoHJQ"^^com.yelp:restaurant("Rangoon Ruby"), distance=1959.476997329948m }, +C: { id="-g_tUhyvUFbxOs0aB28j1w"^^com.yelp:restaurant("Palo Alto Creamery"), distance=1821.7202848874374m } +C: ]] +C: #[count=50] +C: #[more=true]; +A: The distance is 1.2 mi. +AT: $dialogue @org.thingpedia.dialogue.transaction.sys_recommend_one; +U: \t $stop; +UT: $stop; +==== +# test +#! vote: down +#! comment: test comment for dialogue turns +#! additional +#! lines +==== +# test +U: \t $stop; +UT: $stop; ==== # test U: \t $dialogue @org.thingpedia.dialogue.transaction.execute; -U: now => (@com.yelp.restaurant(), id =~ "ramen nagi") => notify; +U: @com.yelp.restaurant() filter id =~ "ramen nagi"; UT: $dialogue @org.thingpedia.dialogue.transaction.execute; UT: @com.yelp.restaurant() filter id =~ "ramen nagi"; -C: $dialogue @org.thingpedia.dialogue.transaction.sys_recommend_one; +C: $dialogue @org.thingpedia.dialogue.transaction.execute; C: @com.yelp.restaurant() filter id =~ "ramen nagi" C: #[results=[ C: { id="vhfPni9pci29SEHrN1OtRg"^^com.yelp:restaurant("Ramen Nagi"), image_url="https://s3-media3.fl.yelpcdn.com/bphoto/OKCXWIEFIkNdvkqETl0Bqw/o.jpg"^^tt:picture, link="https://www.yelp.com/biz/ramen-nagi-palo-alto?adjust_creative=hejPBQRox5iXtqGPiDw4dg&utm_campaign=yelp_api_v3&utm_medium=api_v3_business_search&utm_source=hejPBQRox5iXtqGPiDw4dg"^^tt:url, cuisines=["ramen"^^com.yelp:restaurant_cuisine("Ramen"), "noodles"^^com.yelp:restaurant_cuisine("Noodles")], price=enum moderate, rating=4.5, reviewCount=1625, geo=new Location(37.445523, -122.1607073261) } @@ -582,24 +789,33 @@ AT: $dialogue @org.thingpedia.dialogue.transaction.sys_recommend_one; #! comment: test comment for dialogue turns #! additional #! lines -U: \r {"code":["bookkeeping","special","special:yes"],"entities":{}} -UT: $dialogue @org.thingpedia.dialogue.transaction.learn_more; +U: \t $yes; +UT: $yes; +C: $dialogue @org.thingpedia.dialogue.transaction.learn_more; +C: @com.yelp.restaurant() filter id =~ "ramen nagi" +C: #[results=[ +C: { id="vhfPni9pci29SEHrN1OtRg"^^com.yelp:restaurant("Ramen Nagi"), image_url="https://s3-media3.fl.yelpcdn.com/bphoto/OKCXWIEFIkNdvkqETl0Bqw/o.jpg"^^tt:picture, link="https://www.yelp.com/biz/ramen-nagi-palo-alto?adjust_creative=hejPBQRox5iXtqGPiDw4dg&utm_campaign=yelp_api_v3&utm_medium=api_v3_business_search&utm_source=hejPBQRox5iXtqGPiDw4dg"^^tt:url, cuisines=["ramen"^^com.yelp:restaurant_cuisine("Ramen"), "noodles"^^com.yelp:restaurant_cuisine("Noodles")], price=enum moderate, rating=4.5, reviewCount=1625, geo=new Location(37.445523, -122.1607073261) } +C: ]]; +A: What would you like to know? +AT: $dialogue @org.thingpedia.dialogue.transaction.sys_learn_more_what; +U: \t $stop; +UT: $stop; ==== # test U: \t $dialogue @org.thingpedia.dialogue.transaction.execute; -U: now => (@com.yelp.restaurant(), id =~ "ramen nagi") => notify; +U: @com.yelp.restaurant() filter id =~ "ramen nagi"; UT: $dialogue @org.thingpedia.dialogue.transaction.execute; UT: @com.yelp.restaurant() filter id =~ "ramen nagi"; -C: $dialogue @org.thingpedia.dialogue.transaction.sys_recommend_one; +C: $dialogue @org.thingpedia.dialogue.transaction.execute; C: @com.yelp.restaurant() filter id =~ "ramen nagi" C: #[results=[ C: { id="vhfPni9pci29SEHrN1OtRg"^^com.yelp:restaurant("Ramen Nagi"), image_url="https://s3-media3.fl.yelpcdn.com/bphoto/OKCXWIEFIkNdvkqETl0Bqw/o.jpg"^^tt:picture, link="https://www.yelp.com/biz/ramen-nagi-palo-alto?adjust_creative=hejPBQRox5iXtqGPiDw4dg&utm_campaign=yelp_api_v3&utm_medium=api_v3_business_search&utm_source=hejPBQRox5iXtqGPiDw4dg"^^tt:url, cuisines=["ramen"^^com.yelp:restaurant_cuisine("Ramen"), "noodles"^^com.yelp:restaurant_cuisine("Noodles")], price=enum moderate, rating=4.5, reviewCount=1625, geo=new Location(37.445523, -122.1607073261) } C: ]]; A: Ramen Nagi is a moderate restaurant near [Latitude: 37.446 deg, Longitude: -122.161 deg]. AT: $dialogue @org.thingpedia.dialogue.transaction.sys_recommend_one; -U: \r {"code":["bookkeeping","special","special:no"],"entities":{}} -UT: $dialogue @org.thingpedia.dialogue.transaction.cancel; -C: $dialogue @org.thingpedia.dialogue.transaction.sys_end; +U: \t $no; +UT: $no; +C: $dialogue @org.thingpedia.dialogue.transaction.cancel; C: @com.yelp.restaurant() filter id =~ "ramen nagi" C: #[results=[ C: { id="vhfPni9pci29SEHrN1OtRg"^^com.yelp:restaurant("Ramen Nagi"), image_url="https://s3-media3.fl.yelpcdn.com/bphoto/OKCXWIEFIkNdvkqETl0Bqw/o.jpg"^^tt:picture, link="https://www.yelp.com/biz/ramen-nagi-palo-alto?adjust_creative=hejPBQRox5iXtqGPiDw4dg&utm_campaign=yelp_api_v3&utm_medium=api_v3_business_search&utm_source=hejPBQRox5iXtqGPiDw4dg"^^tt:url, cuisines=["ramen"^^com.yelp:restaurant_cuisine("Ramen"), "noodles"^^com.yelp:restaurant_cuisine("Noodles")], price=enum moderate, rating=4.5, reviewCount=1625, geo=new Location(37.445523, -122.1607073261) } @@ -612,25 +828,26 @@ AT: $dialogue @org.thingpedia.dialogue.transaction.sys_end; #! lines ==== # test +U: \t $stop; +UT: $stop; +==== +# test U: \t $dialogue @org.thingpedia.dialogue.transaction.execute; -U: now => (@com.spotify.song(), id =~ "Nice For What") => notify; +U: @com.spotify.song() filter id =~ "Nice For What"; UT: $dialogue @org.thingpedia.dialogue.transaction.execute; UT: @com.spotify.song() filter id =~ "Nice For What"; -C: $dialogue @org.thingpedia.dialogue.transaction.sys_recommend_one; +C: $dialogue @org.thingpedia.dialogue.transaction.execute; C: @com.spotify(id="com.spotify-7"^^tt:device_id("Some Device 7")).song() filter id =~ "Nice For What" C: #[results=[ C: { id="spotify:track:3CA9pLiwRIGtUBiMjbZmRw"^^com.spotify:song("Nice For What"), artists=["spotify:artist:3TVXtAsR1Inumwj472S9r4"^^com.spotify:artist("Drake")], album="spotify:album:1ATL5GLyefJaxhQzSPVrLX"^^com.spotify:album("Scorpion"), genres=["canadian hip hop", "canadian pop", "hip hop", "pop rap", "rap", "toronto rap"], release_date=new Date("2018-06-29T00:00:00.000Z"), popularity=79, energy=90, danceability=58 } C: ]]; -C: @com.spotify.play_song(song="spotify:track:3CA9pLiwRIGtUBiMjbZmRw"^^com.spotify:song("Nice For What")) -C: #[confirm=enum proposed]; A: Nice For What is a song by Drake in the album Scorpion. Would you like to play it on Spotify? AT: $dialogue @org.thingpedia.dialogue.transaction.sys_recommend_one; AT: @com.spotify.play_song(song="spotify:track:3CA9pLiwRIGtUBiMjbZmRw"^^com.spotify:song("Nice For What")) AT: #[confirm=enum proposed]; -U: \r {"code":["bookkeeping","special","special:yes"],"entities":{}} -UT: $dialogue @org.thingpedia.dialogue.transaction.execute; -UT: @com.spotify.play_song(song="spotify:track:3CA9pLiwRIGtUBiMjbZmRw"^^com.spotify:song("Nice For What")); -C: $dialogue @org.thingpedia.dialogue.transaction.sys_action_success; +U: \t $yes; +UT: $yes; +C: $dialogue @org.thingpedia.dialogue.transaction.execute; C: @com.spotify(id="com.spotify-7"^^tt:device_id("Some Device 7")).song() filter id =~ "Nice For What" C: #[results=[ C: { id="spotify:track:3CA9pLiwRIGtUBiMjbZmRw"^^com.spotify:song("Nice For What"), artists=["spotify:artist:3TVXtAsR1Inumwj472S9r4"^^com.spotify:artist("Drake")], album="spotify:album:1ATL5GLyefJaxhQzSPVrLX"^^com.spotify:album("Scorpion"), genres=["canadian hip hop", "canadian pop", "hip hop", "pop rap", "rap", "toronto rap"], release_date=new Date("2018-06-29T00:00:00.000Z"), popularity=79, energy=90, danceability=58 } @@ -647,24 +864,26 @@ AT: $dialogue @org.thingpedia.dialogue.transaction.sys_action_success; #! lines ==== # test +U: \t $stop; +UT: $stop; +==== +# test U: \t $dialogue @org.thingpedia.dialogue.transaction.execute; -U: now => (@com.spotify.song(), id =~ "Nice For What") => notify; +U: @com.spotify.song() filter id =~ "Nice For What"; UT: $dialogue @org.thingpedia.dialogue.transaction.execute; UT: @com.spotify.song() filter id =~ "Nice For What"; -C: $dialogue @org.thingpedia.dialogue.transaction.sys_recommend_one; +C: $dialogue @org.thingpedia.dialogue.transaction.execute; C: @com.spotify(id="com.spotify-7"^^tt:device_id("Some Device 7")).song() filter id =~ "Nice For What" C: #[results=[ C: { id="spotify:track:3CA9pLiwRIGtUBiMjbZmRw"^^com.spotify:song("Nice For What"), artists=["spotify:artist:3TVXtAsR1Inumwj472S9r4"^^com.spotify:artist("Drake")], album="spotify:album:1ATL5GLyefJaxhQzSPVrLX"^^com.spotify:album("Scorpion"), genres=["canadian hip hop", "canadian pop", "hip hop", "pop rap", "rap", "toronto rap"], release_date=new Date("2018-06-29T00:00:00.000Z"), popularity=79, energy=90, danceability=58 } C: ]]; -C: @com.spotify.play_song(song="spotify:track:3CA9pLiwRIGtUBiMjbZmRw"^^com.spotify:song("Nice For What")) -C: #[confirm=enum proposed]; A: Nice For What is a song by Drake in the album Scorpion. Would you like to play it on Spotify? AT: $dialogue @org.thingpedia.dialogue.transaction.sys_recommend_one; AT: @com.spotify.play_song(song="spotify:track:3CA9pLiwRIGtUBiMjbZmRw"^^com.spotify:song("Nice For What")) AT: #[confirm=enum proposed]; -U: \r {"code":["bookkeeping","special","special:no"],"entities":{}} -UT: $dialogue @org.thingpedia.dialogue.transaction.cancel; -C: $dialogue @org.thingpedia.dialogue.transaction.sys_end; +U: \t $no; +UT: $no; +C: $dialogue @org.thingpedia.dialogue.transaction.cancel; C: @com.spotify(id="com.spotify-7"^^tt:device_id("Some Device 7")).song() filter id =~ "Nice For What" C: #[results=[ C: { id="spotify:track:3CA9pLiwRIGtUBiMjbZmRw"^^com.spotify:song("Nice For What"), artists=["spotify:artist:3TVXtAsR1Inumwj472S9r4"^^com.spotify:artist("Drake")], album="spotify:album:1ATL5GLyefJaxhQzSPVrLX"^^com.spotify:album("Scorpion"), genres=["canadian hip hop", "canadian pop", "hip hop", "pop rap", "rap", "toronto rap"], release_date=new Date("2018-06-29T00:00:00.000Z"), popularity=79, energy=90, danceability=58 } @@ -677,14 +896,26 @@ AT: $dialogue @org.thingpedia.dialogue.transaction.sys_end; #! lines ==== # test +U: \t $stop; +UT: $stop; +==== +# test #! vote: up #! comment: test comment for dialogue turns #! additional #! lines U: \t $dialogue @org.thingpedia.dialogue.transaction.execute; -U: now => @com.spotify.play_song(song="spotify:track:4sPmO7WMQUAf45kwMOtONw"^^com.spotify:song("Hello")); +U: @com.spotify.play_song(song="spotify:track:4sPmO7WMQUAf45kwMOtONw"^^com.spotify:song("Hello")); UT: $dialogue @org.thingpedia.dialogue.transaction.execute; UT: @com.spotify.play_song(song="spotify:track:4sPmO7WMQUAf45kwMOtONw"^^com.spotify:song("Hello")); +C: $dialogue @org.thingpedia.dialogue.transaction.execute; +C: @com.spotify(id="com.spotify-7"^^tt:device_id("Some Device 7")).play_song(song="spotify:track:4sPmO7WMQUAf45kwMOtONw"^^com.spotify:song("Hello")) +C: #[results=[]] +C: #[error=enum no_active_device]; +A: Sorry, you must open the Spotify app first. +AT: $dialogue @org.thingpedia.dialogue.transaction.sys_action_error; +U: \t $stop; +UT: $stop; ==== # test #! vote: down @@ -692,9 +923,17 @@ UT: @com.spotify.play_song(song="spotify:track:4sPmO7WMQUAf45kwMOtONw"^^com.spot #! additional #! lines U: \t $dialogue @org.thingpedia.dialogue.transaction.execute; -U: now => @com.spotify.play_song(song="spotify:track:2zYzyRzz6pRmhPzyfMEC8s"^^com.spotify:song("Hello")); +U: @com.spotify.play_song(song="spotify:track:2zYzyRzz6pRmhPzyfMEC8s"^^com.spotify:song("Hello")); UT: $dialogue @org.thingpedia.dialogue.transaction.execute; UT: @com.spotify.play_song(song="spotify:track:2zYzyRzz6pRmhPzyfMEC8s"^^com.spotify:song("Hello")); +C: $dialogue @org.thingpedia.dialogue.transaction.execute; +C: @com.spotify(id="com.spotify-7"^^tt:device_id("Some Device 7")).play_song(song="spotify:track:2zYzyRzz6pRmhPzyfMEC8s"^^com.spotify:song("Hello")) +C: #[results=[]] +C: #[error="Some other error occurred"]; +A: Sorry, Some other error occurred. +AT: $dialogue @org.thingpedia.dialogue.transaction.sys_action_error; +U: \t $stop; +UT: $stop; ==== # test #! vote: up @@ -702,9 +941,20 @@ UT: @com.spotify.play_song(song="spotify:track:2zYzyRzz6pRmhPzyfMEC8s"^^com.spot #! additional #! lines U: \t $dialogue @org.thingpedia.dialogue.transaction.execute; -U: now => [release_date] of (@com.spotify.song(), id =~ "Nice For What") => notify; +U: [release_date] of @com.spotify.song() filter id =~ "Nice For What"; UT: $dialogue @org.thingpedia.dialogue.transaction.execute; UT: [release_date] of @com.spotify.song() filter id =~ "Nice For What"; +C: $dialogue @org.thingpedia.dialogue.transaction.execute; +C: [release_date] of @com.spotify(id="com.spotify-7"^^tt:device_id("Some Device 7")).song() filter id =~ "Nice For What" +C: #[results=[ +C: { id="spotify:track:3CA9pLiwRIGtUBiMjbZmRw"^^com.spotify:song("Nice For What"), release_date=new Date("2018-06-29T00:00:00.000Z") } +C: ]]; +A: Nice For What is a song released on June 28, 2018. Would you like to play it on Spotify? +AT: $dialogue @org.thingpedia.dialogue.transaction.sys_recommend_one; +AT: @com.spotify.play_song(song="spotify:track:3CA9pLiwRIGtUBiMjbZmRw"^^com.spotify:song("Nice For What")) +AT: #[confirm=enum proposed]; +U: \t $stop; +UT: $stop; ==== # test #! vote: down @@ -712,9 +962,30 @@ UT: [release_date] of @com.spotify.song() filter id =~ "Nice For What"; #! additional #! lines U: \t $dialogue @org.thingpedia.dialogue.transaction.execute; -U: now => [release_date] of (@com.spotify.song(), id =~ "hello") => notify; +U: [release_date] of @com.spotify.song() filter id =~ "hello"; UT: $dialogue @org.thingpedia.dialogue.transaction.execute; UT: [release_date] of @com.spotify.song() filter id =~ "hello"; +C: $dialogue @org.thingpedia.dialogue.transaction.execute; +C: [release_date] of @com.spotify(id="com.spotify-7"^^tt:device_id("Some Device 7")).song() filter id =~ "hello" +C: #[results=[ +C: { id="spotify:track:7qwt4xUIqQWCu1DJf96g2k"^^com.spotify:song("Hello?"), release_date=new Date("2018-05-25T00:00:00.000Z") }, +C: { id="spotify:track:2r6OAV3WsYtXuXjvJ1lIDi"^^com.spotify:song("Hello (feat. A Boogie Wit da Hoodie)"), release_date=new Date("2020-07-20T00:00:00.000Z") }, +C: { id="spotify:track:4sPmO7WMQUAf45kwMOtONw"^^com.spotify:song("Hello"), release_date=new Date("2016-06-24T00:00:00.000Z") }, +C: { id="spotify:track:7a2IJHzw9WJjoknwdnCop0"^^com.spotify:song("hello!"), release_date=new Date("2019-11-13T00:00:00.000Z") }, +C: { id="spotify:track:2c62Xf5Po1YSa1N6LOjPHy"^^com.spotify:song("Hello My Old Heart"), release_date=new Date("2011-12-01T00:00:00.000Z") }, +C: { id="spotify:track:0mHyWYXmmCB9iQyK18m3FQ"^^com.spotify:song("Hello"), release_date=new Date("1983-01-01T00:00:00.000Z") }, +C: { id="spotify:track:1p61zyWNtBhbbAFzg4HUiq"^^com.spotify:song("Hello Beautiful"), release_date=new Date("2018-01-27T00:00:00.000Z") }, +C: { id="spotify:track:30Chv2SmIry70YwtmtaKnj"^^com.spotify:song("Hello"), release_date=new Date("2014-12-09T00:00:00.000Z") }, +C: { id="spotify:track:6vmAgl2y9MpoZKrVUXrPe5"^^com.spotify:song("Hello Darlin'"), release_date=new Date("1970-03-23T00:00:00.000Z") }, +C: { id="spotify:track:0vZ97gHhemKm6c64hTfJNA"^^com.spotify:song("Hello, Goodbye - Remastered 2009"), release_date=new Date("1967-11-27T00:00:00.000Z") } +C: ]] +C: #[count=11]; +A: Hello? Is a song released on May 24, 2018. Would you like to play it on Spotify? +AT: $dialogue @org.thingpedia.dialogue.transaction.sys_recommend_one; +AT: @com.spotify.play_song(song="spotify:track:7qwt4xUIqQWCu1DJf96g2k"^^com.spotify:song("Hello?")) +AT: #[confirm=enum proposed]; +U: \t $stop; +UT: $stop; ==== # test #! vote: up @@ -722,9 +993,20 @@ UT: [release_date] of @com.spotify.song() filter id =~ "hello"; #! additional #! lines U: \t $dialogue @org.thingpedia.dialogue.transaction.execute; -U: now => (sort popularity desc of @com.spotify.song())[1:3] => notify; +U: sort(popularity desc of @com.spotify.song())[1 : 3]; UT: $dialogue @org.thingpedia.dialogue.transaction.execute; UT: sort(popularity desc of @com.spotify.song())[1 : 3]; +C: $dialogue @org.thingpedia.dialogue.transaction.execute; +C: sort(popularity desc of @com.spotify(id="com.spotify-7"^^tt:device_id("Some Device 7")).song())[1 : 3] +C: #[results=[ +C: { id="spotify:track:7ytR5pFWmSjzHJIeQkgog4"^^com.spotify:song("ROCKSTAR (feat. Roddy Ricch)"), artists=["spotify:artist:4r63FhuTkUYltbVAg5TQnk"^^com.spotify:artist("DaBaby"), "spotify:artist:757aE44tKEUQEqRuT6GnEB"^^com.spotify:artist("Roddy Ricch")], album="spotify:album:623PL2MBg50Br5dLXC9E9e"^^com.spotify:album("BLAME IT ON BABY"), genres=["north carolina hip hop", "rap"], release_date=new Date("2020-04-17T00:00:00.000Z"), popularity=100, energy=69, danceability=74 }, +C: { id="spotify:track:0VjIjW4GlUZAMYd2vXMi3b"^^com.spotify:song("Blinding Lights"), artists=["spotify:artist:1Xyo4u8uXC1ZmMpatF05PJ"^^com.spotify:artist("The Weeknd")], album="spotify:album:4yP0hdKOZPNshxUOjY0cZj"^^com.spotify:album("After Hours"), genres=["canadian contemporary r&b", "canadian pop", "pop"], release_date=new Date("2020-03-20T00:00:00.000Z"), popularity=99, energy=73, danceability=51 }, +C: { id="spotify:track:7eJMfftS33KTjuF7lTsMCx"^^com.spotify:song("death bed (coffee for your head)"), artists=["spotify:artist:6bmlMHgSheBauioMgKv2tn"^^com.spotify:artist("Powfu"), "spotify:artist:35l9BRT7MXmM8bv2WDQiyB"^^com.spotify:artist("beabadoobee")], album="spotify:album:2p9gK2BcdrloHNJwarc9gc"^^com.spotify:album("death bed (coffee for your head)"), genres=[], release_date=new Date("2020-02-08T00:00:00.000Z"), popularity=95, energy=43, danceability=72 } +C: ]]; +A: I have found ROCKSTAR (feat. Roddy Ricch), Blinding Lights or death bed (coffee for your head). +AT: $dialogue @org.thingpedia.dialogue.transaction.sys_recommend_three; +U: \t $stop; +UT: $stop; ==== # test #! vote: down @@ -732,16 +1014,28 @@ UT: sort(popularity desc of @com.spotify.song())[1 : 3]; #! additional #! lines U: \t $dialogue @org.thingpedia.dialogue.transaction.execute; -U: now => [release_date] of (@com.spotify.album(), id =~ "the wall") => notify; +U: [release_date] of @com.spotify.album() filter id =~ "the wall"; UT: $dialogue @org.thingpedia.dialogue.transaction.execute; UT: [release_date] of @com.spotify.album() filter id =~ "the wall"; +C: $dialogue @org.thingpedia.dialogue.transaction.execute; +C: [release_date] of @com.spotify(id="com.spotify-7"^^tt:device_id("Some Device 7")).album() filter id =~ "the wall" +C: #[results=[ +C: { id="spotify:album:5Dbax7G8SWrP9xyzkOvy2F"^^com.spotify:album("The Wall"), release_date=new Date("1979-11-30T00:00:00.000Z") }, +C: { id="spotify:album:283NWqNsCA9GwVHrJk59CG"^^com.spotify:album("The Writing's On The Wall"), release_date=new Date("1999-07-27T00:00:00.000Z") }, +C: { id="spotify:album:2ZytN2cY4Zjrr9ukb2rqTP"^^com.spotify:album("Off the Wall"), release_date=new Date("1979-08-10T00:00:00.000Z") }, +C: { id="spotify:album:1GlOZiKHTgtJqEF2FRii9y"^^com.spotify:album("Over The Garden Wall"), release_date=new Date("2017-09-19T00:00:00.000Z") } +C: ]]; +A: The Wall is an album from November 29, 1979. +AT: $dialogue @org.thingpedia.dialogue.transaction.sys_recommend_one; +U: \t $stop; +UT: $stop; ==== # test U: \t $dialogue @org.thingpedia.dialogue.transaction.execute; -U: now => [release_date] of (@com.spotify.album(), id =~ "the wall") => notify; +U: [release_date] of @com.spotify.album() filter id =~ "the wall"; UT: $dialogue @org.thingpedia.dialogue.transaction.execute; UT: [release_date] of @com.spotify.album() filter id =~ "the wall"; -C: $dialogue @org.thingpedia.dialogue.transaction.sys_recommend_one; +C: $dialogue @org.thingpedia.dialogue.transaction.execute; C: [release_date] of @com.spotify(id="com.spotify-7"^^tt:device_id("Some Device 7")).album() filter id =~ "the wall" C: #[results=[ C: { id="spotify:album:5Dbax7G8SWrP9xyzkOvy2F"^^com.spotify:album("The Wall"), release_date=new Date("1979-11-30T00:00:00.000Z") }, @@ -756,29 +1050,43 @@ AT: $dialogue @org.thingpedia.dialogue.transaction.sys_recommend_one; #! additional #! lines U: \t $dialogue @org.thingpedia.dialogue.transaction.execute; -U: now => @org.thingpedia.weather.current(location=new Location(37.442156, -122.1634471, "Palo Alto, California")) => notify; +U: @org.thingpedia.weather.current(location=new Location(37.442156, -122.1634471, "Palo Alto, California")); UT: $dialogue @org.thingpedia.dialogue.transaction.execute; UT: @org.thingpedia.weather.current(location=new Location(37.442156, -122.1634471, "Palo Alto, California")); +C: $dialogue @org.thingpedia.dialogue.transaction.execute; +C: [release_date] of @com.spotify(id="com.spotify-7"^^tt:device_id("Some Device 7")).album() filter id =~ "the wall" +C: #[results=[ +C: { id="spotify:album:5Dbax7G8SWrP9xyzkOvy2F"^^com.spotify:album("The Wall"), release_date=new Date("1979-11-30T00:00:00.000Z") }, +C: { id="spotify:album:283NWqNsCA9GwVHrJk59CG"^^com.spotify:album("The Writing's On The Wall"), release_date=new Date("1999-07-27T00:00:00.000Z") }, +C: { id="spotify:album:2ZytN2cY4Zjrr9ukb2rqTP"^^com.spotify:album("Off the Wall"), release_date=new Date("1979-08-10T00:00:00.000Z") }, +C: { id="spotify:album:1GlOZiKHTgtJqEF2FRii9y"^^com.spotify:album("Over The Garden Wall"), release_date=new Date("2017-09-19T00:00:00.000Z") } +C: ]]; +C: @org.thingpedia.weather.current(location=new Location(37.442156, -122.1634471, "Palo Alto, California")) +C: #[results=[ +C: { location=new Location(37.442156, -122.1634471, "Palo Alto, California"), temperature=15.6C, wind_speed=3.4mps, humidity=91.3, cloudiness=4.7, fog=0, status=enum sunny, icon="http://api.met.no/weatherapi/weathericon/1.1/?symbol=1;content_type=image/png"^^tt:picture } +C: ]]; +A: The current weather in Palo Alto, California is sunny. The temperature is 60.1 F and the humidity is 91.3 %. +AT: $dialogue @org.thingpedia.dialogue.transaction.sys_display_result; +U: \t $stop; +UT: $stop; ==== # test U: \t $dialogue @org.thingpedia.dialogue.transaction.execute; -U: now => (@com.spotify.song(), id =~ "Nice For What") => notify; +U: @com.spotify.song() filter id =~ "Nice For What"; UT: $dialogue @org.thingpedia.dialogue.transaction.execute; UT: @com.spotify.song() filter id =~ "Nice For What"; -C: $dialogue @org.thingpedia.dialogue.transaction.sys_recommend_one; +C: $dialogue @org.thingpedia.dialogue.transaction.execute; C: @com.spotify(id="com.spotify-7"^^tt:device_id("Some Device 7")).song() filter id =~ "Nice For What" C: #[results=[ C: { id="spotify:track:3CA9pLiwRIGtUBiMjbZmRw"^^com.spotify:song("Nice For What"), artists=["spotify:artist:3TVXtAsR1Inumwj472S9r4"^^com.spotify:artist("Drake")], album="spotify:album:1ATL5GLyefJaxhQzSPVrLX"^^com.spotify:album("Scorpion"), genres=["canadian hip hop", "canadian pop", "hip hop", "pop rap", "rap", "toronto rap"], release_date=new Date("2018-06-29T00:00:00.000Z"), popularity=79, energy=90, danceability=58 } C: ]]; -C: @com.spotify.play_song(song="spotify:track:3CA9pLiwRIGtUBiMjbZmRw"^^com.spotify:song("Nice For What")) -C: #[confirm=enum proposed]; A: Nice For What is a song by Drake in the album Scorpion. Would you like to play it on Spotify? AT: $dialogue @org.thingpedia.dialogue.transaction.sys_recommend_one; AT: @com.spotify.play_song(song="spotify:track:3CA9pLiwRIGtUBiMjbZmRw"^^com.spotify:song("Nice For What")) AT: #[confirm=enum proposed]; -U: \r {"code":["bookkeeping","special","special:no"],"entities":{}} -UT: $dialogue @org.thingpedia.dialogue.transaction.cancel; -C: $dialogue @org.thingpedia.dialogue.transaction.sys_end; +U: \t $no; +UT: $no; +C: $dialogue @org.thingpedia.dialogue.transaction.cancel; C: @com.spotify(id="com.spotify-7"^^tt:device_id("Some Device 7")).song() filter id =~ "Nice For What" C: #[results=[ C: { id="spotify:track:3CA9pLiwRIGtUBiMjbZmRw"^^com.spotify:song("Nice For What"), artists=["spotify:artist:3TVXtAsR1Inumwj472S9r4"^^com.spotify:artist("Drake")], album="spotify:album:1ATL5GLyefJaxhQzSPVrLX"^^com.spotify:album("Scorpion"), genres=["canadian hip hop", "canadian pop", "hip hop", "pop rap", "rap", "toronto rap"], release_date=new Date("2018-06-29T00:00:00.000Z"), popularity=79, energy=90, danceability=58 } @@ -791,14 +1099,29 @@ AT: $dialogue @org.thingpedia.dialogue.transaction.sys_end; #! lines ==== # test +U: \t $stop; +UT: $stop; +==== +# test #! vote: up #! comment: test comment for dialogue turns #! additional #! lines U: \t $dialogue @org.thingpedia.dialogue.transaction.execute; -U: now => (sort popularity desc of @com.spotify.song())[1] => notify; +U: sort(popularity desc of @com.spotify.song())[1]; UT: $dialogue @org.thingpedia.dialogue.transaction.execute; UT: sort(popularity desc of @com.spotify.song())[1]; +C: $dialogue @org.thingpedia.dialogue.transaction.execute; +C: sort(popularity desc of @com.spotify(id="com.spotify-7"^^tt:device_id("Some Device 7")).song())[1] +C: #[results=[ +C: { id="spotify:track:7ytR5pFWmSjzHJIeQkgog4"^^com.spotify:song("ROCKSTAR (feat. Roddy Ricch)"), artists=["spotify:artist:4r63FhuTkUYltbVAg5TQnk"^^com.spotify:artist("DaBaby"), "spotify:artist:757aE44tKEUQEqRuT6GnEB"^^com.spotify:artist("Roddy Ricch")], album="spotify:album:623PL2MBg50Br5dLXC9E9e"^^com.spotify:album("BLAME IT ON BABY"), genres=["north carolina hip hop", "rap"], release_date=new Date("2020-04-17T00:00:00.000Z"), popularity=100, energy=69, danceability=74 } +C: ]]; +A: The most popular song is ROCKSTAR (feat. Roddy Ricch). It is a song by DaBaby in the album BLAME IT ON BABY. Would you like to play it on Spotify? +AT: $dialogue @org.thingpedia.dialogue.transaction.sys_recommend_one; +AT: @com.spotify.play_song(song="spotify:track:7ytR5pFWmSjzHJIeQkgog4"^^com.spotify:song("ROCKSTAR (feat. Roddy Ricch)")) +AT: #[confirm=enum proposed]; +U: \t $stop; +UT: $stop; ==== # test #! vote: down @@ -806,9 +1129,20 @@ UT: sort(popularity desc of @com.spotify.song())[1]; #! additional #! lines U: \t $dialogue @org.thingpedia.dialogue.transaction.execute; -U: now => (sort popularity asc of @com.spotify.song())[1] => notify; +U: sort(popularity asc of @com.spotify.song())[1]; UT: $dialogue @org.thingpedia.dialogue.transaction.execute; UT: sort(popularity asc of @com.spotify.song())[1]; +C: $dialogue @org.thingpedia.dialogue.transaction.execute; +C: sort(popularity asc of @com.spotify(id="com.spotify-7"^^tt:device_id("Some Device 7")).song())[1] +C: #[results=[ +C: { id="spotify:track:0VXeNEtuiD77F2StUeUadK"^^com.spotify:song("Love Yourself"), artists=["spotify:artist:1uNFoZAHBGtllmzznpCI3s"^^com.spotify:artist("Justin Bieber")], album="spotify:album:74jDw6TsdKnT92V790QfvF"^^com.spotify:album("Romantic Pop Songs"), genres=["canadian pop", "pop", "post-teen pop"], release_date=new Date("2020-08-10T00:00:00.000Z"), popularity=0, energy=43, danceability=78 } +C: ]]; +A: The least popular song is Love Yourself. It is a song by Justin Bieber in the album Romantic Pop Songs. Would you like to play it on Spotify? +AT: $dialogue @org.thingpedia.dialogue.transaction.sys_recommend_one; +AT: @com.spotify.play_song(song="spotify:track:0VXeNEtuiD77F2StUeUadK"^^com.spotify:song("Love Yourself")) +AT: #[confirm=enum proposed]; +U: \t $stop; +UT: $stop; ==== # test #! vote: up @@ -816,9 +1150,18 @@ UT: sort(popularity asc of @com.spotify.song())[1]; #! additional #! lines U: \t $dialogue @org.thingpedia.dialogue.transaction.execute; -U: now => (sort reviewCount desc of @com.yelp.restaurant())[1] => notify; +U: sort(reviewCount desc of @com.yelp.restaurant())[1]; UT: $dialogue @org.thingpedia.dialogue.transaction.execute; UT: sort(reviewCount desc of @com.yelp.restaurant())[1]; +C: $dialogue @org.thingpedia.dialogue.transaction.execute; +C: sort(reviewCount desc of @com.yelp.restaurant())[1] +C: #[results=[ +C: { id="jwaXc3VVGDFQu1aCoiXwdw"^^com.yelp:restaurant("NOLA Restaurant & Bar"), image_url="https://s3-media2.fl.yelpcdn.com/bphoto/tna5SSdgZq3fFxoi4-Xx7A/o.jpg"^^tt:picture, link="https://www.yelp.com/biz/nola-restaurant-and-bar-palo-alto?adjust_creative=hejPBQRox5iXtqGPiDw4dg&utm_campaign=yelp_api_v3&utm_medium=api_v3_business_search&utm_source=hejPBQRox5iXtqGPiDw4dg"^^tt:url, cuisines=["cajun"^^com.yelp:restaurant_cuisine("Cajun/Creole"), "newamerican"^^com.yelp:restaurant_cuisine("American (New)")], price=enum expensive, rating=3.5, reviewCount=2821, geo=new Location(37.4450225830078, -122.161323547363), phone="+16503282722"^^tt:phone_number } +C: ]]; +A: The restaurant with the highest review count is NOLA Restaurant & Bar. It is an expensive restaurant near [Latitude: 37.445 deg, Longitude: -122.161 deg]. +AT: $dialogue @org.thingpedia.dialogue.transaction.sys_recommend_one; +U: \t $stop; +UT: $stop; ==== # test #! vote: down @@ -826,9 +1169,18 @@ UT: sort(reviewCount desc of @com.yelp.restaurant())[1]; #! additional #! lines U: \t $dialogue @org.thingpedia.dialogue.transaction.execute; -U: now => (sort reviewCount asc of @com.yelp.restaurant())[1] => notify; +U: sort(reviewCount asc of @com.yelp.restaurant())[1]; UT: $dialogue @org.thingpedia.dialogue.transaction.execute; UT: sort(reviewCount asc of @com.yelp.restaurant())[1]; +C: $dialogue @org.thingpedia.dialogue.transaction.execute; +C: sort(reviewCount asc of @com.yelp.restaurant())[1] +C: #[results=[ +C: { id="yzBYyndSQ3RI6Hci2F9pRQ"^^com.yelp:restaurant("The Ivy Cambridge Brasserie"), image_url="https://s3-media4.fl.yelpcdn.com/bphoto/9daDsE_Aq-AtJnDDPruBSQ/o.jpg"^^tt:picture, link="https://www.yelp.com/biz/the-ivy-cambridge-brasserie-cambridge?adjust_creative=hejPBQRox5iXtqGPiDw4dg&utm_campaign=yelp_api_v3&utm_medium=api_v3_business_search&utm_source=hejPBQRox5iXtqGPiDw4dg"^^tt:url, cuisines=["british"^^com.yelp:restaurant_cuisine("British")], rating=4.5, reviewCount=12, geo=new Location(52.2069714284192, 0.118247921026355), phone="+441223344044"^^tt:phone_number } +C: ]]; +A: The restaurant with the lowest review count is The Ivy Cambridge Brasserie. It is a restaurant near [Latitude: 52.207 deg, Longitude: 0.118 deg] rated 4.5 star. +AT: $dialogue @org.thingpedia.dialogue.transaction.sys_recommend_one; +U: \t $stop; +UT: $stop; ==== # test #! vote: up @@ -839,6 +1191,15 @@ U: \t $dialogue @org.thingpedia.dialogue.transaction.execute; U: @com.thecatapi.get(); UT: $dialogue @org.thingpedia.dialogue.transaction.execute; UT: @com.thecatapi.get(); +C: $dialogue @org.thingpedia.dialogue.transaction.execute; +C: @com.thecatapi(id="com.thecatapi-9"^^tt:device_id("Some Device 9")).get() +C: #[results=[ +C: { id="str:ENTITY_com.thecatapi:image_id::0:"^^com.thecatapi:image_id, picture_url="str:ENTITY_tt:picture::36:"^^tt:picture, link="str:ENTITY_tt:url::42:"^^tt:url } +C: ]]; +A: Here is your cat picture. +AT: $dialogue @org.thingpedia.dialogue.transaction.sys_display_result; +U: \t $stop; +UT: $stop; ==== # test #! vote: down @@ -849,6 +1210,26 @@ U: \t $dialogue @org.thingpedia.dialogue.transaction.execute; U: @com.xkcd.comic(); UT: $dialogue @org.thingpedia.dialogue.transaction.execute; UT: @com.xkcd.comic(); +C: $dialogue @org.thingpedia.dialogue.transaction.execute; +C: @com.xkcd(id="com.xkcd-10"^^tt:device_id("Some Device 10")).comic() +C: #[results=[ +C: { id=20, title="Some Device 10", release_date=new Date("2018-01-20T08:00:00.000Z"), picture_url="str:ENTITY_tt:picture::14:"^^tt:picture, link="str:ENTITY_tt:url::1:"^^tt:url, alt_text="Some Device 10" }, +C: { id=451, title="str:QUOTED_STRING::49:", release_date=new Date("2020-03-20T07:00:00.000Z"), picture_url="str:ENTITY_tt:picture::41:"^^tt:picture, link="str:ENTITY_tt:url::28:"^^tt:url, alt_text="str:QUOTED_STRING::4:" }, +C: { id=20, title="Some Device 10", release_date=new Date("2018-01-20T08:00:00.000Z"), picture_url="str:ENTITY_tt:picture::27:"^^tt:picture, link="str:ENTITY_tt:url::14:"^^tt:url, alt_text="str:QUOTED_STRING::4:" }, +C: { id=20, title="Some Device 10", release_date=new Date("2018-01-20T08:00:00.000Z"), picture_url="str:ENTITY_tt:picture::47:"^^tt:picture, link="str:ENTITY_tt:url::19:"^^tt:url, alt_text="str:QUOTED_STRING::38:" }, +C: { id=20, title="str:QUOTED_STRING::6:", release_date=new Date("2018-01-20T08:00:00.000Z"), picture_url="str:ENTITY_tt:picture::3:"^^tt:picture, link="str:ENTITY_tt:url::4:"^^tt:url, alt_text="Some Device 10" }, +C: { id=20, title="Some Device 10", release_date=new Date("2018-01-20T08:00:00.000Z"), picture_url="str:ENTITY_tt:picture::46:"^^tt:picture, link="str:ENTITY_tt:url::35:"^^tt:url, alt_text="Some Device 10" }, +C: { id=20, title="str:QUOTED_STRING::28:", release_date=new Date("2018-01-20T08:00:00.000Z"), picture_url="str:ENTITY_tt:picture::33:"^^tt:picture, link="str:ENTITY_tt:url::28:"^^tt:url, alt_text="str:QUOTED_STRING::44:" }, +C: { id=837, title="str:QUOTED_STRING::29:", release_date=new Date("2020-01-23T08:00:00.000Z"), picture_url="str:ENTITY_tt:picture::7:"^^tt:picture, link="str:ENTITY_tt:url::4:"^^tt:url, alt_text="str:QUOTED_STRING::40:" }, +C: { id=481, title="Some Device 10", release_date=new Date("2018-01-20T08:00:00.000Z"), picture_url="str:ENTITY_tt:picture::12:"^^tt:picture, link="str:ENTITY_tt:url::20:"^^tt:url, alt_text="Some Device 10" }, +C: { id=638, title="Some Device 10", release_date=new Date("2018-01-20T08:00:00.000Z"), picture_url="str:ENTITY_tt:picture::40:"^^tt:picture, link="str:ENTITY_tt:url::35:"^^tt:url, alt_text="str:QUOTED_STRING::3:" } +C: ]] +C: #[count=50] +C: #[more=true]; +A: I found xkcd 20. It is an xkcd comic from January 20, 2018 titled Some Device 10. +AT: $dialogue @org.thingpedia.dialogue.transaction.sys_recommend_one; +U: \t $stop; +UT: $stop; ==== # test #! vote: up @@ -859,22 +1240,29 @@ U: \t $dialogue @org.thingpedia.dialogue.transaction.execute; U: sort(release_date desc of @com.xkcd.comic())[1]; UT: $dialogue @org.thingpedia.dialogue.transaction.execute; UT: sort(release_date desc of @com.xkcd.comic())[1]; +C: $dialogue @org.thingpedia.dialogue.transaction.execute; +C: sort(release_date desc of @com.xkcd(id="com.xkcd-10"^^tt:device_id("Some Device 10")).comic())[1] +C: #[results=[ +C: { id=371, title="str:QUOTED_STRING::3:", release_date=new Date("2020-08-31T07:00:00.000Z"), picture_url="str:ENTITY_tt:picture::26:"^^tt:picture, link="str:ENTITY_tt:url::39:"^^tt:url, alt_text="str:QUOTED_STRING::6:" } +C: ]]; +A: The latest xkcd comic is 371. It is an xkcd comic from August 31, 2020 titled str:QUOTED_STRING::3:. +AT: $dialogue @org.thingpedia.dialogue.transaction.sys_recommend_one; +U: \t $stop; +UT: $stop; ==== # test U: \t $dialogue @org.thingpedia.dialogue.transaction.execute; U: @com.xkcd.comic() => @com.twitter.post(status=title); UT: $dialogue @org.thingpedia.dialogue.transaction.execute; UT: @com.xkcd.comic() => @com.twitter.post(status=title); -C: $dialogue @org.thingpedia.dialogue.transaction.sys_confirm_action; +C: $dialogue @org.thingpedia.dialogue.transaction.execute; C: @com.xkcd(id="com.xkcd-10"^^tt:device_id("Some Device 10")).comic() => @com.twitter(id="twitter-foo"^^tt:device_id("Twitter Account foo")).post(status=title); A: Ok, do you want me to get xkcd comic and then tweet the title? AT: $dialogue @org.thingpedia.dialogue.transaction.sys_confirm_action; AT: @com.xkcd(id="com.xkcd-10"^^tt:device_id("Some Device 10")).comic() => @com.twitter(id="twitter-foo"^^tt:device_id("Twitter Account foo")).post(status=title); U: \t $yes; -UT: $dialogue @org.thingpedia.dialogue.transaction.execute; -UT: @com.xkcd(id="com.xkcd-10"^^tt:device_id("Some Device 10")).comic() => @com.twitter(id="twitter-foo"^^tt:device_id("Twitter Account foo")).post(status=title) -UT: #[confirm=enum confirmed]; -C: $dialogue @org.thingpedia.dialogue.transaction.sys_action_success; +UT: $yes; +C: $dialogue @org.thingpedia.dialogue.transaction.execute; C: @com.xkcd(id="com.xkcd-10"^^tt:device_id("Some Device 10")).comic() => @com.twitter(id="twitter-foo"^^tt:device_id("Twitter Account foo")).post(status=title) C: #[results=[]]; A: Your request was completed successfully. @@ -885,11 +1273,15 @@ AT: $dialogue @org.thingpedia.dialogue.transaction.sys_action_success; #! lines ==== # test +U: \t $stop; +UT: $stop; +==== +# test U: \t $dialogue @org.thingpedia.dialogue.transaction.execute; U: @org.thingpedia.builtin.thingengine.builtin.faq_reply(question=enum about_almond_identity); UT: $dialogue @org.thingpedia.dialogue.transaction.execute; UT: @org.thingpedia.builtin.thingengine.builtin.faq_reply(question=enum about_almond_identity); -C: $dialogue @org.thingpedia.dialogue.transaction.sys_action_success; +C: $dialogue @org.thingpedia.dialogue.transaction.execute; C: @org.thingpedia.builtin.thingengine.builtin(id="thingengine-own-global"^^tt:device_id("Builtin")).faq_reply(question=enum about_almond_identity) C: #[results=[ C: { question=enum about_almond_identity, reply="str:QUOTED_STRING::42:" } @@ -902,31 +1294,45 @@ AT: $dialogue @org.thingpedia.dialogue.transaction.sys_action_success; #! lines ==== # test -#! vote: down -#! comment: test comment for dialogue turns -#! additional -#! lines +U: \t $stop; +UT: $stop; +==== +# test U: \t $dialogue @org.thingpedia.dialogue.transaction.execute; -U: @org.thingpedia.iot.light-bulb.set_power(power=enum(off)); +U: @org.thingpedia.iot.light-bulb.set_power(power=enum off); UT: $dialogue @org.thingpedia.dialogue.transaction.execute; UT: @org.thingpedia.iot.light-bulb.set_power(power=enum off); -==== -# test -#! vote: up +A: You do not have a Light Bulb configured. You will need to enable Home Assistant or Philips Hue before you can use that command. +#! vote: down #! comment: test comment for dialogue turns #! additional #! lines +==== +# test +U: \t $stop; +UT: $stop; +==== +# test U: \t $dialogue @org.thingpedia.dialogue.transaction.execute; U: @org.thingpedia.iot.door.state(); UT: $dialogue @org.thingpedia.dialogue.transaction.execute; UT: @org.thingpedia.iot.door.state(); +A: You need to enable Home Assistant before you can use that command. +#! vote: up +#! comment: test comment for dialogue turns +#! additional +#! lines +==== +# test +U: \t $stop; +UT: $stop; ==== # test U: \t $dialogue @org.thingpedia.dialogue.transaction.execute; -U: now => @org.thingpedia.weather.current(location=new Location(37.442156, -122.1634471, "Palo Alto, California")) => notify; +U: @org.thingpedia.weather.current(location=new Location(37.442156, -122.1634471, "Palo Alto, California")); UT: $dialogue @org.thingpedia.dialogue.transaction.execute; UT: @org.thingpedia.weather.current(location=new Location(37.442156, -122.1634471, "Palo Alto, California")); -C: $dialogue @org.thingpedia.dialogue.transaction.sys_display_result; +C: $dialogue @org.thingpedia.dialogue.transaction.execute; C: @org.thingpedia.weather.current(location=new Location(37.442156, -122.1634471, "Palo Alto, California")) C: #[results=[ C: { location=new Location(37.442156, -122.1634471, "Palo Alto, California"), temperature=15.6C, wind_speed=3.4mps, humidity=91.3, cloudiness=4.7, fog=0, status=enum sunny, icon="http://api.met.no/weatherapi/weathericon/1.1/?symbol=1;content_type=image/png"^^tt:picture } @@ -935,7 +1341,7 @@ A: The current weather in Palo Alto, California is sunny. The temperature is 60. AT: $dialogue @org.thingpedia.dialogue.transaction.sys_display_result; U: \t $dialogue @org.thingpedia.dialogue.transaction.greet; UT: $dialogue @org.thingpedia.dialogue.transaction.greet; -C: $dialogue @org.thingpedia.dialogue.transaction.sys_greet; +C: $dialogue @org.thingpedia.dialogue.transaction.greet; C: @org.thingpedia.weather.current(location=new Location(37.442156, -122.1634471, "Palo Alto, California")) C: #[results=[ C: { location=new Location(37.442156, -122.1634471, "Palo Alto, California"), temperature=15.6C, wind_speed=3.4mps, humidity=91.3, cloudiness=4.7, fog=0, status=enum sunny, icon="http://api.met.no/weatherapi/weathericon/1.1/?symbol=1;content_type=image/png"^^tt:picture } @@ -944,7 +1350,7 @@ A: Hi, how can I help you? AT: $dialogue @org.thingpedia.dialogue.transaction.sys_greet; U: \t $dialogue @org.thingpedia.dialogue.transaction.cancel; UT: $dialogue @org.thingpedia.dialogue.transaction.cancel; -C: $dialogue @org.thingpedia.dialogue.transaction.sys_end; +C: $dialogue @org.thingpedia.dialogue.transaction.cancel; C: @org.thingpedia.weather.current(location=new Location(37.442156, -122.1634471, "Palo Alto, California")) C: #[results=[ C: { location=new Location(37.442156, -122.1634471, "Palo Alto, California"), temperature=15.6C, wind_speed=3.4mps, humidity=91.3, cloudiness=4.7, fog=0, status=enum sunny, icon="http://api.met.no/weatherapi/weathericon/1.1/?symbol=1;content_type=image/png"^^tt:picture } diff --git a/test/agent/index.js b/test/agent/index.js index 2c7a2b3c3..bf0bc5bb0 100644 --- a/test/agent/index.js +++ b/test/agent/index.js @@ -159,7 +159,7 @@ class MockUser { async function mockNLU(conversation) { // inject some mocking in the parser: - conversation._nlu.onlineLearn = function(utterance, targetCode) { + conversation._loop._nlu.onlineLearn = function(utterance, targetCode) { if (utterance === 'get an xkcd comic') assert.strictEqual(targetCode.join(' '), 'now => @com.xkcd.get_comic => notify'); else if (utterance === '!! test command multiple results !!') @@ -171,8 +171,8 @@ async function mockNLU(conversation) { const commands = yaml.safeLoad(await util.promisify(fs.readFile)( path.resolve(path.dirname(module.filename), './mock-nlu.yaml'))); - const realSendUtterance = conversation._nlu.sendUtterance; - conversation._nlu.sendUtterance = async function(utterance) { + const realSendUtterance = conversation._loop._nlu.sendUtterance; + conversation._loop._nlu.sendUtterance = async function(utterance) { if (utterance === '!! test command host unreach !!') { const e = new Error('Host is unreachable'); e.code = 'EHOSTUNREACH'; @@ -188,7 +188,7 @@ async function mockNLU(conversation) { err.code = command.error.code; throw err; } - return { tokens, entities, candidates: command.candidates }; + return { tokens, entities, candidates: command.candidates, intent: { ignore: 0, command: 1, other: 0 } }; } } diff --git a/test/agent/tests.txt b/test/agent/tests.txt index fc0ca4e3c..79241db9e 100644 --- a/test/agent/tests.txt +++ b/test/agent/tests.txt @@ -265,7 +265,7 @@ A: >> expecting = null U: !! test command always failed !! -A: Sorry, I did not understand that. +A: Sorry, I did not understand that. Can you rephrase it? A: >> context = null // {} A: >> expecting = null diff --git a/tool/interactive-annotate.ts b/tool/interactive-annotate.ts index bbafd6544..3175323c3 100644 --- a/tool/interactive-annotate.ts +++ b/tool/interactive-annotate.ts @@ -18,7 +18,6 @@ // // Author: Giovanni Campagna -import assert from 'assert'; import * as argparse from 'argparse'; import * as fs from 'fs'; import * as readline from 'readline'; @@ -450,45 +449,7 @@ class Annotator extends events.EventEmitter { } private async _inputToDialogueState(input : ThingTalk.Ast.Input) : Promise { - if (input instanceof ThingTalk.Ast.ControlCommand) { - if (input.intent instanceof ThingTalk.Ast.SpecialControlIntent) { - switch (input.intent.type) { - case 'yes': - case 'no': { - const value = new ThingTalk.Ast.BooleanValue(input.intent.type === 'yes'); - const handled = await this._dialoguePolicy.handleAnswer(this._context, value); - if (!handled) - return null; - return ThingTalkUtils.computePrediction(this._context, handled, 'user'); - } - default: - return null; - } - } - if (input.intent instanceof ThingTalk.Ast.ChoiceControlIntent) - return null; - - if (input.intent instanceof ThingTalk.Ast.AnswerControlIntent) { - const handled = await this._dialoguePolicy.handleAnswer(this._context, input.intent.value); - if (!handled) - return null; - return ThingTalkUtils.computePrediction(this._context, handled, 'user'); - } - - throw new TypeError(`Unrecognized bookkeeping intent`); - } else if (input instanceof ThingTalk.Ast.Program) { - // convert thingtalk programs to dialogue states so we can use "\t" without too much typing - const prediction = new ThingTalk.Ast.DialogueState(null, 'org.thingpedia.dialogue.transaction', 'execute', null, []); - for (const stmt of input.statements) { - if (stmt instanceof ThingTalk.Ast.Assignment) - throw new Error(`Unsupported: assignment statement`); - prediction.history.push(new ThingTalk.Ast.DialogueHistoryItem(null, stmt, null, 'accepted')); - } - return prediction; - } - - assert(input instanceof ThingTalk.Ast.DialogueState); - return input; + return ThingTalkUtils.inputToDialogueState(this._dialoguePolicy, this._context, input); } private async _handleUtterance(utterance : string) { diff --git a/tool/server.ts b/tool/server.ts index 2e5b7dd90..45865cb34 100644 --- a/tool/server.ts +++ b/tool/server.ts @@ -24,7 +24,7 @@ import bodyParser from 'body-parser'; // FIXME //import logger from 'morgan'; import errorhandler from 'errorhandler'; -import qv from 'query-validation'; +import * as qv from 'query-validation'; import * as Tp from 'thingpedia'; import * as ThingTalk from 'thingtalk'; @@ -106,35 +106,16 @@ const QUERY_PARAMS = { async function queryNLU(params : Record, data : QueryNLUData, res : express.Response) { - const thingtalk_version = data.thingtalk_version; const app = res.app; - if (thingtalk_version !== ThingTalk.version) { - res.status(400).json({ error: 'Invalid ThingTalk version' }); - return; - } if (params.locale !== app.args.locale) { res.status(400).json({ error: 'Unsupported language' }); return; } - // emulate the frontend classifier for API compatibility - const intent = { - question: 0, - command: 1, - chatty: 0, - other: 0 - }; - const result = await res.app.backend.nlu.sendUtterance(data.q, data.context ? data.context.split(' ') : undefined, data.entities, data); - - res.json({ - candidates: result.candidates, - tokens: result.tokens, - entities: result.entities, - intent - }); + res.json(result); } interface QueryNLGData {