/**
 *  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
 *
 *      https://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 { Injectable } from '@angular/core' ; import * as use from '@tensorflow-models/universal-sentence-encoder' ; import { HttpClient } from '@angular/common/http' ; function dot ( data1 , data2 ) { let sum = 0 ; for ( let i = 0 ; i < data1 . length ; i ++ ) { sum += data1 [ i ] * data2 [ i ] ; } return sum ; } export type BotName = 'maid' | 'butler' | 'chef' ; interface IModelFetchResult { queryMap : { [ query ] : string ; } ; embeddingMap : { [ query ] : number [ ] ; } ; } @ Injectable ( { providedIn : 'root' } ) export class BotResponseService { private readonly modelPromise = use . load ( ) ; private readonly modelPromisesByBotName = { } ; private loadModelsPromise ? : Promise < IModelFetchResult [ ] > ; private minimumScore = 0.6 ; private readonly statesByBotName : { [ botName ] : string | null ; } = { maid : null , butler : null , chef : null , } ; constructor ( private http ) { } private async loadModel ( botName ) { return this . http . get < IModelFetchResult > ( `assets/models/${ botName }.model.json` ) . toPromise ( ) ; } loadModels ( ) { if ( ! this . loadModelsPromise ) { this . loadModelsPromise = Promise . all ( [ 'maid' , 'chef' , 'butler' ] . map ( ( botName ) => { const promise = this . loadModel ( botName ) ; this . modelPromisesByBotName [ botName ] = promise ; return promise ; } ) ) ; } return this . loadModelsPromise ; } setState ( { maid , butler , chef } ) { if ( maid !== undefined ) { this . statesByBotName . maid = maid ; } if ( butler !== undefined ) { this . statesByBotName . butler = butler ; } if ( chef !== undefined ) { this . statesByBotName . chef = chef ; } } getState ( botName ) { return this . statesByBotName [ botName ] ; } param query param botName async getResponse ( query , botName ) { botName = botName . toLowerCase ( ) as BotName ; const { queryMap , embeddingMap } = await this . modelPromisesByBotName [ botName ] ; const model = await this . modelPromise ; const queryEmbedding = await model . embed ( [ query ] ) ; const queryArrays = await queryEmbedding . array ( ) ; let maxScore = - Infinity ; let bestMatch ; for ( const potentialMatch of Object . keys ( embeddingMap ) ) { const score = dot ( queryArrays [ 0 ] , embeddingMap [ potentialMatch ] ) ; if ( score > maxScore ) { maxScore = score ; bestMatch = potentialMatch ; } } console . info ( 'Best match: "' + bestMatch + '" with score ' + maxScore ) ; if ( bestMatch === undefined || maxScore < this . minimumScore ) { if ( maxScore < this . minimumScore ) { console . info ( 'Ignoring match because its score is below the threshhold of ' + this . minimumScore ) ; } let queryKeyPair = [ null ] ; let match ; if ( bestMatch ) { queryKeyPair = [ bestMatch ] ; if ( this . statesByBotName [ botName ] ) { queryKeyPair . push ( this . statesByBotName [ botName ] ) ; } match = queryMap [ JSON . stringify ( queryKeyPair ) ] ; if ( match ) { if ( match . NewState ) { this . statesByBotName [ botName ] = match . NewState ; } return match . Response ; } } queryKeyPair [ 0 ] = null ; if ( this . statesByBotName [ botName ] ) { queryKeyPair . push ( this . statesByBotName [ botName ] ) ; } match = queryMap [ JSON . stringify ( queryKeyPair ) ] ; if ( match ) { if ( match . NewState ) { this . statesByBotName [ botName ] = match . NewState ; } return match . Response ; } return undefined ; } return queryMap [ bestMatch ] . Response ; } }