From 25adf406c810e48b1277105dd6c269a2ed601d28 Mon Sep 17 00:00:00 2001 From: louistiti Date: Sun, 27 Mar 2022 19:50:35 +0800 Subject: [PATCH] feat(server): context and slot filling, keep context and await for entities --- core/data/en/answers.json | 5 ++ server/src/core/conversation.js | 101 +++++++++++++++++--------------- server/src/core/nlu.js | 87 +++++++++++++++++++-------- 3 files changed, 123 insertions(+), 70 deletions(-) diff --git a/core/data/en/answers.json b/core/data/en/answers.json index a3aeb4a6..f2aa9315 100644 --- a/core/data/en/answers.json +++ b/core/data/en/answers.json @@ -57,6 +57,11 @@ "Sorry, I don't speak this language yet", "You are awesome, but I can't speak this language yet", "It looks like a language I can't understand at the moment" + ], + "random_context_out_of_topic": [ + "Sure, let's change the topic", + "Aah, you want to change the subject, sure", + "Mmmh, as you wish, let's switch conversation" ] } } diff --git a/server/src/core/conversation.js b/server/src/core/conversation.js index 6ac381e8..d2ac1eb0 100644 --- a/server/src/core/conversation.js +++ b/server/src/core/conversation.js @@ -2,16 +2,19 @@ import log from '@/helpers/log' import fs from 'fs' const maxContextHistory = 5 +const defaultActiveContext = { + name: null, + domain: null, + intent: null, + slots: { }, + activatedAt: 0 +} class Conversation { constructor (id = 'conv0') { // Identify conversations to allow more features in the future (multiple speakers, etc.) this._id = id - this._activeContext = { - name: null, - slots: { }, - activatedAt: 0 - } + this._activeContext = defaultActiveContext this._previousContexts = { } log.title('Conversation') @@ -30,17 +33,6 @@ class Conversation { return this._previousContexts } - /** - * Get data of the current conversation instance - */ - getCurrent () { - return { - id: this._id, - activeContext: this._activeContext, - previousContexts: this._previousContexts - } - } - /** * Check whether there is an active context */ @@ -74,57 +66,52 @@ class Conversation { * then save the current active context to the contexts history */ if (this._activeContext.name && this._activeContext.name !== outputContext) { - const previousContextsKeys = Object.keys(this._previousContexts) - - // Remove oldest context from the history stack - if (previousContextsKeys.length >= maxContextHistory) { - delete this._previousContexts[previousContextsKeys[0]] - } - - this._previousContexts[this._activeContext.name] = this._activeContext + this.pushToPreviousContextsStack() } else if (!this._activeContext.name) { // Activate new context - this._activeContext.name = outputContext - this._activeContext.activatedAt = Date.now() + this._activeContext = { + name: outputContext, + domain, + intent, + slots: { }, + activatedAt: Date.now() + } } - this.setSlots(slots, { - lang, - domain, - intent, - entities - }) + this.setSlots(lang, entities, slots) } } /** * Set slots in active context */ - setSlots (slots, valueObj) { - const { - lang, - domain, - intent, - entities - } = valueObj + setSlots (lang, entities, slots = this._activeContext.slots) { const slotKeys = Object.keys(slots) for (let i = 0; i < slotKeys.length; i += 1) { const key = slotKeys[i] const slotObj = slots[key] - const [slotName, slotEntity] = key.split('#') + const isFirstSet = key.includes('#') + let slotName = slotObj.name + let slotEntity = slotObj.expectedEntity + let { questions } = slotObj + + // If it's the first slot setting grabbed from the model or not + if (isFirstSet) { + [slotName, slotEntity] = key.split('#') + questions = slotObj.locales[lang] + } + const [foundEntity] = entities.filter(({ entity }) => entity === slotEntity) - const questions = slotObj.locales[lang] - const question = questions[Math.floor(Math.random() * questions.length)] + const pickedQuestion = questions[Math.floor(Math.random() * questions.length)] const slot = this._activeContext.slots[slotName] const newSlot = { name: slotName, - domain, - intent, - entity: slotEntity, + expectedEntity: slotEntity, value: foundEntity, isFilled: !!foundEntity, - question + questions, + pickedQuestion } /** @@ -151,6 +138,28 @@ class Conversation { return this._activeContext.slots[notFilledSlotKey] } + + /** + * Clean up active context + */ + cleanActiveContext () { + this.pushToPreviousContextsStack() + this._activeContext = defaultActiveContext + } + + /** + * Push active context to the previous contexts stack + */ + pushToPreviousContextsStack () { + const previousContextsKeys = Object.keys(this._previousContexts) + + // Remove the oldest context from the history stack if it reaches the maximum limit + if (previousContextsKeys.length >= maxContextHistory) { + delete this._previousContexts[previousContextsKeys[0]] + } + + this._previousContexts[this._activeContext.name] = this._activeContext + } } export default Conversation diff --git a/server/src/core/nlu.js b/server/src/core/nlu.js index de741562..f5746558 100644 --- a/server/src/core/nlu.js +++ b/server/src/core/nlu.js @@ -112,25 +112,63 @@ class Nlu { }) } + // TODO: create specific method const hasActivatedContext = this.conv.hasActiveContext() if (hasActivatedContext) { + const { domain, intent } = this.conv.activeContext + const [skillName, actionName] = intent.split('.') + const nluDataFilePath = join(process.cwd(), 'skills', domain, skillName, `nlu/${this.brain.lang}.json`) + const nluResultObj = { + utterance, + entities: [], + classification: { + domain, + skill: skillName, + action: actionName + } + } + const entities = await this.ner.extractEntities( + this.brain.lang, + nluDataFilePath, + nluResultObj + ) + + this.conv.setSlots(this.brain.lang, entities) + + console.log('active context obj', this.conv.activeContext) + console.log('nluResultObj', nluResultObj) + + const notFilledSlot = this.conv.getNotFilledSlot() + /** + * Loop for questions if a slot hasn't been filled + * and at least an entity has been found + */ + if (notFilledSlot && entities.length > 0) { + this.brain.talk(notFilledSlot.pickedQuestion) + this.brain.socket.emit('is-typing', false) + + return resolve() + } + + this.brain.talk(`${this.brain.wernicke('random_context_out_of_topic')}.`) + this.conv.cleanActiveContext() + /** * TODO: * 1. Extract entities from utterance * 2. If none of them match any slot in the active context, then continue * 3. If an entity match slot in active context, then fill it + * 4. Add logs in terminal about context switching, active context, etc. */ } - console.log('active context obj', this.conv.activeContext) - const result = await this.nlp.process(utterance) - console.log('result', result) + // console.log('result', result) const { locale, domain, intent, score, answers } = result const [skillName, actionName] = intent.split('.') - let obj = { + let nluResultObj = { utterance, entities: [], answers, // For dialog skill type @@ -184,14 +222,14 @@ class Nlu { version, utterance, lang: this.brain.lang, - classification: obj.classification + classification: nluResultObj.classification }) .then(() => { /* */ }) .catch(() => { /* */ }) } if (intent === 'None') { - const fallback = Nlu.fallback(obj, langs[lang.getLongCode(locale)].fallbacks) + const fallback = Nlu.fallback(nluResultObj, langs[lang.getLongCode(locale)].fallbacks) if (fallback === false) { if (!opts.mute) { @@ -212,21 +250,21 @@ class Nlu { }) } - obj = fallback + nluResultObj = fallback } log.title('NLU') - log.success(`Intent found: ${obj.classification.skill}.${obj.classification.action} (domain: ${obj.classification.domain})`) + log.success(`Intent found: ${nluResultObj.classification.skill}.${nluResultObj.classification.action} (domain: ${nluResultObj.classification.domain})`) - const nluDataFilePath = join(process.cwd(), 'skills', obj.classification.domain, obj.classification.skill, `nlu/${this.brain.lang}.json`) + const nluDataFilePath = join(process.cwd(), 'skills', nluResultObj.classification.domain, nluResultObj.classification.skill, `nlu/${this.brain.lang}.json`) const { type: skillType } = domainHelper.getSkillInfo(domain, skillName) - obj.skillType = skillType + nluResultObj.skillType = skillType try { - obj.entities = await this.ner.extractEntities( + nluResultObj.entities = await this.ner.extractEntities( this.brain.lang, nluDataFilePath, - obj + nluResultObj ) } catch (e) /* istanbul ignore next */ { if (log[e.type]) { @@ -248,13 +286,14 @@ class Nlu { actionName, domain, intent, - entities: obj.entities + entities: nluResultObj.entities }) const notFilledSlot = this.conv.getNotFilledSlot() // Loop for questions if a slot hasn't been filled if (notFilledSlot) { - this.brain.talk(notFilledSlot.question) + console.log('in original loop') + this.brain.talk(notFilledSlot.pickedQuestion) this.brain.socket.emit('is-typing', false) return resolve() @@ -265,7 +304,7 @@ class Nlu { // return resolve() // TODO: fill with contexts? - // obj.slots = slots + // nluResultObj.slots = slots // console.log('getIntentEntityNames', // await this.nlp.slotManager.getIntentEntityNames(intent)) // ['number'] @@ -282,7 +321,7 @@ class Nlu { try { // Inject action entities with the others if there is - const data = await this.brain.execute(obj, { mute: opts.mute }) + const data = await this.brain.execute(nluResultObj, { mute: opts.mute }) const processingTimeEnd = Date.now() const processingTime = processingTimeEnd - processingTimeStart @@ -308,8 +347,8 @@ class Nlu { * Pickup and compare the right fallback * according to the wished skill action */ - static fallback (obj, fallbacks) { - const words = obj.utterance.toLowerCase().split(' ') + static fallback (nluResultObj, fallbacks) { + const words = nluResultObj.utterance.toLowerCase().split(' ') if (fallbacks.length > 0) { log.info('Looking for fallbacks...') @@ -323,14 +362,14 @@ class Nlu { } if (JSON.stringify(tmpWords) === JSON.stringify(fallbacks[i].words)) { - obj.entities = [] - obj.classification.domain = fallbacks[i].domain - obj.classification.skill = fallbacks[i].skill - obj.classification.action = fallbacks[i].action - obj.classification.confidence = 1 + nluResultObj.entities = [] + nluResultObj.classification.domain = fallbacks[i].domain + nluResultObj.classification.skill = fallbacks[i].skill + nluResultObj.classification.action = fallbacks[i].action + nluResultObj.classification.confidence = 1 log.success('Fallback found') - return obj + return nluResultObj } } }