!7 concept Cmd { S patterns; // for mmo_parsePattern S exampleInputs; // line-separated S cmd; // a star pattern recognized by the backend S questionsForArguments; // line-separated S conditions; int index; bool autoGenerated; LS translatableFields() { ret splitAtSpace("patterns exampleInputs cmd questionsForArguments"); } transient MMOPattern parsedPattern; void change { parsedPattern = null; super.change(); } MMOPattern parsedPattern() { if (parsedPattern == null) parsedPattern = mmo_parsePattern(patterns); ret parsedPattern; } sS _fieldOrder = "autoGenerated exampleInputs patterns cmd questionsForArguments conditions"; } concept Replacement { S in, out; // word to replace and which word to replace it with S except; // IDs of Cmd concepts to skip sS _fieldOrder = "in out except"; } concept Mishearing { S in, out; } cmodule ChatBotFrontend > DynCRUD { switchable S backendModuleLibID = "#1027443/Scenarios"; O currentAttractor; transient CRUD replacementsCRUD = new(Replacement); transient CRUD mishearingsCRUD = new(Mishearing); class HandleArgument implements IF1 { S cmd; LS argQuestions; new LS arguments; *() {} *(S *cmd, LS *argQuestions) {} // process an argument public S get(S arg) { arguments.add(arg); if (l(arguments) >= countAsteriskTokens(cmd)) ret sendToBackend(format_quoteAll(cmd, asObjectArray(arguments))); else ret handleQuestionWithArguments(this); } S currentQuestion() { ret or2(_get(argQuestions, l(arguments)), "Need argument"); } } runnable class Cancel { setField(currentAttractor := null); } class UndoHandler implements IF1 { public S get(Bool yes) { if (!yes) ret "OK"; // user said no ret (S) sendToBackend("undo"); } } start { dm_watchFieldAndNow backendModuleLibID(r { dm_setModuleName("Frontend for " + dm_moduleName(backend())) }); crud.multiLineFields = litset("exampleInputs", "questionsForArguments"); crud.sorter = func(Cl l) -> L { sortByField index(l) }; crud.formFixer = 48; } S handleCommand(Cmd cmd) { if (cmd == null) null; print("handleCommand " + cmd.cmd); if (match("undo after confirm", cmd.cmd)) { O undo = dm_call(backend(), 'lastUndo); if (undo == null) ret "Nothing to undo"; new WaitForAnswer_YesNo attractor; attractor.question = "Undo " + undo + "?"; setField(currentAttractor := attractor); print("Set attractor to: " + attractor); attractor.processAnswer = new UndoHandler; attractor.cancelSilently = new Cancel; ret attractor.question; } if (hasAsteriskTokens(cmd.cmd)) ret handleQuestionWithArguments( new HandleArgument(cmd.cmd, tlft(cmd.questionsForArguments))); else ret sendToBackend(cmd.cmd); } S handleQuestionWithArguments(HandleArgument handler) { new WaitForName attractor; attractor.question = handler.currentQuestion(); setField(currentAttractor := attractor); print("Set attractor to: " + attractor); attractor.processName = handler; attractor.cancelSilently = new Cancel; ret attractor.question; } S sendToBackend(S cmd) { ret (S) dm_call(backend(), 'answer, cmd); } S backend() { ret dm_require(backendModuleLibID); } visual { JComponent mainCRUD = super.visualize(); addComponents(crud.buttons, jbutton("Talk to me", rThread talkToMe), jPopDownButton_noText( "Apply replacements", rThread applyReplacements, "Consistency check", rThread consistencyCheck)); ret jtabs( "Commands" := mainCRUD, "Replacements" := replacementsCRUD.visualize(), "Mishearings" := mishearingsCRUD.visualize()); } void talkToMe enter { dm_showConversationPopupForModule(); } void consistencyCheck enter { reindex(); L cmds = list(Cmd); L errors = mmo_consistencyCheck( map(cmds, cmd -> pair(cmd.exampleInputs, cmd.patterns))); if (empty(errors)) infoBox("No consistency problems"); else dm_showText(n2(errors, "consistency problem"), lines(errors)); } void reindex { int index = 1; for (Cmd cmd : conceptsSortedByFields Cmd('autoGenerated, 'index)) cset(cmd, index := index++); } void applyReplacements { deleteConceptsWhere Cmd(autoGenerated := true); for (Cmd cmd) for (Replacement r) { if (jcontains(r.except, str(cmd.id))) continue; SS map = litcimap(r.in, r.out, plural(r.in), plural(r.out)); Cmd cmd2 = shallowCloneUnlisted(cmd); cset(cmd2, autoGenerated := true); for (S field : cmd.translatableFields()) cset(cmd2, field, fixAOrAn(replacePhrases(map, getString(cmd, field)))); if (anyFieldsDifferent(cmd, cmd2, cmd.translatableFields())) registerConcept(cmd2); } consistencyCheck(); } // API // for initial fill of translation table. probably doesn't catch many patterns usually void copyCommandsFromBackend() { for (S cmd : allIfQuotedPatterns(loadSnippet(beforeSlash(backendModuleLibID)))) uniqConcept(+cmd); } S answer(S s) { S s1 = s; s = replacePhrases(fieldToFieldIndexCI('in, 'out, list(Mishearing)), s); if (neq(s, s1)) print("Corrected to: " + s); if (currentAttractor != null) { print("Sending to attractor " + currentAttractor + ": " + s); S a = strOrNull(call(currentAttractor, 'answer, s)); if (a != null) { print("Attractor said: " + a); ret a; } } L cmds = filter(conceptsSortedByField Cmd('index), c -> nempty(c.patterns) && checkCondition(c)); Cmd cmd = mmo_matchMultiWithTypos(1, cmds, c -> c.parsedPattern(), s); if (cmd != null) ret handleCommand(cmd); ret sendToBackend(s); } bool checkCondition(Cmd cmd) { if (match("have scenario", cmd.conditions)) ret print("have scenario", dm_call(backend(), 'selectedScenario)) != null; true; } }