diff --git a/src/isu.ts b/src/isu.ts index 7800c9b..4910f7f 100644 --- a/src/isu.ts +++ b/src/isu.ts @@ -47,7 +47,7 @@ const dmMachine = setup({ context.ssRef.send({ type: "SPEAK", value: { - utterance: nlg(context.next_move), + utterance: nlg(context.is.next_move), }, }), /** ASR */ @@ -64,18 +64,27 @@ const dmMachine = setup({ context: ({ spawn }) => { return { ssRef: spawn(speechstate, { input: settings }), - next_move: { - type: "greet", - content: null, - }, is: { + domain: [ + { + type: "resolves", + content: ["pizza", (x) => `favorite_food ${x}`], + }, + { + type: "resolves", + content: ["favorite_food pizza", (x) => `favorite_food ${x}`], + }, + ], + next_move: null, private: { + plan: [], agenda: [ { type: "greet", content: null, }, ], + bel: ["favorite_food pizza"], }, shared: { lu: undefined, qud: [], com: [] }, }, @@ -140,33 +149,108 @@ const dmMachine = setup({ Idle: { always: { target: "Speaking", - guard: ({ context }) => !!context.next_move, + guard: ({ context }) => !!context.is.next_move, }, }, Speaking: { - entry: [ - raise(({ context }) => ({ - type: "SAYS", - value: { - speaker: "sys", - move: context.next_move, - }, - })), - "speak_next_move", - assign({ next_move: null }), - ], + entry: "speak_next_move", on: { SPEAK_COMPLETE: { target: "Idle", + actions: [ + raise(({ context }) => ({ + type: "SAYS", + value: { + speaker: "sys", + move: context.is.next_move, + }, + })), + assign(({ context }) => { + return { is: { ...context.is, next_move: null } }; + }), + ], }, }, }, }, }, DME: { - initial: "Update", // todo: shd be Select + initial: "Select", states: { - Select: {}, + Select: { + entry: ({ context }) => + console.debug("[DM] ENTERING SELECT", context.is), + initial: "SelectAction", + states: { + SelectAction: { + always: [ + { + target: "SelectMove", + guard: { + type: "isu", + params: { name: "select_respond" }, + }, + actions: { + type: "isu", + params: { name: "select_respond" }, + }, + }, + { + target: "SelectMove", + guard: { + type: "isu", + params: { name: "select_from_plan" }, + }, + actions: { + type: "isu", + params: { name: "select_from_plan" }, + }, + }, + { target: "SelectMove" }, // TODO check it -- needed for greeting + ], + }, + SelectMove: { + always: [ + { + target: "SelectionDone", + guard: { + type: "isu", + params: { name: "select_ask" }, + }, + actions: { + type: "isu", + params: { name: "select_ask" }, + }, + }, + { + target: "SelectionDone", + guard: { + type: "isu", + params: { name: "select_answer" }, + }, + actions: { + type: "isu", + params: { name: "select_answer" }, + }, + }, + { + target: "SelectionDone", + guard: { + type: "isu", + params: { name: "select_other" }, + }, + actions: { + type: "isu", + params: { name: "select_other" }, + }, + }, + { target: "SelectionDone" }, + ], + }, + SelectionDone: { type: "final" }, + }, + onDone: "Update", + }, Update: { initial: "Init", states: { @@ -178,11 +262,11 @@ const dmMachine = setup({ }, }, Grounding: { + // TODO: rename to Perception? on: { SAYS: { target: "Integrate", actions: [ - // () => console.log("<>"), { type: "updateLatestMove", }, @@ -229,7 +313,20 @@ const dmMachine = setup({ ], }, DowndateQUD: { - always: { target: "LoadPlan" }, + always: [ + { + target: "LoadPlan", + guard: { + type: "isu", + params: { name: "downdate_qud" }, + }, + actions: { + type: "isu", + params: { name: "downdate_qud" }, + }, + }, + { target: "LoadPlan" }, + ], }, LoadPlan: { always: { target: "ExecPlan" }, @@ -275,7 +372,10 @@ export function setupButton(element: HTMLElement) { } /** -usr> What's your favourite food? +sys> Hello! You can ask me anything! +{type: "greet", content: null} + +usr> What's your favorite food? {type: "ask", content: (x) => `favorite_food ${x}`} sys> Pizza diff --git a/src/rules.ts b/src/rules.ts index 40ddba9..b12e5f0 100644 --- a/src/rules.ts +++ b/src/rules.ts @@ -1,15 +1,10 @@ -import { Question, DMContext, InformationState } from "./types"; - -/** TODO need something better... */ -function domainRelevant(answer: string, question: Question): boolean { - if ( - question === ((x: string) => `favorite_food ${x}`) && - answer === "pizza" - ) { - return true; - } - return false; -} +import { + Question, + DMContext, + InformationState, + Move, + DomainRelation, +} from "./types"; type Rules = { [index: string]: (context: DMContext) => { @@ -18,11 +13,18 @@ type Rules = { }; }; +function relevant(x: DomainRelation): boolean { + return ["relevant", "resolves"].includes(x.type); +} +function resolves(x: DomainRelation): boolean { + return "resolves" === x.type; +} + export const rules: Rules = { - clear_agenda: (context) => { + clear_agenda: ({ is }) => { const newIS = { - ...context.is, - private: { ...context.is.private, agenda: [] }, + ...is, + private: { ...is.private, agenda: [] }, }; console.debug(`[ISU clear_agenda]`, newIS); return { @@ -56,19 +58,13 @@ export const rules: Rules = { * Integrate */ /** rule 2.2 */ - integrate_sys_ask: (context) => { - if ( - context.is.shared.lu!.speaker === "sys" && - context.is.shared.lu!.move.type === "ask" - ) { + integrate_sys_ask: ({ is }) => { + if (is.shared.lu!.speaker === "sys" && is.shared.lu!.move.type === "ask") { const newIS = { - ...context.is, + ...is, shared: { - ...context.is.shared, - qud: [ - context.is.shared.lu!.move.content as Question, - ...context.is.shared.qud, - ], + ...is.shared, + qud: [is.shared.lu!.move.content as Question, ...is.shared.qud], }, }; console.debug(`[ISU integrate_sys_ask]`, newIS); @@ -79,24 +75,18 @@ export const rules: Rules = { } return { preconditions: false, - effects: context.is, + effects: is, }; }, /** rule 2.3 */ - integrate_usr_ask: (context) => { - if ( - context.is.shared.lu!.speaker === "usr" && - context.is.shared.lu!.move.type === "ask" - ) { + integrate_usr_ask: ({ is }) => { + if (is.shared.lu!.speaker === "usr" && is.shared.lu!.move.type === "ask") { const newIS = { - ...context.is, + ...is, shared: { - ...context.is.shared, - qud: [ - context.is.shared.lu!.move.content as Question, - ...context.is.shared.qud, - ], + ...is.shared, + qud: [is.shared.lu!.move.content as Question, ...is.shared.qud], }, }; console.debug(`[ISU integrate_usr_ask]`, newIS); @@ -107,31 +97,39 @@ export const rules: Rules = { } return { preconditions: false, - effects: context.is, + effects: is, }; }, /** rule 2.4 */ - integrate_answer: (context) => { - const q = context.is.shared.qud[0]; - const a = context.is.shared.lu?.move.content as string; - if (context.is.shared.lu?.move.type === "answer" && domainRelevant(a, q)) { - const newIS = { - ...context.is, - shared: { - ...context.is.shared, - com: [q(a), ...context.is.shared.com], - }, - }; - console.debug(`[ISU integrate_answer]`, newIS); - return { - preconditions: false, - effects: context.is, - }; + integrate_answer: ({ is }) => { + const topQUD = is.shared.qud[0]; + const a = is.shared.lu!.move.content as string; + if (topQUD && is.shared.lu!.move.type === "answer") { + if ( + !!is.domain + .filter((r) => relevant(r)) + .filter((r) => r.content[1].toString() === topQUD.toString()) + .filter((r) => r.content[0] === a)[0] + ) { + // TODO (?) should combined proposition be added to domain? + const newIS = { + ...is, + shared: { + ...is.shared, + com: [topQUD(a), ...is.shared.com], + }, + }; + console.debug(`[ISU integrate_usr_ask]`, newIS); + return { + preconditions: true, + effects: newIS, + }; + } } return { preconditions: false, - effects: context.is, + effects: is, }; }, @@ -152,29 +150,192 @@ export const rules: Rules = { effects: context.is, }; }, -}; -// resolve_top_qud: (c) => { -// if (c.is.shared.lu) { -// if (c.is.shared.lu.move.type === "answer" && c.is.shared.qud[0]) { -// let q = c.is.shared.qud[0]; -// let r = c.is.shared.lu.move.content as string; -// return { -// guard: true, -// effects: assign(({ context }: { context: DMContext }) => { -// const newIS = { -// ...context.is, -// shared: { -// ...context.is.shared, -// com: [q(r), ...context.is.shared.com], -// }, -// }; -// console.debug("[ISU resolve_top_qud]", newIS); -// return { is: newIS }; -// }), -// }; -// } -// } -// return { guard: false, effects: undefined }; -// }, -// }; + /** TODO rule 2.7 integrate_usr_quit */ + + /** TODO rule 2.8 integrate_sys_quit */ + + /** + * DowndateQUD + */ + /** rule 2.5 */ + downdate_qud: ({ is }) => { + const q = is.shared.qud[0]; + for (const p of is.shared.com) { + if ( + is.domain + .filter((r) => resolves(r)) + .filter((r) => r.content[0] === p) + .some((r) => r.content[0].toString() === q.toString()) + ) { + const newIS = { + ...is, + shared: { + ...is.shared, + qud: [...is.shared.qud.slice(1)], + }, + }; + console.debug(`[ISU downdate_qud]`, newIS); + return { + preconditions: false, + effects: newIS, + }; + } + } + return { + preconditions: false, + effects: is, + }; + }, + + /** + * ExecPlan + */ + /** rule 2.9: for now, we assume that there is always a BEL for every + * question in the domain + */ + find_plan: (context) => { + return { + preconditions: false, + effects: context.is, + }; + }, + + /** TODO rule 2.10 remove_findout */ + + /** TODO rule 2.11 exec_consult_db */ + + /** + * Select + */ + /** rule 2.12 */ + select_from_plan: ({ is }) => { + if (is.private.agenda.length === 0 && !!is.private.plan[0]) { + const action = is.private.plan[0]; + const newIS = { + ...is, + private: { + ...is.private, + agenda: [action, ...is.private.agenda], + }, + }; + console.debug(`[ISU select_from_plan]`, newIS); + return { + preconditions: true, + effects: newIS, + }; + } + return { + preconditions: false, + effects: is, + }; + }, + + /** rule 2.13 */ + select_ask: ({ is }) => { + let newIS = is; + if ( + is.private.agenda[0] && + ["findout", "raise"].includes(is.private.agenda[0].type) + ) { + const q = is.private.agenda[0].content; + if (is.private.plan[0] && is.private.plan[0].type === "raise") { + newIS = { + ...is, + next_move: { type: "ask", content: q }, + private: { ...is.private, plan: [...is.private.plan.slice(1)] }, + }; + console.debug(`[ISU select_ask]`, newIS); + } else { + newIS = { + ...is, + next_move: { type: "ask", content: q }, + }; + console.debug(`[ISU select_ask]`, newIS); + } + return { + preconditions: true, + effects: newIS, + }; + } + return { + preconditions: false, + effects: is, + }; + }, + + /** rule 2.14 */ + select_respond: ({ is }) => { + if ( + is.private.agenda.length === 0 && + is.private.plan.length === 0 && + is.shared.qud[0] + ) { + const topQUD = is.shared.qud[0]; + for (const rel of is.domain + .filter((r) => relevant(r)) + .filter((r) => r.content[1].toString() === topQUD.toString())) { + const p = rel.content[0]; + if (is.private.bel.includes(p) && !is.shared.com.includes(p)) { + const respondMove: Move = { type: "respond", content: topQUD }; + const newIS = { + ...is, + private: { + ...is.private, + agenda: [respondMove, ...is.private.agenda], + }, + }; + console.debug(`[ISU select_respond]`, newIS); + return { + preconditions: true, + effects: newIS, + }; + } + } + } + return { + preconditions: false, + effects: is, + }; + }, + + select_answer: ({ is }) => { + if (is.private.agenda[0] && is.private.agenda[0].type === "respond") { + const question = is.private.agenda[0].content as Question; + for (const rel of is.domain + .filter((r) => relevant(r)) + .filter((r) => r.content[1].toString() === question.toString())) { + const p = rel.content[0]; + if (is.private.bel.includes(p) && !is.shared.com.includes(p)) { + const answerMove: Move = { type: "answer", content: p }; + const newIS = { ...is, next_move: answerMove }; + console.debug(`[ISU select_answer]`, newIS); + return { + preconditions: true, + effects: newIS, + }; + } + } + } + return { + preconditions: false, + effects: is, + }; + }, + + /** only for greet for now */ + select_other: ({ is }) => { + if (is.private.agenda[0] && is.private.agenda[0].type === "greet") { + const newIS = { ...is, next_move: is.private.agenda[0] }; + console.debug(`[ISU select_answer]`, newIS); + return { + preconditions: true, + effects: newIS, + }; + } + return { + preconditions: false, + effects: is, + }; + }, +}; diff --git a/src/types.ts b/src/types.ts index a5c81a4..3184995 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,21 +1,39 @@ import { SpeechStateExternalEvent } from "speechstate"; +export type DomainRelation = { + type: "resolves" | "relevant"; + content: [ShortAnswer | Proposition, Question]; +}; + +type ShortAnswer = string; +type Proposition = string; + export type Question = WhQuestion; -type WhQuestion = (a: string) => string; +type WhQuestion = (a: ShortAnswer) => Proposition; export interface Move { - type: "ask" | "answer" | "respond" | "greet" | "unknown"; - content: null | string | Question; + // no difference between Move and Action for now + type: + | "ask" + | "answer" + | "respond" + | "greet" + | "unknown" + | "raise" + | "findout"; + content: null | Proposition | ShortAnswer | Question; } type Speaker = "usr" | "sys"; export interface InformationState { - private: { agenda: Move[] }; + next_move: Move | null; + domain: DomainRelation[]; + private: { agenda: Move[]; plan: Move[]; bel: Proposition[] }; shared: { lu?: { speaker: Speaker; move: Move }; - qud: ((a: string) => string)[]; - com: string[]; + qud: Question[]; + com: Proposition[]; }; } @@ -23,7 +41,6 @@ export interface DMContext { ssRef: any; /** interface variables */ - next_move: Move | null; latest_speaker?: Speaker; latest_move?: Move; @@ -34,5 +51,5 @@ export interface DMContext { export type DMEvent = SpeechStateExternalEvent | SaysMoveEvent; export type SaysMoveEvent = { type: "SAYS"; - value: { speaker: string; move: Move }; + value: { speaker: Speaker; move: Move }; };