1
1
mirror of https://github.com/leon-ai/leon.git synced 2024-12-03 02:45:21 +03:00

refactor(server): default NLU result

This commit is contained in:
louistiti 2023-03-20 22:35:44 +08:00
parent a8e3fb8b87
commit a215fc8770
2 changed files with 52 additions and 70 deletions

View File

@ -1,17 +1,3 @@
/**
* TODO next:
* 1. [OK] Fix brain.ts TS errors
* 2. [OK] Refactor brain.ts; split "execute" into smaller functions:
* // [OK] handle this scope into its own method:
// - handleLogicActionSkillProcessOutput
// - handleLogicActionSkillProcessError
// - handleLogicActionSkillProcessClose
* 3. Fix nlu.ts TS errors
* 4. Refactor nlu.ts; split into smaller functions
* 5. Restore multi client support on HTTP server / socket server
* 6. Publish to "develop" (or just fix TS errors only and publish first, then refactor)
*/
import fs from 'node:fs' import fs from 'node:fs'
import path from 'node:path' import path from 'node:path'
import { ChildProcessWithoutNullStreams, spawn } from 'node:child_process' import { ChildProcessWithoutNullStreams, spawn } from 'node:child_process'

View File

@ -5,6 +5,7 @@ import { spawn } from 'node:child_process'
import axios from 'axios' import axios from 'axios'
import kill from 'tree-kill' import kill from 'tree-kill'
import type { NLUResult } from '@/core/nlp/types'
import { langs } from '@@/core/langs.json' import { langs } from '@@/core/langs.json'
import { version } from '@@/package.json' import { version } from '@@/package.json'
import { HAS_LOGGER, IS_TESTING_ENV, TCP_SERVER_BIN_PATH } from '@/constants' import { HAS_LOGGER, IS_TESTING_ENV, TCP_SERVER_BIN_PATH } from '@/constants'
@ -14,35 +15,30 @@ import { StringHelper } from '@/helpers/string-helper'
import { LangHelper } from '@/helpers/lang-helper' import { LangHelper } from '@/helpers/lang-helper'
import Conversation from '@/core/conversation' import Conversation from '@/core/conversation'
const defaultNluResultObj = { const DEFAULT_NLU_RESULT = {
utterance: null, utterance: '',
currentEntities: [], currentEntities: [],
entities: [], entities: [],
currentResolvers: [], currentResolvers: [],
resolvers: [], resolvers: [],
slots: null, slots: {},
configDataFilePath: null, configDataFilePath: '',
answers: [], // For dialog action type answers: [], // For dialog action type
classification: { classification: {
domain: null, domain: '',
skill: null, skill: '',
action: null, action: '',
confidence: 0 confidence: 0
} }
} }
export default class NLU { export default class NLU {
private static instance: NLU private static instance: NLU
private conversation = new Conversation('conv0')
private nluResult: NLUResult = DEFAULT_NLU_RESULT
constructor() { constructor() {
if (!NLU.instance) { if (!NLU.instance) {
this.globalResolversNlp = {}
this.skillsResolversNlp = {}
this.mainNlp = {}
this.ner = {}
this.conv = new Conversation('conv0')
this.nluResult = defaultNluResultObj // TODO
LogHelper.title('NLU') LogHelper.title('NLU')
LogHelper.success('New instance') LogHelper.success('New instance')
@ -118,7 +114,7 @@ export default class NLU {
* Handle in action loop logic before NLU processing * Handle in action loop logic before NLU processing
*/ */
async handleActionLoop(utterance) { async handleActionLoop(utterance) {
const { domain, intent } = this.conv.activeContext const { domain, intent } = this.conversation.activeContext
const [skillName, actionName] = intent.split('.') const [skillName, actionName] = intent.split('.')
const configDataFilePath = join( const configDataFilePath = join(
process.cwd(), process.cwd(),
@ -128,8 +124,8 @@ export default class NLU {
`config/${BRAIN.lang}.json` `config/${BRAIN.lang}.json`
) )
this.nluResult = { this.nluResult = {
...defaultNluResultObj, // Reset entities, slots, etc. ...DEFAULT_NLU_RESULT, // Reset entities, slots, etc.
slots: this.conv.activeContext.slots, slots: this.conversation.activeContext.slots,
utterance, utterance,
configDataFilePath, configDataFilePath,
classification: { classification: {
@ -209,7 +205,7 @@ export default class NLU {
// Ensure expected items are in the utterance, otherwise clean context and reprocess // Ensure expected items are in the utterance, otherwise clean context and reprocess
if (!hasMatchingEntity && !hasMatchingResolver) { if (!hasMatchingEntity && !hasMatchingResolver) {
BRAIN.talk(`${BRAIN.wernicke('random_context_out_of_topic')}.`) BRAIN.talk(`${BRAIN.wernicke('random_context_out_of_topic')}.`)
this.conv.cleanActiveContext() this.conversation.cleanActiveContext()
await this.process(utterance) await this.process(utterance)
return null return null
} }
@ -218,9 +214,9 @@ export default class NLU {
const processedData = await BRAIN.execute(this.nluResult) const processedData = await BRAIN.execute(this.nluResult)
// Reprocess with the original utterance that triggered the context at first // Reprocess with the original utterance that triggered the context at first
if (processedData.core?.restart === true) { if (processedData.core?.restart === true) {
const { originalUtterance } = this.conv.activeContext const { originalUtterance } = this.conversation.activeContext
this.conv.cleanActiveContext() this.conversation.cleanActiveContext()
await this.process(originalUtterance) await this.process(originalUtterance)
return null return null
} }
@ -233,15 +229,15 @@ export default class NLU {
!processedData.action.next_action && !processedData.action.next_action &&
processedData.core?.isInActionLoop === false processedData.core?.isInActionLoop === false
) { ) {
this.conv.cleanActiveContext() this.conversation.cleanActiveContext()
return null return null
} }
// Break the action loop and prepare for the next action if necessary // Break the action loop and prepare for the next action if necessary
if (processedData.core?.isInActionLoop === false) { if (processedData.core?.isInActionLoop === false) {
this.conv.activeContext.isInActionLoop = !!processedData.action.loop this.conversation.activeContext.isInActionLoop = !!processedData.action.loop
this.conv.activeContext.actionName = processedData.action.next_action this.conversation.activeContext.actionName = processedData.action.next_action
this.conv.activeContext.intent = `${processedData.classification.skill}.${processedData.action.next_action}` this.conversation.activeContext.intent = `${processedData.classification.skill}.${processedData.action.next_action}`
} }
return processedData return processedData
@ -268,7 +264,7 @@ export default class NLU {
if (processedData && Object.keys(processedData).length > 0) { if (processedData && Object.keys(processedData).length > 0) {
// Set new context with the next action if there is one // Set new context with the next action if there is one
if (processedData.action.next_action) { if (processedData.action.next_action) {
this.conv.activeContext = { this.conversation.activeContext = {
lang: BRAIN.lang, lang: BRAIN.lang,
slots: processedData.slots, slots: processedData.slots,
isInActionLoop: !!processedData.nextAction.loop, isInActionLoop: !!processedData.nextAction.loop,
@ -313,14 +309,14 @@ export default class NLU {
await this.mergeSpacyEntities(utterance) await this.mergeSpacyEntities(utterance)
// Pre NLU processing according to the active context if there is one // Pre NLU processing according to the active context if there is one
if (this.conv.hasActiveContext()) { if (this.conversation.hasActiveContext()) {
// When the active context is in an action loop, then directly trigger the action // When the active context is in an action loop, then directly trigger the action
if (this.conv.activeContext.isInActionLoop) { if (this.conversation.activeContext.isInActionLoop) {
return resolve(await this.handleActionLoop(utterance)) return resolve(await this.handleActionLoop(utterance))
} }
// When the active context has slots filled // When the active context has slots filled
if (Object.keys(this.conv.activeContext.slots).length > 0) { if (Object.keys(this.conversation.activeContext.slots).length > 0) {
try { try {
return resolve(await this.handleSlotFilling(utterance)) return resolve(await this.handleSlotFilling(utterance))
} catch (e) { } catch (e) {
@ -339,13 +335,13 @@ export default class NLU {
* If there are several "delete it" across skills, Leon needs to make use of * If there are several "delete it" across skills, Leon needs to make use of
* the current context ({domain}.{skill}) to define the most accurate classification * the current context ({domain}.{skill}) to define the most accurate classification
*/ */
if (this.conv.hasActiveContext()) { if (this.conversation.hasActiveContext()) {
classifications.forEach(({ intent: newIntent, score: newScore }) => { classifications.forEach(({ intent: newIntent, score: newScore }) => {
if (newScore > 0.6) { if (newScore > 0.6) {
const [skillName] = newIntent.split('.') const [skillName] = newIntent.split('.')
const newDomain = MODEL_LOADER.mainNLPContainer.getIntentDomain(locale, newIntent) const newDomain = MODEL_LOADER.mainNLPContainer.getIntentDomain(locale, newIntent)
const contextName = `${newDomain}.${skillName}` const contextName = `${newDomain}.${skillName}`
if (this.conv.activeContext.name === contextName) { if (this.conversation.activeContext.name === contextName) {
score = newScore score = newScore
intent = newIntent intent = newIntent
domain = newDomain domain = newDomain
@ -356,7 +352,7 @@ export default class NLU {
const [skillName, actionName] = intent.split('.') const [skillName, actionName] = intent.split('.')
this.nluResult = { this.nluResult = {
...defaultNluResultObj, // Reset entities, slots, etc. ...DEFAULT_NLU_RESULT, // Reset entities, slots, etc.
utterance, utterance,
answers, // For dialog action type answers, // For dialog action type
classification: { classification: {
@ -452,8 +448,8 @@ export default class NLU {
// In case all slots have been filled in the first utterance // In case all slots have been filled in the first utterance
if ( if (
this.conv.hasActiveContext() && this.conversation.hasActiveContext() &&
Object.keys(this.conv.activeContext.slots).length > 0 Object.keys(this.conversation.activeContext.slots).length > 0
) { ) {
try { try {
return resolve(await this.handleSlotFilling(utterance)) return resolve(await this.handleSlotFilling(utterance))
@ -463,10 +459,10 @@ export default class NLU {
} }
const newContextName = `${this.nluResult.classification.domain}.${skillName}` const newContextName = `${this.nluResult.classification.domain}.${skillName}`
if (this.conv.activeContext.name !== newContextName) { if (this.conversation.activeContext.name !== newContextName) {
this.conv.cleanActiveContext() this.conversation.cleanActiveContext()
} }
this.conv.activeContext = { this.conversation.activeContext = {
lang: BRAIN.lang, lang: BRAIN.lang,
slots: {}, slots: {},
isInActionLoop: false, isInActionLoop: false,
@ -479,17 +475,17 @@ export default class NLU {
} }
// Pass current utterance entities to the NLU result object // Pass current utterance entities to the NLU result object
this.nluResult.currentEntities = this.nluResult.currentEntities =
this.conv.activeContext.currentEntities this.conversation.activeContext.currentEntities
// Pass context entities to the NLU result object // Pass context entities to the NLU result object
this.nluResult.entities = this.conv.activeContext.entities this.nluResult.entities = this.conversation.activeContext.entities
try { try {
const processedData = await BRAIN.execute(this.nluResult) const processedData = await BRAIN.execute(this.nluResult)
// Prepare next action if there is one queuing // Prepare next action if there is one queuing
if (processedData.nextAction) { if (processedData.nextAction) {
this.conv.cleanActiveContext() this.conversation.cleanActiveContext()
this.conv.activeContext = { this.conversation.activeContext = {
lang: BRAIN.lang, lang: BRAIN.lang,
slots: {}, slots: {},
isInActionLoop: !!processedData.nextAction.loop, isInActionLoop: !!processedData.nextAction.loop,
@ -528,11 +524,11 @@ export default class NLU {
* and ask for more entities if necessary * and ask for more entities if necessary
*/ */
async slotFill(utterance) { async slotFill(utterance) {
if (!this.conv.activeContext.nextAction) { if (!this.conversation.activeContext.nextAction) {
return null return null
} }
const { domain, intent } = this.conv.activeContext const { domain, intent } = this.conversation.activeContext
const [skillName, actionName] = intent.split('.') const [skillName, actionName] = intent.split('.')
const configDataFilePath = join( const configDataFilePath = join(
process.cwd(), process.cwd(),
@ -543,7 +539,7 @@ export default class NLU {
) )
this.nluResult = { this.nluResult = {
...defaultNluResultObj, // Reset entities, slots, etc. ...DEFAULT_NLU_RESULT, // Reset entities, slots, etc.
utterance, utterance,
classification: { classification: {
domain, domain,
@ -559,16 +555,16 @@ export default class NLU {
) )
// Continue to loop for questions if a slot has been filled correctly // Continue to loop for questions if a slot has been filled correctly
let notFilledSlot = this.conv.getNotFilledSlot() let notFilledSlot = this.conversation.getNotFilledSlot()
if (notFilledSlot && entities.length > 0) { if (notFilledSlot && entities.length > 0) {
const hasMatch = entities.some( const hasMatch = entities.some(
({ entity }) => entity === notFilledSlot.expectedEntity ({ entity }) => entity === notFilledSlot.expectedEntity
) )
if (hasMatch) { if (hasMatch) {
this.conv.setSlots(BRAIN.lang, entities) this.conversation.setSlots(BRAIN.lang, entities)
notFilledSlot = this.conv.getNotFilledSlot() notFilledSlot = this.conversation.getNotFilledSlot()
if (notFilledSlot) { if (notFilledSlot) {
BRAIN.talk(notFilledSlot.pickedQuestion) BRAIN.talk(notFilledSlot.pickedQuestion)
SOCKET_SERVER.socket.emit('is-typing', false) SOCKET_SERVER.socket.emit('is-typing', false)
@ -578,31 +574,31 @@ export default class NLU {
} }
} }
if (!this.conv.areSlotsAllFilled()) { if (!this.conversation.areSlotsAllFilled()) {
BRAIN.talk(`${BRAIN.wernicke('random_context_out_of_topic')}.`) BRAIN.talk(`${BRAIN.wernicke('random_context_out_of_topic')}.`)
} else { } else {
this.nluResult = { this.nluResult = {
...defaultNluResultObj, // Reset entities, slots, etc. ...DEFAULT_NLU_RESULT, // Reset entities, slots, etc.
// Assign slots only if there is a next action // Assign slots only if there is a next action
slots: this.conv.activeContext.nextAction slots: this.conversation.activeContext.nextAction
? this.conv.activeContext.slots ? this.conversation.activeContext.slots
: {}, : {},
utterance: this.conv.activeContext.originalUtterance, utterance: this.conversation.activeContext.originalUtterance,
configDataFilePath, configDataFilePath,
classification: { classification: {
domain, domain,
skill: skillName, skill: skillName,
action: this.conv.activeContext.nextAction, action: this.conversation.activeContext.nextAction,
confidence: 1 confidence: 1
} }
} }
this.conv.cleanActiveContext() this.conversation.cleanActiveContext()
return BRAIN.execute(this.nluResult) return BRAIN.execute(this.nluResult)
} }
this.conv.cleanActiveContext() this.conversation.cleanActiveContext()
return null return null
} }
@ -617,7 +613,7 @@ export default class NLU {
const hasMandatorySlots = Object.keys(slots)?.length > 0 const hasMandatorySlots = Object.keys(slots)?.length > 0
if (hasMandatorySlots) { if (hasMandatorySlots) {
this.conv.activeContext = { this.conversation.activeContext = {
lang: BRAIN.lang, lang: BRAIN.lang,
slots, slots,
isInActionLoop: false, isInActionLoop: false,
@ -629,7 +625,7 @@ export default class NLU {
entities: this.nluResult.entities entities: this.nluResult.entities
} }
const notFilledSlot = this.conv.getNotFilledSlot() const notFilledSlot = this.conversation.getNotFilledSlot()
// Loop for questions if a slot hasn't been filled // Loop for questions if a slot hasn't been filled
if (notFilledSlot) { if (notFilledSlot) {
const { actions } = JSON.parse( const { actions } = JSON.parse(