UNPKG

@assistant/conversation

Version:
473 lines (436 loc) 13.5 kB
/** * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import * as Schema from '../api/schema' import { Headers } from '../framework' import { Prompt, PromptItem } from './prompt' import { JsonObject, clone, isJsonEqual } from '../common' import { Handler, Intent, Scene, Session, User, Device, Home, Expected } from './handler' import { Context } from './handler/context' import { ILogger } from '../logger' /** * Options used when constructing app object. * * @example * ```javascript * const {conversation} = require('@assistant/conversation'); * * const app = conversation({verification: 'nodejs-cloud-test-project-1234'}); * ``` * * @public */ export interface ConversationV3Options { /** @public */ body?: Schema.HandlerRequest /** @public */ headers?: Headers /** * Represents the client ID given in the Actions Console to use for * authorizing users when performing Google Sign-In. * * @see {@link https://developers.google.com/assistant/identity/gsi-concept-guide | Google Sign-In documentation} * @public */ clientId?: string /** * When set to true, requests and responses will be automatically logged using `logger`. * @public */ debug?: boolean /** * Represents an object with methods for each logging level. By default the * logger binds to `console` methods. * @see {@link debugLogger | Default logger implementation} * @public */ logger?: ILogger /** * Validates whether request is from Google through signature verification. * Uses Google-Auth-Library to verify authorization token against given Google Cloud Project ID. * Auth token is given in request header with key, "authorization". * * HTTP Code 403 will be thrown by default on verification error. * * @example * ```javascript * * const app = conversation({ verification: 'nodejs-cloud-test-project-1234' }) * ``` * * @see {@link https://developers.google.com/assistant/conversational/reference/rest/v1/verify-requests | Verify Requests documentation} * @public */ verification?: ConversationVerification | string } /** @hidden */ export interface ConversationOptions { /** @public */ request?: Schema.HandlerRequest /** @public */ headers?: Headers } /** * Represents details of action to verify requests are coming from Google. * @see {@link https://developers.google.com/assistant/conversational/reference/rest/v1/verify-requests | Verify Requests documentation} * @public */ export interface ConversationVerification { /** * Google Cloud Project ID for the Assistant app. * @public */ project: string /** * Custom status code to return on verification error. * @public */ status?: number /** * Custom error message as a string or a function that returns a string * given the original error message set by the library. * * The message will get sent back in the JSON top level `error` property. * @public */ error?: string | ((error: string) => string) } /** * Represents a turn of the conversation. This is provided as `conv` in an * intent handler. * @public */ export class ConversationV3 { /** @public */ request: Schema.HandlerRequest /** @public */ headers: Headers /** * Get the current handler information like handler name. * * @example * ```javascript * * app.handle('handler name', conv => { * const handlerName = conv.handler.name * }) * ``` * * @public */ handler: Schema.Handler /** * Represents the last matched intent. * @public */ intent: Schema.Intent /** * Info on the current and next scene when the function was called. Will be filled * when the fulfillment call is made within the scope of a scene. * * Represent a scene. Scenes can call fulfillment, add prompts, and collect slot values from * the user. Scenes are triggered by events or intents and can trigger events and match * intents to transition to other scenes. * * Represents the current and next scene. If `Scene.next` is set the runtime will * immediately transition to the specified scene. * @public */ scene: Schema.Scene /** * Holds session data like the session id and session parameters. * * Contains information on the current conversation session * * Describes data for the current session, session parameters can be created, * updated, or removed by the fulfillment. * * @example * ```javascript * // Assign color to session storage * app.handle('storeColor', conv => { * let color = 'red'; * conv.session.params.exampleColor = color; * }); * * // Retrieve color from session storage * app.handle('getStoredColor', conv => { * let color = conv.session.params.exampleColor; * }); * ``` * * @see {@link https://developers.google.com/assistant/conversational/storage-session | Session Storage documentation} * @public */ session: Schema.Session /** * Represents the user making a request to the Action. * * @example * ```javascript * // Assign color to user storage * app.handle('storeColor', conv => { * let color = 'red'; * conv.user.params.exampleColor = color; * }); * * // Retrieve color from user storage * app.handle('getStoredColor', conv => { * let color = conv.user.params.exampleColor; * }); * ``` * @see {@link https://developers.google.com/assistant/conversational/storage-user | User Storage documentation} * @public */ user: User /** * Represents the device the user is using to make a request to the Action. * @see {@link https://developers.google.com/assistant/conversational/permissions | Permissions documentation} * @public */ device: Schema.Device /** * Represents the HomeGraph structure that the user's target device belongs * to. * * @example * ```javascript * // Assign color to home storage * app.handle('storeColor', conv => { * let color = 'red'; * conv.home.params.exampleColor = color; * }); * * // Retrieve color from home storage * app.handle('getStoredColor', conv => { * let color = conv.home.params.exampleColor; * }); * ``` * @see {@link https://developers.google.com/assistant/conversational/storage-home | Home Storage documentation} * @public */ home: Home /** * Describes the expectations for the next dialog turn. * @public */ expected: Expected /** * Contains context information when user makes query. Such context includes but not limited * to info about active media session, state of canvas web app, etc. * @public */ context: Schema.Context /** * Represents the prompts to be sent to the user, these prompts will be appended * to previously added messages unless explicitly overwritten. * @public */ prompt: Prompt /** @public */ overwrite = true /** @public */ digested = false /** * Represents a request sent to a developer's fulfillment by Google. * @public */ body: Schema.HandlerRequest /** @hidden */ _internal: { raw?: JsonObject, promptSet: boolean, orig: { scene: Schema.Scene, session: Schema.Session, user: Schema.User, home: Schema.Home, }, } /** * Initializes conversational application. * @param options A set of options that apply to the application. * @public */ constructor(options: ConversationV3Options = {}) { const { headers = {}, body = {} } = options this.request = body this.headers = headers this.handler = new Handler(body.handler) this.intent = new Intent(body.intent) this.scene = new Scene(body.scene) this.session = new Session(body.session as Schema.Session) this.user = new User(body.user) this.device = new Device(body.device) this.home = new Home(body.home) this.expected = new Expected() this.context = new Context(body.context) this.prompt = new Prompt() // Create a instance of prompt to keep track of prompts to be sent. // Set request values to compare later to see what the developer changed this._internal = { promptSet: false, orig: { scene: clone(this.scene), session: clone(this.session), user: clone(this.user), home: clone(this.home), }, } } /** * Manually sets response JSON. * @public */ json<T = JsonObject>(json: T) { this._internal.raw = json return this } /** * Add prompt items to be sent back for fulfillment. * * Prompt items are limited to 2 simple responses. * More than 2 will result in an error in fulfillment. * The first simple added in order will be set to `firstSimple`. * The last simple added in order will be set to `lastSimple`. * * @example * ```javascript * * const app = conversation() * * app.handle('main', conv => { * const ssml = '<speak>Hi! <break time="1"/> ' + * 'I can read out an ordinal like <say-as interpret-as="ordinal">123</say-as>. ' + * 'Say a number.</speak>' * conv.add(ssml) * }) * ``` * * @param promptItems A response fragment for the library to construct a single complete response * @public */ add(...promptItems: PromptItem[]) { if (this.digested) { throw new Error('Response has already been sent. ' + 'Is this being used in an async call that was not ' + 'returned as a promise to the intent handler?') } this.prompt.add(...promptItems) this._internal.promptSet = true return this } /** * Append speech responses to be sent back for fulfillment. * * @example * ```javascript * * const app = conversation() * * app.handle('handler name', conv => { * const ssml = '<speak>Hi! <break time="1"/> ' + * 'I can read out an ordinal like <say-as interpret-as="ordinal">123</say-as>. ' + * 'Say a number.</speak>' * conv.append(ssml) * }) * ``` * * @param speech A speech string to be appended * @public */ append(speech: string) { if (this.digested) { throw new Error('Response has already been sent. ' + 'Is this being used in an async call that was not ' + 'returned as a promise to the intent handler?') } this.prompt.append(speech) this._internal.promptSet = true return this } /** * Returns generated JSON response. * * Note this method sets the `digested` field to `true` and can only be * called once. * @public */ response(): Schema.HandlerResponse { if (this.digested) { throw new Error('Response has already been digested') } this.digested = true const session = new Session(this.session) if (session.typeOverrides!.length) { for (const override of session.typeOverrides!) { if (override.mode) { // Temporarily use typeOverrideMode property for mode until // typeOverrideMode is fully deleted and migrated out (override as JsonObject).typeOverrideMode = override.mode delete override.mode } } } else { delete session.typeOverrides } // Create response and include field that should be included in every response const response: Schema.HandlerResponse = { // Echo back session variables in response session, } // Add other attributes to response if they exist and are different form the request if (!this.overwrite) { this.prompt.override = false } response.prompt = new Prompt(this.prompt) if (this.scene && !isJsonEqual({...this.scene}, {...this._internal.orig.scene})) { response.scene = new Scene(this.scene) } if (this.user && !isJsonEqual({...this.user}, {...this._internal.orig.user})) { response.user = new User(this.user) } if (this.home && !isJsonEqual({...this.home}, {...this._internal.orig.home})) { (response as Schema.HandlerResponse).home = new Home(this.home) } if (this.expected.languageCode || (this.expected.speech && this.expected.speech.length)) { response.expected = this.expected if (this.expected.speech && !this.expected.speech.length) { delete response.expected.speech } } return clone(response) } /** * Returns manually set JSON response or generates a response. * * If the response has to be generated, it sets the `digested` field to * `true`. * @public */ serialize(): Schema.HandlerResponse { if (this._internal.raw) { return this._internal.raw } const handlerResponse: Schema.HandlerResponse = this.response() return handlerResponse } } export interface ExceptionHandler<TConversation extends ConversationV3> { /** @public */ // tslint:disable-next-line:no-any allow to return any just detect if is promise (conv: TConversation, error: Error): Promise<any> | any }