Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .yarnrc.yml
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
compressionLevel: mixed

enableGlobalCache: false

nodeLinker: node-modules
36 changes: 20 additions & 16 deletions src/dme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,40 +69,44 @@ export const dme = setup({
return input;
},
initial: "Select",
states: {
Select: {
states: { // Two BIG BOY states: 1) Select, 2) Update
Select: { // “decide what I (the system) will say next.”
initial: "SelectAction",
states: {
SelectAction: {
always: [
isuTransition("SelectMove", "select_respond"),
isuTransition("SelectMove", "select_from_plan"),
{ target: "SelectMove" }, // TODO check it -- needed for greeting
],
SelectAction: { // “What kind of thing should I do next?”
always: [ // lways: [ ... ] block means: as soon as we enter this state, immediately evaluate these transitions in order.
isuTransition("SelectMove", "select_respond"), //select_respond → If there’s a question under discussion (QUD) and the system knows something relevant, prepare a respond action.
isuTransition("SelectMove", "select_from_plan"), // 2. select_from_plan → If there’s something in the current plan, copy the first step to the agenda.
{ target: "SelectMove" }, { target: "SelectMove" } // → default fallback — if nothing matched, just move on (needed for greetings etc.).
], // So, SelectAction picks a rule to fire, and then hands control to SelectMove, which decides what move to produce.
},
SelectMove: {
SelectMove: { // • SelectMove → choose a rule that turns that agenda into a move (an actual utterance).
always: [
isuTransition("SelectionDone", "select_negative_understanding"), // To activate the no input rule
isuTransition("SelectionDone", "select_ask"),
isuTransition("SelectionDone", "select_answer"),
isuTransition("SelectionDone", "select_other"),
{ target: "SelectionDone" },
],
},
SelectionDone: {
SelectionDone: { // SelectionDone → send the next_moves back up to the parent machine (sendBackNextMoves).
always: [{ actions: [{ type: "sendBackNextMoves" }] }],
type: "final",
},
},
onDone: "Update",
},
Update: {
Update: { // “process what you (the user) just said and adjust my memory.”
initial: "Init",
states: {
Init: {
Init: { // Init → clears the old agenda.
always: isuTransition("Grounding", "clear_agenda"),
},
Grounding: {
// TODO: rename to Perception?
/* • Grounding → waits for SAYS (someone spoke). when it happens, it runs
• updateLatestMoves → store what was said,
• get_latest_move → move it into the shared memory.
*/
on: {
SAYS: {
target: "Integrate",
Expand All @@ -115,7 +119,7 @@ export const dme = setup({
},
},
},
Integrate: {
Integrate: { // Integrate → figure out what kind of move it was (ask, answer, greet, etc.) and update the information state.
always: [
isuTransition("DowndateQUD", "integrate_usr_request"),
isuTransition("DowndateQUD", "integrate_sys_ask"),
Expand All @@ -125,7 +129,7 @@ export const dme = setup({
{ target: "DowndateQUD" },
],
},
DowndateQUD: {
DowndateQUD: { // DowndateQUD → remove any questions that are now answered; or look for a plan that handles the new situation.
always: [
isuTransition("LoadPlan", "downdate_qud"),
isuTransition("LoadPlan", "find_plan"),
Expand All @@ -135,7 +139,7 @@ export const dme = setup({
LoadPlan: {
always: { target: "ExecPlan" },
},
ExecPlan: {
ExecPlan: { // • LoadPlan → ExecPlan → perform internal steps like consulting the database, removing resolved actions.
always: [
isuTransition("ExecPlan", "remove_findout"),
isuTransition("ExecPlan", "exec_consultDB"),
Expand Down
38 changes: 33 additions & 5 deletions src/is.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,35 +11,59 @@ export const initialIS = (): InformationState => {
const predicates: { [index: string]: string } = {
// Mapping from predicate to sort
favorite_food: "food",
disliked_food: "food",
booking_course: "course",
booking_room: "room",
booking_day: "day",
};
const individuals: { [index: string]: string } = {
// Mapping from individual to sort
pizza: "food",
sushi: "food",

LT2319: "course",
"Dialogue systems 2": "course",

G212: "room",
J440: "room",
friday: "day",
tuesday: "day",
};
return {
domain: {
predicates: predicates,
individuals: individuals,
plans: [
{
{ // We don’t need two plans with the same content. find_plan picks the first match, so the second won’t run. I
type: "issue",
content: WHQ("booking_room"),
plan: [
findout(WHQ("booking_day")),
findout(WHQ("booking_course")),
consultDB(WHQ("booking_room")),
],
/*
• How the steps tick forward:
1. select_from_plan copies the first plan step to agenda.
2. select_ask turns findout(Q) into a spoken question.
3. user answers → integrate_answer adds a proposition to shared.com.
4. remove_findout sees Q is resolved → pops that step.
5. next step runs (another findout or consultDB).
6. exec_consultDB adds the DB result to beliefs; select_answer turns relevant belief into a spoken answer.
*/
},
],
},
database: {
consultDB: (question, facts) => {
if (objectsEqual(question, WHQ("booking_room"))) {
const course = getFactArgument(facts, "booking_course");
if (course == "LT2319") {
const course = getFactArgument(facts, "booking_course");
const day = getFactArgument(facts, "booking_day");
if (course == "LT2319" && day?.toLowerCase() == "friday") {
return { predicate: "booking_room", argument: "G212" };
}
} else if (course == "LT2319" && day?.toLowerCase() == "tuesday") {
return { predicate: "booking_room", argument: "J440" }
};
}
return null;
},
Expand All @@ -55,6 +79,10 @@ export const initialIS = (): InformationState => {
],
bel: [{ predicate: "favorite_food", argument: "pizza" }],
},
shared: { lu: undefined, qud: [], com: [] },
shared: {
lu: undefined, // When someone speaks, that turn's "moves" gets stored under : is.shared.lu.moves
qud: [],
com: []
},
};
};
22 changes: 18 additions & 4 deletions src/isu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ const settings = {

const dmMachine = setup({
actors: {
/*
In XState, actors are child machines you can spawn.
So this line says:
“inside this big DM machine, I can start a smaller actor called dme using the machine we imported from dme.ts.”
*/
dme: dme,
},
actions: {
Expand Down Expand Up @@ -67,7 +72,7 @@ const dmMachine = setup({
Main: {
type: "parallel",
states: {
Interpret: {
Interpret: { // Interpret is the ears,
initial: "Idle",
states: {
Idle: {
Expand All @@ -93,13 +98,22 @@ const dmMachine = setup({
})),
},
ASR_NOINPUT: {
// TODO
target: "Idle",
actions: [
assign(() => ({
lastUserMoves: [],
})),
sendTo("dmeID", () => ({
type: "SAYS",
value: { speaker: "usr", moves: [] },
})),
],
},
},
},
},
},
Generate: {
Generate: { // Generate is the mouth,
initial: "Idle",
states: {
Idle: {
Expand All @@ -126,7 +140,7 @@ const dmMachine = setup({
},
},
},
DME: {
DME: { // DME is the thinking brain,
invoke: {
src: "dme",
id: "dmeID",
Expand Down
58 changes: 47 additions & 11 deletions src/nlug.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,36 @@ const nluMapping: NLUMapping = {
content: WHQ("favorite_food"),
},
],


pizza: [
{
type: "answer",
content: "pizza",
},
],

sushi: [
{
type: "answer",
content: "sushi",
},
],

friday: [
{
type: "answer",
content: "friday"
}
],

tuesday: [
{
type: "answer",
content: "tuesday"
}
],

"dialogue systems 2": [
{
type: "answer",
Expand All @@ -39,25 +63,37 @@ const nluMapping: NLUMapping = {
],
};
const nlgMapping: NLGMapping = [
[{ type: "ask", content: WHQ("booking_course") }, "Which course?"],

// The greeting
[{ type: "greet", content: null }, "Hello! You can ask me anything!"],

// The questions:
[{ type: "ask", content: WHQ("booking_course")}, "Which course?"],
[{ type: "ask", content: WHQ("booking_day")}, "Which day?"],

// The answers:
[
{
type: "answer",
content: { predicate: "favorite_food", argument: "pizza" },
},
{type: "answer", content: { predicate: "favorite_food", argument: "pizza" }},
"Pizza.",
],
[
{
type: "answer",
content: { predicate: "booking_room", argument: "G212" },
},
{type: "answer", content: { predicate: "booking_room", argument: "G212" }},
"The lecture is in G212.",
],
[
{ type: "answer", content: { predicate: "booking_room", argument: "J440" } },
"The lecture is in J440.",
],

// The no_input case:
[
{ type: "no_input_feedback", content: null },
"I didn’t hear anything from you."
],

];

export function nlg(moves: Move[]): string {
export function nlg(moves: Move[]): string { // nlg() finds matching moves and joins them into a final utterance string.
console.log("generating moves", moves);
function generateMove(move: Move): string {
const mapping = nlgMapping.find((x) => objectsEqual(x[0], move));
Expand All @@ -73,6 +109,6 @@ export function nlg(moves: Move[]): string {

/** NLU mapping function can be replaced by statistical NLU
*/
export function nlu(utterance: string): Move[] {
export function nlu(utterance: string): Move[] { // nlu() just looks up a user’s string (case-insensitive) and returns its Move[].
return nluMapping[utterance.toLowerCase()] || [];
}
39 changes: 39 additions & 0 deletions src/rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,33 @@ export const rules: Rules = {
});
},

/**
* No input rule
*/

select_negative_understanding: ({ is }) => {
const lu = is.shared.lu;
if (!(lu && lu.speaker === "usr" && lu.moves.length === 0)) return;

// Look for the current question to (re)ask if plan is waiting on a findout/raise
const pending = is.private.agenda[0] ?? is.private.plan[0];
const shouldRepeat =
pending && (pending.type === "findout" || pending.type === "raise");
const q = shouldRepeat ? (pending.content as Question) : undefined;

const newMoves = [
...is.next_moves,
{ type: "no_input_feedback", content: null } as Move,
...(q ? [{ type: "ask", content: q } as Move] : []),
];

// Note: we DO NOT pop the plan here. (For 'raise', Select’s own logic may pop.)
return () => ({
...is,
next_moves: newMoves,
});
},

/**
* Grounding
*/
Expand Down Expand Up @@ -69,6 +96,18 @@ export const rules: Rules = {
for (const move of is.shared.lu!.moves) {
if (move.type === "ask") {
const q = move.content;
const topQUD = is.shared.qud[0];

// 🔍 NEW: if we are re-asking the *same* question, don’t push a duplicate
if (topQUD && objectsEqual(topQUD, q)) {
return () => ({
...is,
// QUD unchanged
shared: { ...is.shared },
});
}

// original behaviour
return () => ({
...is,
shared: {
Expand Down
Loading