From 6e09f39666fae556e10a79e40c2bc7db0463740f Mon Sep 17 00:00:00 2001 From: vavanzan2 Date: Fri, 26 Sep 2025 19:34:54 +0200 Subject: [PATCH] Lab1 --- src/is.ts | 50 +++++++++++++++++++++------ src/isu.ts | 7 +++- src/nlug.ts | 84 ++++++++++++++++++++++++++++++++++++++++----- src/rules.ts | 57 +++++++++++++++++++++++++------ src/types.ts | 8 ++++- test/dme.test.ts | 89 ++++++++++++++++++++++++++++++++++++++++++++++-- 6 files changed, 262 insertions(+), 33 deletions(-) diff --git a/src/is.ts b/src/is.ts index d04611d..5cdc81c 100644 --- a/src/is.ts +++ b/src/is.ts @@ -7,19 +7,29 @@ import { getFactArgument, } from "./utils"; -export const initialIS = (): InformationState => { +export const initialIS = (): InformationState => { //based on const we have the return const predicates: { [index: string]: string } = { // Mapping from predicate to sort favorite_food: "food", + food_you_hate: "food", + which_day: "day", + booking_room: "room", booking_course: "course", }; - const individuals: { [index: string]: string } = { + const individuals: { [index: string]: string } = { //based on individuals we have the return // Mapping from individual to sort pizza: "food", + hotdog: "food", + kebab: "food", + friday: "day", + tuesday: "day", + G212: "room", + J440: "room", LT2319: "course", + "Dialogue Systems 2": "course", }; return { - domain: { + domain: { //domain is the posisbilities that are available. All possible plans. predicates: predicates, individuals: individuals, plans: [ @@ -27,34 +37,54 @@ export const initialIS = (): InformationState => { type: "issue", content: WHQ("booking_room"), plan: [ - findout(WHQ("booking_course")), + findout(WHQ("which_day")), + findout (WHQ("booking_course")), consultDB(WHQ("booking_room")), ], }, + { + type: "issue", + content: WHQ("which_course"), + plan: [ + findout(WHQ("booking_course")), + consultDB(WHQ("which_day")), + ], + }, ], }, database: { consultDB: (question, facts) => { if (objectsEqual(question, WHQ("booking_room"))) { - const course = getFactArgument(facts, "booking_course"); - if (course == "LT2319") { + const day = getFactArgument(facts, "which_day"); + const course = getFactArgument(facts, "booking_course"); // likely "LT2319" + // Only answer if the course is Dialogue systems 2 (LT2319) + if (!course || course.toLowerCase() !== "lt2319") return null; + //facts mean answer to the question + if (day?.toLowerCase() == "friday") { return { predicate: "booking_room", argument: "G212" }; + } else if (day?.toLowerCase() == "tuesday") { + return { predicate: "booking_room", argument: "J440" }; } } return null; }, }, next_moves: [], + private: { - plan: [], - agenda: [ + plan: [], // Plans are domain-specific and high-level descriptions of how goals are achieved. + agenda: [ // next action to be performed { type: "greet", content: null, }, ], - bel: [{ predicate: "favorite_food", argument: "pizza" }], + bel: [{ predicate: "favorite_food", argument: "pizza" }], //things in your head, ex.: you know in your mind }, - shared: { lu: undefined, qud: [], com: [] }, + shared: { + lu: undefined, // lu is the last utterance + qud: [], // QUD + com: [] //com is common ground + }, }; }; diff --git a/src/isu.ts b/src/isu.ts index 82f21ea..36685f2 100644 --- a/src/isu.ts +++ b/src/isu.ts @@ -83,7 +83,7 @@ const dmMachine = setup({ type: "SAYS", value: { speaker: "usr", - moves: context.lastUserMoves, + moves: context.lastUserMoves ?? [], }, })), }, @@ -95,6 +95,11 @@ const dmMachine = setup({ ASR_NOINPUT: { // TODO }, + ASR_NOINPUT: { + target: "Idle", + actions: sendTo("dmID", () => ({ type: "SAYS", value:{ speaker: "usr", moves: [],}, + })), }, + } }, }, }, diff --git a/src/nlug.ts b/src/nlug.ts index dfd3741..ed005ed 100644 --- a/src/nlug.ts +++ b/src/nlug.ts @@ -7,15 +7,19 @@ interface NLUMapping { type NLGMapping = [Move, string][]; const nluMapping: NLUMapping = { - "where is the lecture?": [{ + "where is the lecture?": [{ //it has to do with the type, which is ask type: "ask", content: WHQ("booking_room"), }], - "what's your favorite food?": [{ + "what's your favorite food?": [{ //it has to do with the type, which is ask type: "ask", - content: WHQ("favorite_food"), + content: WHQ("favorite_food"), //WHQ is a type of question that has a predicate }], - pizza: [{ + "can you book me a course": [{ + type: "ask", + content: WHQ("booking_course"), + }], + pizza: [{ //define answer objects type: "answer", content: "pizza", }], @@ -27,12 +31,36 @@ const nluMapping: NLUMapping = { type: "answer", content: "LT2319", }], + "friday": [{ + type: "answer", + content: "friday", + }], + "tuesday": [{ + type: "answer", + content: "tuesday", + }], + "G212": [{ + type: "answer", + content: "G212", + }], + "J440": [{ + type: "answer", + content: "J440", + }], }; const nlgMapping: NLGMapping = [ + [{ type: "neg_contact", content: null}, "I didn’t hear anything from you."], [{ type: "ask", content: WHQ("booking_course") }, "Which course?"], + [{ type: "ask", content: WHQ("which_day") }, "Which day?"], + [{ type: "answer", content: { predicate: "booking_room", argument: "J440" } }, + "The lecture is in J440.", + ], + [{ type: "answer", content: { predicate: "booking_room", argument: "G212" } }, + "The lecture is in G212.", + ], + [{ type: "greet", content: null }, "Hello! You can ask me anything!"], - [ - { + [{ type: "answer", content: { predicate: "favorite_food", argument: "pizza" }, }, @@ -49,20 +77,58 @@ const nlgMapping: NLGMapping = [ export function nlg(moves: Move[]): string { console.log("generating moves", moves); + + // 🔹 helpers mínimos para fallback + function realizeAsk(move: Move): string { + const q: any = move.content; + const p = (q?.predicate || "").toLowerCase(); + if (p === "booking_course") return "Which course?"; + if (p === "which_day" || p === "booking_day" || p === "day") return "Which day?"; + if (p === "booking_room" || p === "room") return "Which room?"; + return `Which ${p}?`; + } +function realizeAnswer(move: Move): string { + const c: any = move.content; + if (typeof c === "string") { + // nunca retorne vazio + return c.length > 0 ? c : "Sorry, I don’t have an answer."; + } + const pred = (c?.predicate || "").toLowerCase(); + const arg = c?.argument ?? ""; + if (pred === "booking_room" || pred === "room" || pred === "location" || pred === "loc") { + // ✅ frase que o teste espera + return `The lecture is in ${arg}.`; + } + // fallback seguro + return `${pred}: ${arg}`; + } + + function generateMove(move: Move): string { const mapping = nlgMapping.find((x) => objectsEqual(x[0], move)); if (mapping) { return mapping[1]; } - throw new Error(`Failed to generate move ${JSON.stringify(move)}`); + // 🔹 Fallbacks mínimos (mantendo sua lógica original como prioridade) + if (move.type === "ask") return realizeAsk(move); + if (move.type === "answer") return realizeAnswer(move); + if (move.type === "greet") return "Hello! You can ask me anything!"; + if (move.type === "neg_contact") return "I didn’t hear anything from you."; + if (move.type === "request") return (move.content as string) || "What would you like to know?"; + + // Último recurso (não retornar string vazia) + return "Sorry, I’m not sure."; } + const utterance = moves.map(generateMove).join(' '); console.log("generated utterance:", utterance); - return utterance; +return utterance.length > 0 ? utterance : "I didn’t hear anything from you."; } /** NLU mapping function can be replaced by statistical NLU */ export function nlu(utterance: string): Move[] { - return nluMapping[utterance.toLowerCase()] || []; + const u = utterance.toLowerCase(); + if ( u === "*no_input*") return []; + return nluMapping[u] || []; } diff --git a/src/rules.ts b/src/rules.ts index 7d96875..e78c686 100644 --- a/src/rules.ts +++ b/src/rules.ts @@ -6,7 +6,21 @@ import { Action, } from "./types"; import { relevant, resolves, combine } from "./semantics"; -import { objectsEqual } from "./utils"; +import { objectsEqual, WHQ } from "./utils"; + +export function negativeContactRule(is: InformationState): InformationState { + const lu = is.shared.lu; + if (lu?.speaker === "usr" && (!lu.moves || lu.moves.length === 0)) { + const moves: Move[] = [{ type: "neg_contact", content: null }]; + const topQ = is.shared.qud[0]; // pergunta corrente (se houver) + if (topQ) { + moves.push({ type: "ask", content: topQ }); + } + const base = is.next_moves ?? []; + is.next_moves = [...base, ...moves]; + } + return is; +} type Rules = { [index: string]: ( @@ -38,6 +52,12 @@ export const rules: Rules = { }); }, + // Task 2a/2b: feedback (+ repete question if there's QUD) + neg_contact: ({ is }) => { + // sempre retornamos o estado (mesmo que sem mudanças) para manter o pipeline puro + return () => negativeContactRule(is); + }, + /** * Integrate */ @@ -140,10 +160,6 @@ export const rules: Rules = { } }, - /** TODO rule 2.7 integrate_usr_quit */ - - /** TODO rule 2.8 integrate_sys_quit */ - /** * DowndateQUD */ @@ -223,12 +239,16 @@ export const rules: Rules = { is.shared.com, ); if (propositionFromDB) { + // FIX: sintaxe e lógica — apenas agenda 'respond' e salva a crença. + // Quem coloca o 'answer' em next_moves é select_respond/select_answer. + const respondAction: Action = { type: "respond", content: question }; return () => ({ ...is, private: { ...is.private, - plan: [...is.private.plan.slice(1)], + plan: is.private.plan.slice(1), bel: [...is.private.bel, propositionFromDB], + agenda: [respondAction, ...is.private.agenda], }, }); } @@ -261,16 +281,33 @@ export const rules: Rules = { ["findout", "raise"].includes(is.private.agenda[0].type) ) { const q = is.private.agenda[0].content as Question; + // Se o último turno do usuário foi "no-input" (sem moves), prefixa o feedback. + const noInput = + is.shared.lu?.speaker === "usr" && + (!is.shared.lu.moves || is.shared.lu.moves.length === 0); + const maybeNeg: Move[] = noInput + ? [{ type: "neg_contact", content: null }] + : []; + if (is.private.plan[0] && is.private.plan[0].type === "raise") { + // FIX: incluir maybeNeg também no ramo 'raise' newIS = { ...is, - next_moves: [ ...is.next_moves, { type: "ask", content: q } ], + next_moves: [ + ...is.next_moves, + ...maybeNeg, + { type: "ask", content: q }, + ], private: { ...is.private, plan: [...is.private.plan.slice(1)] }, }; } else { newIS = { ...is, - next_moves: [ ...is.next_moves, { type: "ask", content: q } ], + next_moves: [ + ...is.next_moves, + ...maybeNeg, + { type: "ask", content: q }, + ], }; } return () => newIS; @@ -314,7 +351,7 @@ export const rules: Rules = { const answerMove: Move = { type: "answer", content: bel }; return () => ({ ...is, - next_moves: [ ...is.next_moves, answerMove ] + next_moves: [...is.next_moves, answerMove], }); } } @@ -326,7 +363,7 @@ export const rules: Rules = { if (is.private.agenda[0] && is.private.agenda[0].type === "greet") { return () => ({ ...is, - next_moves: [ ...is.next_moves, is.private.agenda[0] as Move ] + next_moves: [...is.next_moves, is.private.agenda[0] as Move], }); } }, diff --git a/src/types.ts b/src/types.ts index 5b146de..f752cdc 100644 --- a/src/types.ts +++ b/src/types.ts @@ -42,8 +42,14 @@ interface AskMove { type: "ask"; content: Question; } +interface FeedbackMove { + type: "neg_contact"; + content: null; +} + +export type Move = OtherMove | AnswerMove | AskMove | FeedbackMove; +// Feedback from the system to the user (*no_input*) -export type Move = OtherMove | AnswerMove | AskMove; export type Action = { type: diff --git a/test/dme.test.ts b/test/dme.test.ts index 917dbcd..09e25e2 100644 --- a/test/dme.test.ts +++ b/test/dme.test.ts @@ -118,7 +118,7 @@ describe("DME tests", () => { actor, (snapshot) => snapshot.context.dialogue.length === expectedSoFar.length, { - timeout: 1000 /** allowed time to transition to the expected state */, + timeout: 4000 /** allowed time to transition to the expected state */, }, ); expect(snapshot.context.dialogue).toEqual(expectedSoFar); @@ -132,14 +132,99 @@ describe("DME tests", () => { { speaker: "sys", message: "Pizza." }, ]); }); + describe("Negative system contact feedback", () => { + runTest([ + { speaker: "sys", message: "Hello! You can ask me anything!" }, + { speaker: "usr", message: "*no_input*" }, + { speaker: "sys", message: "I didn’t hear anything from you." }, + ]); +}); + describe("Negative feedback followed by repeated question", () => { + runTest([ + { speaker: "sys", message: "Hello! You can ask me anything!" }, + { speaker: "usr", message: "Where is the lecture?" }, + { speaker: "sys", message: "Which day?" }, + { speaker: "usr", message: "*no_input*" }, + { speaker: "sys", message: "I didn’t hear anything from you. Which day?" }, + { speaker: "usr", message: "friday" }, + { speaker: "sys", message: "Which course?" }, + { speaker: "usr", message: "*no_input*" }, + { speaker: "sys", message: "I didn’t hear anything from you. Which course?" }, + { speaker: "usr", message: "Dialogue Systems 2" }, + { speaker: "sys", message: "The lecture is in G212." }, + ]); +}); + +describe("Task 2c – Multiple no_input repetitions (Friday: G212)", () => { + runTest([ + { speaker: "sys", message: "Hello! You can ask me anything!" }, + { speaker: "usr", message: "Where is the lecture?" }, + { speaker: "sys", message: "Which day?" }, + + + { speaker: "usr", message: "*no_input*" }, + { speaker: "sys", message: "I didn’t hear anything from you. Which day?" }, + { speaker: "usr", message: "*no_input*" }, + { speaker: "sys", message: "I didn’t hear anything from you. Which day?" }, + + + { speaker: "usr", message: "friday" }, + { speaker: "sys", message: "Which course?" }, + + { speaker: "usr", message: "*no_input*" }, + { speaker: "sys", message: "I didn’t hear anything from you. Which course?" }, + { speaker: "usr", message: "*no_input*" }, + { speaker: "sys", message: "I didn’t hear anything from you. Which course?" }, + + + { speaker: "usr", message: "Dialogue Systems 2" }, + { speaker: "sys", message: "The lecture is in G212." }, + ]); +}); +describe("Task 2c – Multiple no_input repetitions (Tuesday: J440)", () => { + runTest([ + { speaker: "sys", message: "Hello! You can ask me anything!" }, + { speaker: "usr", message: "Where is the lecture?" }, + { speaker: "sys", message: "Which day?" }, + + { speaker: "usr", message: "*no_input*" }, + { speaker: "sys", message: "I didn’t hear anything from you. Which day?" }, + + { speaker: "usr", message: "tuesday" }, + { speaker: "sys", message: "Which course?" }, + + { speaker: "usr", message: "*no_input*" }, + { speaker: "sys", message: "I didn’t hear anything from you. Which course?" }, + { speaker: "usr", message: "*no_input*" }, + { speaker: "sys", message: "I didn’t hear anything from you. Which course?" }, + + { speaker: "usr", message: "Dialogue Systems 2" }, + { speaker: "sys", message: "The lecture is in J440." }, + ]); +}); + describe("system answer from database", () => { runTest([ - { speaker: "sys", message: "Hello! You can ask me anything!" }, + { speaker: "sys", message: "Hello! You can ask me anything!"}, { speaker: "usr", message: "Where is the lecture?" }, + { speaker: "sys", message: "Which day?"}, + { speaker: "usr", message: "friday" }, { speaker: "sys", message: "Which course?" }, { speaker: "usr", message: "Dialogue Systems 2" }, { speaker: "sys", message: "The lecture is in G212." }, ]); }); + describe("system answer from database", () => { + runTest([ + { speaker: "sys", message: "Hello! You can ask me anything!" }, + { speaker: "usr", message: "Where is the lecture?" }, + { speaker: "sys", message: "Which day?" }, + { speaker: "usr", message: "tuesday" }, + { speaker: "sys", message: "Which course?" }, + { speaker: "usr", message: "Dialogue Systems 2" }, + { speaker: "sys", message: "The lecture is in J440." }, + ]); + }); }); +