Uses 911K of libraries. Click here for Pure Java version (23262L/132K).
1 | !7 |
2 | |
3 | cmodule TomiiBoiAnswers > DynPrintLogAndEnabled {
|
4 | switchable int port = 8080; |
5 | switchable S frontendName = "tomiiBoiDiscordBot"; |
6 | |
7 | transient CRUD<Category> categoriesCRUD; |
8 | transient CRUD<Server> serversCRUD; |
9 | transient CRUD<Channel> channelsCRUD; |
10 | transient CRUD<ServerToCategory> serverToCategoryCRUD; |
11 | |
12 | start {
|
13 | init(); |
14 | categoriesCRUD = new CRUD(Category); |
15 | serversCRUD = new CRUD(Server); |
16 | serverToCategoryCRUD = new CRUD(ServerToCategory); |
17 | channelsCRUD = new CRUD(Channel); |
18 | |
19 | thread {
|
20 | dm_serveHttpFromFunction(port, lambda2 html); |
21 | print("Admin live at: http://localhost:" + port);
|
22 | dm_registerAs('tomiiBoiQA);
|
23 | } |
24 | } |
25 | |
26 | visualize {
|
27 | JComponent c = super.visualize(); |
28 | addComponents(buttons, |
29 | jbutton("Open admin in browser", rThread { openURLInBrowser("http://localhost:" + port) }),
|
30 | jPopDownButton_noText( |
31 | "Import data...", rThreadEnter importData)); |
32 | c = jtabs( |
33 | "Main", c, |
34 | "Categories", categoriesCRUD.visualize(), |
35 | "Servers", serversCRUD.visualize(), |
36 | "Server-to-category", serverToCategoryCRUD.visualize(), |
37 | "Channels", channelsCRUD.visualize()); |
38 | channelsCRUD.addButton("Update list", rThreadEnter grabChannels);
|
39 | ret c; |
40 | } |
41 | |
42 | void importData {
|
43 | selectFile("Tomii Brain File", voidfunc(File f) enter {
|
44 | replaceConceptsWithTextFileOnNextStart(f); |
45 | dm_reloadModule(); |
46 | }); |
47 | } |
48 | |
49 | void grabChannels {
|
50 | dm_call(frontendName, 'grabChannels); |
51 | } |
52 | |
53 | // API |
54 | |
55 | Server addServer(S serverID, S name) {
|
56 | ret csetAndReturn(uniq Server(+serverID), +name); |
57 | } |
58 | |
59 | Channel addChannel(Server server, S channelID, S name) {
|
60 | ret csetAndReturn(uniq Channel(+server, +channelID), +name); |
61 | } |
62 | |
63 | Channel channelForID(S channelID) {
|
64 | ret conceptWhere Channel(+channelID); |
65 | } |
66 | |
67 | Cl<S> categoriesForServer(Server server) {
|
68 | Set<Category> set = asSet(conceptsWhere Category(onByDefault := true)); |
69 | for (ServerToCategory link : conceptsWhere(ServerToCategory, +server)) |
70 | addOrRemove(set, link.category, link.enabled); |
71 | ret collectAsSet name(set); |
72 | } |
73 | } |
74 | |
75 | //sS mainBotID = #1026411; |
76 | static int maxTypos = 1; |
77 | |
78 | sS adminTitle = "Tomii Boi Chat Bot Admin"; |
79 | sS shortLink; // catchy link to admin if available |
80 | |
81 | sS embedderLink; // where chat bot will go |
82 | sS goDaddyEmbedCode; |
83 | |
84 | static Cache<Scorer<ConsistencyError>> consistencyCheckResult = new(lambda0 consistencyCheck); |
85 | |
86 | concept Server {
|
87 | S serverID, name; |
88 | |
89 | toString { ret name; }
|
90 | } |
91 | |
92 | concept ServerToCategory {
|
93 | Server server; |
94 | Category category; |
95 | bool enabled; |
96 | |
97 | sS _fieldOrder = "server category enabled"; |
98 | } |
99 | |
100 | concept Channel {
|
101 | Server server; |
102 | S channelID; |
103 | S name; |
104 | bool botEnabled = true; |
105 | |
106 | toString { ret name; }
|
107 | sS _fieldOrder = "botEnabled name server"; |
108 | } |
109 | |
110 | concept Category {
|
111 | int index; |
112 | S name; |
113 | bool onByDefault = true; |
114 | |
115 | toString { ret name; }
|
116 | } |
117 | |
118 | concept Language {
|
119 | int index; |
120 | S code; |
121 | } |
122 | |
123 | concept QA {
|
124 | int index; |
125 | S language; |
126 | S questions; // line by line |
127 | S patterns; // to be determined |
128 | S answers; // one answer, or "random { answers separated by empty lines }"
|
129 | S category; // "business", "smalltalk" |
130 | |
131 | transient MMOPattern parsedPattern; |
132 | |
133 | sS _fieldOrder = "language category index questions patterns answers"; |
134 | |
135 | void change {
|
136 | parsedPattern = null; |
137 | super.change(); |
138 | } |
139 | |
140 | MMOPattern parsedPattern() {
|
141 | if (parsedPattern == null) |
142 | parsedPattern = mmo_parsePattern(patterns); |
143 | ret parsedPattern; |
144 | } |
145 | } |
146 | |
147 | concept Defaults {
|
148 | S lastLanguage; |
149 | } |
150 | |
151 | concept Settings {
|
152 | S contactFormViberID; |
153 | bool botOn, botAutoOpen; |
154 | } |
155 | |
156 | svoid init {
|
157 | processConceptsOverwriteFile(); |
158 | //print("fieldOrder", getFieldOrder(QA));
|
159 | dbIndexing(Language, 'index, Category, 'index, Channel, 'channelID); |
160 | print("QA count: " + countConcepts(QA));
|
161 | indexSingletonConcept(Defaults); |
162 | indexSingletonConcept(Settings); |
163 | |
164 | // languages, default language |
165 | cset(conceptsWhere QA(language := null), language := 'en); |
166 | uniq(Language, code := 'en); |
167 | removeConceptsWhere(Language, code := 'de); |
168 | |
169 | // categories, default category |
170 | int i = 0; |
171 | for (S category : ll("business", "smalltalk", "btd"))
|
172 | cset(uniq(Category, name := category), index := i++); |
173 | cset(conceptsWhere QA(category := null), category := "business"); |
174 | |
175 | onConceptsChange(r { consistencyCheckResult.clear(); });
|
176 | reindexQAs(); |
177 | } |
178 | |
179 | html { try {
|
180 | print(+uri); |
181 | if (swic(addSlash(uri), "/answer/")) {
|
182 | temp tempSetTL(opt_noDefault, valueIs1 noDefault(params)); |
183 | ret serveText(unnull(answer(params.get("q"), "en")));
|
184 | } |
185 | |
186 | // force auth |
187 | /*try answer callHtmlMethod(getBot(mainBotID), "/auth-only", mapPlus( |
188 | params, uri := rawLink(uri)));*/ |
189 | |
190 | if (eq(uri, "/download")) |
191 | ret serveText(conceptsStructure()); |
192 | |
193 | HCRUD_Concepts<QA> data = new HCRUD_Concepts<QA>(QA) {
|
194 | S itemName() { ret "question"; }
|
195 | |
196 | S fieldHelp(S field) {
|
197 | if (eq(field, "index")) |
198 | ret "lower index = higher matching precedence"; |
199 | if (eq(field, "patterns")) |
200 | ret [[Phrases to match. Use "+" as "and" operator, commas as "or" operator. Round brackets for grouping. Quotes for special characters. Put exclamation mark after phrase to disable typo correction.]]; |
201 | if (eq(field, "answers")) |
202 | ret "Text of answer. Use random { ... } for multiple answers (separated from each other by an empty line)";
|
203 | null; |
204 | } |
205 | |
206 | Renderer getRenderer(S field) {
|
207 | if (eqOneOf(field, 'questions, 'answers)) |
208 | ret new TextArea(80, 10); |
209 | if (eq(field, 'patterns)) |
210 | ret new TextField(80); |
211 | if (eq(field, 'language)) |
212 | ret new ComboBox(collect code(conceptsSortedByField(Language, 'index))); |
213 | if (eq(field, 'category)) |
214 | ret new ComboBox(collect name(conceptsSortedByField(Category, 'index))); |
215 | ret super.getRenderer(field); |
216 | } |
217 | |
218 | // sort by language first, then by priority (category + index) |
219 | L<QA> defaultSort(L<QA> l) {
|
220 | ret sortByFieldDesc language(sortQAs(l)); |
221 | } |
222 | |
223 | Map<S, O> emptyObject() {
|
224 | ret mapPlus(super.emptyObject(), language := uniq(Defaults).lastLanguage); |
225 | } |
226 | |
227 | O createObject(SS map) {
|
228 | S language = cast map.get('language);
|
229 | if (language != null) cset(uniq(Defaults), lastLanguage := language); |
230 | ret super.createObject(map); |
231 | } |
232 | }; |
233 | data.onCreateOrUpdate.add(qa -> reindexQAs()); |
234 | |
235 | HCRUD crud = new(rawLink(), data) {
|
236 | S frame(S title, S contents) {
|
237 | title = ahref(or2(shortLink, rawLink("")), adminTitle) + " | " + title;
|
238 | ret hhtml(hhead_title_decode(title) |
239 | + hbody(h2(title) |
240 | + p(joinWithVBar( |
241 | targetBlank(rawLink("download"), "export brain"),
|
242 | )) |
243 | + contents)); |
244 | } |
245 | |
246 | S renderTable(bool withCmds) {
|
247 | Scorer<ConsistencyError> scorer = consistencyCheckResult!; |
248 | ret (empty(scorer.errors) |
249 | ? p("Pattern analysis: No problems found. " + nEntries(countConcepts(QA)) + ".")
|
250 | : joinMap(scorer.errors, lambda1 renderErrorAsParagraph)) |
251 | + super.renderTable(withCmds); |
252 | } |
253 | |
254 | S renderErrorAsParagraph(ConsistencyError error) {
|
255 | S fix = error.renderFix(); |
256 | ret p("Error: " + htmlEncode2(error.msg) + " " +
|
257 | joinMap(error.items, qa -> ahref(editLink(qa.id), "[item]")) |
258 | + (empty(fix) ? "" : " " + fix)); |
259 | } |
260 | }; |
261 | |
262 | // actions |
263 | |
264 | if (eqGet(params, action := 'reindex)) {
|
265 | long id = parseLong(params.get('id));
|
266 | int newIndex = parseInt(params.get('newIndex));
|
267 | QA qa = getConcept(QA, id); |
268 | if (qa == null) ret crud.refreshWithMsgs("Item not found");
|
269 | cset(qa, index := newIndex); |
270 | reindexQAs(); |
271 | ret crud.refreshWithMsgs("Index of item " + qa.id + " changed");
|
272 | } |
273 | |
274 | if (eqGet(params, botOn := "1")) {
|
275 | cset(uniq(Settings), botOn := true); |
276 | ret crud.refreshWithMsgs("Bot turned on!");
|
277 | } |
278 | |
279 | if (eqGet(params, botOff := "1")) {
|
280 | cset(uniq(Settings), botOn := false); |
281 | ret crud.refreshWithMsgs("Bot turned off!");
|
282 | } |
283 | |
284 | if (nemptyGet botAutoOpen(params)) {
|
285 | cset(uniq(Settings), botAutoOpen := eq("1", params.get("botAutoOpen")));
|
286 | ret crud.refreshWithMsgs("Bot auto-open turned " + onOff(botAutoOpen()) + "!");
|
287 | } |
288 | |
289 | ret crud.renderPage(params); |
290 | } catch print e {
|
291 | ret "ERROR."; |
292 | } |
293 | } |
294 | |
295 | static new ThreadLocal<S> language_out; |
296 | static new ThreadLocal<Bool> opt_noDefault; // true = return null instead of #default text |
297 | |
298 | static new ThreadLocal<Cl<S>> categoriesForRequest; |
299 | |
300 | // main API function for other bots |
301 | sS answer(S s, S language) {
|
302 | language_out.set(null); |
303 | ret trim(answer_main(s, language)); |
304 | } |
305 | |
306 | sS answer_main(S s, S language) {
|
307 | S raw = findRawAnswer(s, language, true); |
308 | S contents = extractKeywordPlusBracketed_keepComments("random", raw);
|
309 | if (contents != null) |
310 | ret random(splitAtEmptyLines(contents)); |
311 | ret raw; |
312 | } |
313 | |
314 | sS findRawAnswer(S s, S language, bool allowTypos) {
|
315 | ret selectQA(findMatchingQA(s, language, allowTypos)); |
316 | } |
317 | |
318 | static QA findQAWithTypos(S s, S language) {
|
319 | Lowest<QA> qa = findClosestQA(s, language); |
320 | if (!qa.has()) null; |
321 | if (qa.score() > maxTypos) {
|
322 | print("Rejecting " + nTypos((int) qa.score()) + ": " + s + " / " + qa->patterns);
|
323 | null; |
324 | } |
325 | print("Accepting " + nTypos((int) qa.score()) + ": " + s + " / " + qa->patterns);
|
326 | ret qa!; |
327 | } |
328 | |
329 | static Cl<QA> allQAsForRequest(S language) {
|
330 | Cl<QA> l = conceptsWhere(QA, +language); |
331 | if (categoriesForRequest! != null) |
332 | l = filter(l, qa -> contains(categoriesForRequest!, qa.category)); |
333 | ret l; |
334 | } |
335 | |
336 | static QA findMatchingQA(S s, S language, bool allowTypos) {
|
337 | try object QA qa = findMatchingQA(s, allQAsForRequest(language)); |
338 | try object QA qa = findMatchingQA(s, filter(list(QA), q -> neq(q.language, language))); // TODO: categoriesForRequest |
339 | if (allowTypos) |
340 | try object QA qa = findQAWithTypos(s, language); |
341 | if (!eq(s, "#default") && !isTrue(opt_noDefault!)) ret findMatchingQA("#default", language, false);
|
342 | null; |
343 | } |
344 | |
345 | static Lowest<QA> findClosestQA(S s, S language) {
|
346 | time "findClosestQA" {
|
347 | new Lowest<QA> qa; |
348 | findClosestQA(s, qa, allQAsForRequest(language)); |
349 | findClosestQA(s, qa, filter(list(QA), q -> neq(q.language, language))); |
350 | } |
351 | ret qa; |
352 | } |
353 | |
354 | static L<QA> sortQAs(Cl<QA> qas) {
|
355 | Map<S, Int> categoryToIndex = fieldToFieldIndex('name, 'index, list(Category));
|
356 | ret sortedByCalculatedField(qas, q -> pair(categoryToIndex.get(q.category), q.index)); |
357 | } |
358 | |
359 | svoid reindexQAs() {
|
360 | for (Language lang) {
|
361 | int index = 1; |
362 | for (QA qa : sortQAs(conceptsWhere(QA, language := lang.code))) |
363 | cset(qa, index := index++); |
364 | } |
365 | } |
366 | |
367 | static QA findMatchingQA(S s, Cl<QA> qas) {
|
368 | for (QA qa : sortQAs(qas)) |
369 | if (mmo_match_parsedPattern(qa.parsedPattern(), s)) |
370 | ret qa; |
371 | null; |
372 | } |
373 | |
374 | svoid findClosestQA(S s, Lowest<QA> best, Cl<QA> qas) {
|
375 | for (QA qa : sortQAs(qas)) {
|
376 | Int score = mmo_levenWithSwapsScore_parsedPattern(qa.parsedPattern(), s); |
377 | if (score != null) |
378 | best.put(qa, score); |
379 | } |
380 | } |
381 | |
382 | // returns answers |
383 | sS selectQA(QA qa) {
|
384 | if (qa == null) null; |
385 | language_out.set(qa.language); |
386 | ret qa.answers; |
387 | } |
388 | |
389 | sclass ConsistencyError {
|
390 | S msg; |
391 | L<QA> items; |
392 | |
393 | *(S *msg, QA... items) { this.items = asList(items); }
|
394 | |
395 | S renderFix() { null; }
|
396 | } |
397 | |
398 | svoid checkLocalConsistency(QA qa, Scorer<ConsistencyError> scorer) {
|
399 | LS questions = tlft(qa.questions); |
400 | for (S q : questions) |
401 | if (mmo_match_parsedPattern(qa.parsedPattern(), q)) |
402 | scorer.ok(); |
403 | else |
404 | scorer.error(ConsistencyError("Question " + quote(q) + " not matched by patterns " + quote(qa.patterns), qa));
|
405 | } |
406 | |
407 | svoid checkGlobalConsistency(QA qa, Scorer<ConsistencyError> scorer) {
|
408 | for (S q : tlft(qa.questions)) {
|
409 | QA found = findMatchingQA(q, qa.language, false); |
410 | if (found != null && found != qa) |
411 | scorer.error(new ConsistencyError("Question " + quote(q) + " (item " + qa.id + ") shadowed by patterns " + quote(found.patterns) + " (item " + found.id + ")", qa, found) {
|
412 | S renderFix() {
|
413 | ret ahref(rawLink("?action=reindex&id=" + qa.id + "&newIndex=" + (found.index-1)), "[fix by changing index]");
|
414 | } |
415 | }); |
416 | else |
417 | scorer.ok(); |
418 | } |
419 | } |
420 | |
421 | static Scorer<ConsistencyError> consistencyCheck() {
|
422 | new Scorer<ConsistencyError> scorer; |
423 | scorer.collectErrors(); |
424 | for (QA qa) {
|
425 | checkLocalConsistency(qa, scorer); |
426 | checkGlobalConsistency(qa, scorer); |
427 | } |
428 | ret scorer; |
429 | } |
430 | |
431 | sS contactFormViberID() {
|
432 | ret uniq(Settings).contactFormViberID; |
433 | } |
434 | |
435 | // API |
436 | |
437 | sbool botOn() {
|
438 | ret uniq(Settings).botOn; |
439 | } |
440 | |
441 | sbool botAutoOpen() {
|
442 | ret uniq(Settings).botAutoOpen; |
443 | } |
444 | |
445 | svoid importQA(virtual QA qa_external) {
|
446 | QA qa = shallowCloneToUnlistedConcept QA(qa_external); |
447 | uniq QA(allConceptFieldsAsParams(qa)); |
448 | } |
449 | |
450 | sS rawLink() { ret "/"; }
|
451 | sS rawLink(S uri) { ret addSlashPrefix(uri) ; }
|
Began life as a copy of #1028036
download show line numbers debug dex old transpilations
Travelled to 7 computer(s): bhatertpkbcr, mqqgnosmbjvj, pyentgdyhuwx, pzhvpgtvlbxg, tvejysmllsmz, vouqrxazstgt, xrpafgyirdlv
No comments. add comment
| Snippet ID: | #1028043 |
| Snippet name: | Tomii Boi Answers DB Module backup 1 |
| Eternal ID of this version: | #1028043/1 |
| Text MD5: | 3657674ade2740a4949191d36ba315a3 |
| Transpilation MD5: | 83ca3d369f461164cf8444030974b9c5 |
| Author: | stefan |
| Category: | javax / html |
| Type: | JavaX source code (Dynamic Module) |
| Public (visible to everyone): | Yes |
| Archived (hidden from active list): | No |
| Created/modified: | 2020-05-07 19:51:26 |
| Source code size: | 13126 bytes / 451 lines |
| Pitched / IR pitched: | No / No |
| Views / Downloads: | 382 / 481 |
| Referenced in: | [show references] |