1
1
mirror of https://github.com/leon-ai/leon.git synced 2024-11-27 16:16:48 +03:00

feat(server): context and slot filling, keep context and await for entities

This commit is contained in:
louistiti 2022-03-27 19:50:35 +08:00
parent 1ece25a497
commit 25adf406c8
No known key found for this signature in database
GPG Key ID: 7ECA3DD523793FE6
3 changed files with 123 additions and 70 deletions

View File

@ -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"
]
}
}

View File

@ -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

View File

@ -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
}
}
}