!7 !include once #1027578 // Named // Note: db_mainConcepts() doesn't work (because of cases), always use cc! concept BotName > Named {} // e.g. for finding the module in Cruddie concept Cmd > ConceptWithGlobalID { 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"); } sS importableFields() { ret "cmd conditions exampleInputs globalID index patterns 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 backendModuleID; switchable S baseThingName = "$thing"; O currentAttractor; transient CRUD replacementsCRUD, mishearingsCRUD; 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"); } } void start { cc = dm_handleCaseIDField(); super.start(); dm_watchFieldAndNow backendModuleID(r updateModuleName); crud.multiLineFields = litset("exampleInputs", "questionsForArguments"); crud.sorter = func(Cl l) -> L { sortByField index(l) }; crud.formFixer = 48; replacementsCRUD = new CRUD(cc, Replacement); mishearingsCRUD = new CRUD(cc, Mishearing); dm_vmBus_onMessage_q objectTypesChanged((module, names) -> { if (dm_isSame(module, backend())) importReplacements(); }); } void importReplacements() { LS names = cast dm_call(backend(), 'objectTypes); print("Got names: " + names); if (names != null) { deleteConcepts(cc, Replacement); for (S name : names) if (neqic(name, baseThingName)) cnew(cc, Replacement, in := baseThingName, out := name); } applyReplacements(); } 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 backendModuleID; } visual { JComponent mainCRUD = super.visualize(); addComponents(crud.buttons, jbutton("Talk to me", rThread talkToMe), jPopDownButton_noText( "Apply replacements", rThread applyReplacements, "Import replacements", rThread importReplacements, "Consistency check", rThread consistencyCheck, "Import commands from snippet...", rThread importCmdsFromSnippet)); 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(cc, Cmd, 'autoGenerated, 'index)) cset(cmd, index := index++); } void applyReplacements { deleteConceptsWhere(cc, 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, globalID := aGlobalIDObject()); for (S field : cmd.translatableFields()) cset(cmd2, field, fixAOrAn(replacePhrases(map, getString(cmd, field)))); if (anyFieldsDifferent(cmd, cmd2, cmd.translatableFields())) registerConcept(cc, cmd2); } consistencyCheck(); } // API // for initial fill of translation table. probably doesn't catch many patterns usually void copyCommandsFromBackend() { for (S cmd : allIfQuotedPatterns(loadSnippet(beforeSlash(dm_moduleLibID(backend()))))) 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(cc, 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) { true; } // e.g. from snippet #1027616 void importCmds(S src) { L l = dynShortNamed Cmd(safeUnstructList(src)); int imported = 0; for (O o : l) { S id = getString globalID(o); if (empty(id)) continue; ++imported; GlobalID globalID = GlobalID(id); Cmd cmd = uniq_sync(cc, Cmd, +globalID); for (S field : splitAtSpace(Cmd.importableFields())) cSmartSet(cmd, field, getOpt(o, field)); } topLeftInfoBox("Imported/updated " + nEntries(imported)); if (imported != 0) importReplacements(); } void importCmdsFromSnippet(S snippetID) { print("Importing cmds from " + snippetID); importCmds(loadSnippet(snippetID)); } // import if we have no cmds yet void importCmdsFromSnippetIfEmpty(S snippetID) { if (conceptCount() == 0) importCmdsFromSnippet(snippetID); } void importCmdsFromSnippet enter { selectSnippetID("Commands to import", vf importCmdsFromSnippet); } void connectToBackend(S backendModuleID) { setField(+backendModuleID); } void setBotName(S name) { cset(uniq(cc, BotName), +name); updateModuleName(); } // may return null or empty string S botName() { ret getString name(conceptWhere(cc, BotName)); } void updateModuleName enter { S name = botName(); if (empty(name)) name = dm_moduleName(backend()); dm_setModuleName("Frontend for " + name); } }