Transpiled version (22474L) is out of date.
1 | do not include class And. |
2 | set flag OurSyncCollections. |
3 | |
4 | sS mainBotID; |
5 | static int maxTypos = 1; |
6 | |
7 | sS adminTitle = "Chat Bot Admin"; |
8 | sS shortLink; // catchy link to admin if available |
9 | sbool enableWorkerChat; |
10 | |
11 | sS embedderLink; // where chat bot will go |
12 | sS goDaddyEmbedCode; |
13 | |
14 | static Cache<Scorer<ConsistencyError>> consistencyCheckResult = new(lambda0 consistencyCheck); |
15 | |
16 | concept Category { |
17 | int index; |
18 | S name; |
19 | } |
20 | |
21 | concept Language { |
22 | int index; |
23 | S code; |
24 | } |
25 | |
26 | concept QA { |
27 | int index; |
28 | S language; |
29 | S questions; // line by line |
30 | S patterns; // to be determined |
31 | S answers; // one answer, or "random { answers separated by empty lines }" |
32 | S category; // "business", "smalltalk" |
33 | |
34 | transient MMOPattern parsedPattern; |
35 | |
36 | sS _fieldOrder = "language category index questions patterns answers"; |
37 | |
38 | void change { |
39 | parsedPattern = null; |
40 | super.change(); |
41 | } |
42 | |
43 | MMOPattern parsedPattern() { |
44 | if (parsedPattern == null) |
45 | parsedPattern = mmo2_parsePattern(patterns); |
46 | ret parsedPattern; |
47 | } |
48 | } |
49 | |
50 | concept Defaults { |
51 | S lastLanguage; |
52 | } |
53 | |
54 | concept Settings { |
55 | bool botOn, botAutoOpen; |
56 | } |
57 | |
58 | svoid pQADB { |
59 | print("fieldOrder", getFieldOrder(QA)); |
60 | dbIndexing(Language, 'index, Category, 'index); |
61 | indexSingletonConcept(Defaults); |
62 | indexSingletonConcept(Settings); |
63 | |
64 | // languages, default language |
65 | cset(conceptsWhere QA(language := null), language := 'en); |
66 | uniq(Language, code := 'en); |
67 | removeConceptsWhere(Language, code := 'de); |
68 | |
69 | // categories, default category |
70 | int i = 0; |
71 | for (S category : ll("business", "smalltalk")) |
72 | cset(uniq(Category, name := category), index := i++); |
73 | cset(conceptsWhere QA(category := null), category := "business"); |
74 | |
75 | onConceptsChange(r { consistencyCheckResult.clear(); }); |
76 | reindexQAs(); |
77 | } |
78 | |
79 | html { try { |
80 | if (swic(addSlash(uri), "/answer/")) { |
81 | temp tempSetTL(opt_noDefault, valueIs1 noDefault(params)); |
82 | ret serveText(unnull(answer(params.get("q"), "en"))); |
83 | } |
84 | |
85 | // force auth |
86 | try answer (S) callHtmlMethod2(mainBot(), "/auth-only", mapPlus( |
87 | params, uri := rawLink(uri))); |
88 | |
89 | if (eq(uri, "/demo")) |
90 | ret hrefresh(appendQueryToURL(rawBotLink(mainBotID), _botDemo := 1, bot := 1)); |
91 | |
92 | if (eq(uri, "/download")) |
93 | ret subBot_serveText(conceptsStructure()); |
94 | |
95 | if (eq(uri, "/embedCode")) |
96 | ret hsansserif() + htitle_h2("Chat bot embed code") |
97 | + (empty(goDaddyEmbedCode) ? "" : |
98 | h3("GoDaddy Site Builder") |
99 | |
100 | + p("Add an HTML box with this code:") |
101 | |
102 | + pre(htmlEncode2(goDaddyEmbedCode)) |
103 | |
104 | + h3("Other")) |
105 | |
106 | + p("Add this code in front of your " + tt(htmlEncode2("</body>")) + " tag:") |
107 | |
108 | + pre(htmlEncode2(hjavascript_src_withType(rawBotLink(mainBotID), defer := html_valueLessParam()))); |
109 | |
110 | if (eq(uri, "/dbUploads/import")) { |
111 | long id = parseLong(params.get("id")); |
112 | DBUpload dbUpload = getConcept DBUpload(id); |
113 | if (dbUpload == null) ret "Not found"; |
114 | |
115 | Map<Long, O> qaMap = cast safeUnstructure(dbUpload.text); |
116 | Map<S, QA> existingByPattern = indexByFieldCI patterns(list(QA)); |
117 | new LS msgs; |
118 | msgs.add("Importing..."); |
119 | |
120 | for (S _qaID : startingWith_dropPrefix("qa_", keys(params))) { |
121 | long qaID = parseLong(_qaID); |
122 | virtual QA qa = qaMap.get(qaID); |
123 | if (qa == null) continue; |
124 | |
125 | S patterns = getString patterns(qa), answers = getString answers(qa); |
126 | QA existing = existingByPattern.get(patterns); |
127 | if (existing != null) { |
128 | msgs.add("QA item exists for patterns: " + patterns); |
129 | msgs.add("Answers are " + (eq(answers, existing.answers) ? "identical" : "different")); |
130 | } else { |
131 | QA imported = cnew QA(); |
132 | copyFields(qa, imported, "index", "language", "questions", "patterns", "answers", "category"); |
133 | msgs.add("Imported item " + qaID + " as " + imported.id + " (patterns: " + patterns + ")"); |
134 | } |
135 | } |
136 | |
137 | ret htmlEncode2_nlToBr(lines(msgs)); |
138 | } |
139 | |
140 | if (eq(uri, "/dbUploads/view")) { |
141 | long id = parseLong(params.get("id")); |
142 | DBUpload dbUpload = getConcept DBUpload(id); |
143 | |
144 | S contents; |
145 | if (dbUpload == null) |
146 | contents = "Not found"; |
147 | else { |
148 | new L<Map> data; |
149 | Map<Long, O> qaMap = cast safeUnstructure(dbUpload.text); |
150 | |
151 | // filter and sort |
152 | qaMap = filterByValuePredicate(qaMap, c -> dynShortNameIs QA(c)); |
153 | qaMap = mapSortedByFunctionOnValue(qaMap, c -> neqic(getString category(c), "smalltalk")); |
154 | |
155 | for (long qaID, O c : qaMap) { |
156 | S category = getString category(c); |
157 | data.add(litorderedmap( |
158 | "ID" := qaID, |
159 | "Select" := hcheckbox("qa_" + qaID, eqic(category, "smalltalk")), |
160 | "Category" := htmlEncode2(category), |
161 | "Questions" := htmlEncode2(getString questions(c)), |
162 | "Patterns" := htmlEncode2(getString patterns(c)), |
163 | "Answers" := htmlEncode2(getString answers(c)), |
164 | )); |
165 | } |
166 | contents = hpostform( |
167 | hhidden(+id) |
168 | + p(hsubmit("Import selected questions")) |
169 | + htmlTable2_noHtmlEncode(data), |
170 | action := rawLink("dbUploads/import") |
171 | ); |
172 | } |
173 | |
174 | S title = "Import questions"; |
175 | ret hhtml(hhead_title_decode(title) |
176 | + hbody(h2(title) |
177 | + p(makeNav()) |
178 | + contents)); |
179 | |
180 | } |
181 | |
182 | if (eq(uri, "/dbUploads")) { |
183 | HCRUD_Concepts<DBUpload> data = new HCRUD_Concepts<DBUpload>(DBUpload) { |
184 | Renderer getRenderer(S field) { |
185 | if (eq(field, "text")) |
186 | ret new TextArea(80, 20); |
187 | ret super.getRenderer(field); |
188 | } |
189 | }; |
190 | |
191 | HCRUD crud = new(rawLink("dbUploads"), data) { |
192 | S frame(S title, S contents) { |
193 | title = ahref(or2(shortLink, rawLink("")), adminTitle) + " | " + title; |
194 | ret hhtml(hhead_title_decode(title) |
195 | + hbody(h2(title) |
196 | + p(makeNav()) |
197 | + contents)); |
198 | } |
199 | |
200 | S renderCmds(MapSO item) { |
201 | O id = item.get(data.idField()); |
202 | ret joinNemptiesWithVBar(super.renderCmds(item), ahref(baseLink + "/view?id=" + urlencode(str(id)), "View questions")); |
203 | } |
204 | }; |
205 | |
206 | crud.tableClass = "responstable"; |
207 | ret hsansserif() + hcss_responstable() + crud.renderPage(params); |
208 | } |
209 | |
210 | // make QA CRUD_ |
211 | |
212 | HCRUD_Concepts<QA> data = new HCRUD_Concepts<QA>(QA) { |
213 | S itemName() { ret "question"; } |
214 | |
215 | S fieldHelp(S field) { |
216 | if (eq(field, "category")) |
217 | ret "smalltalk has a lower precedence than business"; |
218 | if (eq(field, "index")) |
219 | ret "lower index = higher matching precedence"; |
220 | if (eq(field, "patterns")) |
221 | 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.]]; |
222 | if (eq(field, "answers")) |
223 | ret "Text of answer. Use random { ... } for multiple answers (separated from each other by an empty line)"; |
224 | null; |
225 | } |
226 | |
227 | Renderer getRenderer(S field) { |
228 | if (eqOneOf(field, 'questions, 'answers)) |
229 | ret new TextArea(80, 10); |
230 | if (eq(field, 'patterns)) |
231 | ret new TextField(80); |
232 | if (eq(field, 'language)) |
233 | ret new ComboBox(collect code(conceptsSortedByField(Language, 'index))); |
234 | if (eq(field, 'category)) |
235 | ret new ComboBox(collect name(conceptsSortedByField(Category, 'index))); |
236 | ret super.getRenderer(field); |
237 | } |
238 | |
239 | // sort by language first, then by priority (category + index) |
240 | L<QA> defaultSort(L<QA> l) { |
241 | ret sortByFieldDesc language(sortQAs(l)); |
242 | } |
243 | |
244 | Map<S, O> emptyObject() { |
245 | ret mapPlus(super.emptyObject(), language := uniq(Defaults).lastLanguage); |
246 | } |
247 | |
248 | O createObject(SS map, S fieldPrefix) { |
249 | S language = cast map.get('language); |
250 | if (language != null) cset(uniq(Defaults), lastLanguage := language); |
251 | ret super.createObject(map, fieldPrefix); |
252 | } |
253 | }; |
254 | data.onCreateOrUpdate.add(qa -> reindexQAs()); |
255 | |
256 | HCRUD crud = new(rawLink(), data) { |
257 | S frame(S title, S contents) { |
258 | S stats = (S) pcallOpt(mainBot(), 'dbStats); |
259 | title = ahref(or2(shortLink, rawLink("")), adminTitle) + " | " + title; |
260 | ret hhtml(hhead_title_decode(title) |
261 | + hbody(h2(title) |
262 | + pIfNempty(stats) |
263 | + p(makeNav()) |
264 | + contents)); |
265 | } |
266 | |
267 | S renderTable(bool withCmds) { |
268 | Scorer<ConsistencyError> scorer = consistencyCheckResult!; |
269 | ret (empty(scorer.errors) |
270 | ? p("Pattern analysis: No problems found. " + nEntries(countConcepts(QA)) + ".") |
271 | : joinMap(scorer.errors, lambda1 renderErrorAsParagraph)) |
272 | + super.renderTable(withCmds); |
273 | } |
274 | |
275 | S renderErrorAsParagraph(ConsistencyError error) { |
276 | S fix = error.renderFix(); |
277 | ret p("Error: " + htmlEncode2(error.msg) + " " + |
278 | joinMap(error.items, qa -> ahref(editLink(qa.id), "[item]")) |
279 | + (empty(fix) ? "" : " " + fix)); |
280 | } |
281 | }; |
282 | |
283 | // actions |
284 | |
285 | if (eqGet(params, action := 'reindex)) { |
286 | long id = parseLong(params.get('id)); |
287 | int newIndex = parseInt(params.get('newIndex)); |
288 | QA qa = getConcept(QA, id); |
289 | if (qa == null) ret crud.refreshWithMsgs("Item not found"); |
290 | cset(qa, index := newIndex); |
291 | reindexQAs(); |
292 | ret crud.refreshWithMsgs("Index of item " + qa.id + " changed"); |
293 | } |
294 | |
295 | if (eqGet(params, botOn := "1")) { |
296 | cset(uniq(Settings), botOn := true); |
297 | ret crud.refreshWithMsgs("Bot turned on!"); |
298 | } |
299 | |
300 | if (eqGet(params, botOff := "1")) { |
301 | cset(uniq(Settings), botOn := false); |
302 | ret crud.refreshWithMsgs("Bot turned off!"); |
303 | } |
304 | |
305 | if (nemptyGet botAutoOpen(params)) { |
306 | cset(uniq(Settings), botAutoOpen := eq("1", params.get("botAutoOpen"))); |
307 | ret crud.refreshWithMsgs("Bot auto-open turned " + onOff(botAutoOpen()) + "!"); |
308 | } |
309 | |
310 | crud.tableClass = "responstable"; |
311 | ret hsansserif() + hcss_responstable() + crud.renderPage(params); |
312 | } catch print e { |
313 | ret "ERROR."; |
314 | } |
315 | } |
316 | |
317 | static new ThreadLocal<S> language_out; |
318 | static new ThreadLocal<Bool> opt_noDefault; // true = return null instead of #default text |
319 | |
320 | // main API function for other bots |
321 | sS answer(S s, S language) { |
322 | language_out.set(null); |
323 | ret trim(answer_main(s, language)); |
324 | } |
325 | |
326 | sS answer_main(S s, S language) { |
327 | S raw = findRawAnswer(s, language, true); |
328 | S contents = extractKeywordPlusBracketed_keepComments("random", raw); |
329 | if (contents != null) |
330 | ret random(splitAtEmptyLines(contents)); |
331 | ret raw; |
332 | } |
333 | |
334 | sS findRawAnswer(S s, S language, bool allowTypos) { |
335 | ret selectQA(findMatchingQA(s, language, allowTypos)); |
336 | } |
337 | |
338 | static QA findQAWithTypos(S s, S language) { |
339 | Lowest<QA> qa = findClosestQA(s, language); |
340 | if (!qa.has()) null; |
341 | if (qa.score() > maxTypos) { |
342 | print("Rejecting " + nTypos((int) qa.score()) + ": " + s + " / " + qa->patterns); |
343 | null; |
344 | } |
345 | print("Accepting " + nTypos((int) qa.score()) + ": " + s + " / " + qa->patterns); |
346 | ret qa!; |
347 | } |
348 | |
349 | static QA findMatchingQA(S s, S language, bool allowTypos) { |
350 | try object QA qa = findMatchingQA(s, conceptsWhere(QA, +language)); |
351 | try object QA qa = findMatchingQA(s, filter(list(QA), q -> neq(q.language, language))); |
352 | if (allowTypos) |
353 | try object QA qa = findQAWithTypos(s, language); |
354 | if (!eq(s, "#default") && !isTrue(opt_noDefault!)) ret findMatchingQA("#default", language, false); |
355 | null; |
356 | } |
357 | |
358 | static Lowest<QA> findClosestQA(S s, S language) { |
359 | time "findClosestQA" { |
360 | new Lowest<QA> qa; |
361 | findClosestQA(s, qa, conceptsWhere(QA, +language)); |
362 | findClosestQA(s, qa, filter(list(QA), q -> neq(q.language, language))); |
363 | } |
364 | ret qa; |
365 | } |
366 | |
367 | static L<QA> sortQAs(Cl<QA> qas) { |
368 | Map<S, Int> categoryToIndex = fieldToFieldIndex('name, 'index, list(Category)); |
369 | ret sortedByCalculatedField(qas, q -> pair(categoryToIndex.get(q.category), q.index)); |
370 | } |
371 | |
372 | svoid reindexQAs() { |
373 | for (Language lang) { |
374 | int index = 1; |
375 | for (QA qa : sortQAs(conceptsWhere(QA, language := lang.code))) |
376 | cset(qa, index := index++); |
377 | } |
378 | } |
379 | |
380 | static QA findMatchingQA(S s, Cl<QA> qas) { |
381 | for (QA qa : sortQAs(qas)) |
382 | if (mmo2_match(qa.parsedPattern(), s)) |
383 | ret qa; |
384 | null; |
385 | } |
386 | |
387 | svoid findClosestQA(S s, Lowest<QA> best, Cl<QA> qas) { |
388 | for (QA qa : sortQAs(qas)) { |
389 | Int score = mmo2_levenWithSwapsScore(qa.parsedPattern(), s); |
390 | if (score != null) |
391 | best.put(qa, score); |
392 | } |
393 | } |
394 | |
395 | // returns answers |
396 | sS selectQA(QA qa) { |
397 | if (qa == null) null; |
398 | language_out.set(qa.language); |
399 | ret qa.answers; |
400 | } |
401 | |
402 | sclass ConsistencyError { |
403 | S msg; |
404 | L<QA> items; |
405 | |
406 | *(S *msg, QA... items) { this.items = asList(items); } |
407 | |
408 | S renderFix() { null; } |
409 | } |
410 | |
411 | svoid checkLocalConsistency(QA qa, Scorer<ConsistencyError> scorer) { |
412 | LS questions = tlft(qa.questions); |
413 | for (S q : questions) |
414 | if (mmo2_match(qa.parsedPattern(), q)) |
415 | scorer.ok(); |
416 | else |
417 | scorer.error(ConsistencyError("Question " + quote(q) + " not matched by patterns " + quote(qa.patterns), qa)); |
418 | } |
419 | |
420 | svoid checkGlobalConsistency(QA qa, Scorer<ConsistencyError> scorer) { |
421 | for (S q : tlft(qa.questions)) { |
422 | QA found = findMatchingQA(q, qa.language, false); |
423 | if (found != null && found != qa) |
424 | scorer.error(new ConsistencyError("Question " + quote(q) + " (item " + qa.id + ") shadowed by patterns " + quote(found.patterns) + " (item " + found.id + ")", qa, found) { |
425 | S renderFix() { |
426 | ret ahref(rawLink("?action=reindex&id=" + qa.id + "&newIndex=" + (found.index-1)), "[fix by changing index]"); |
427 | } |
428 | }); |
429 | else |
430 | scorer.ok(); |
431 | } |
432 | } |
433 | |
434 | static Scorer<ConsistencyError> consistencyCheck() { |
435 | new Scorer<ConsistencyError> scorer; |
436 | scorer.collectErrors(); |
437 | for (QA qa) { |
438 | checkLocalConsistency(qa, scorer); |
439 | checkGlobalConsistency(qa, scorer); |
440 | } |
441 | ret scorer; |
442 | } |
443 | |
444 | // API |
445 | |
446 | sbool botOn() { |
447 | ret uniq(Settings).botOn; |
448 | } |
449 | |
450 | sbool botAutoOpen() { |
451 | ret uniq(Settings).botAutoOpen; |
452 | } |
453 | |
454 | svoid importQA(virtual QA qa_external) { |
455 | QA qa = shallowCloneToUnlistedConcept QA(qa_external); |
456 | uniq QA(allConceptFieldsAsParams(qa)); |
457 | } |
458 | |
459 | static swappable S makeNav() { |
460 | S s = joinWithVBar( |
461 | ahref("?logout=1", "log out"), |
462 | targetBlank(relativeRawBotLink(mainBotID, "/logs"), "chat logs"), |
463 | targetBlank(rawLink("demo?bot=1"), "demo"), |
464 | targetBlank(rawLink("download"), "export brain"), |
465 | ahref(rawLink("embedCode"), "show embed code"), |
466 | botOn() |
467 | ? "Bot is ON (appears on home page) " + ahrefWithConfirm("Switch bot off?", "?botOff=1", "[switch bot off]") |
468 | : "Bot is OFF (appears only with " + targetBlank(appendQueryToURL(embedderLink, bot := 1), "?bot=1") + ") " + ahrefWithConfirm("Switch bot on?", "?botOn=1", "[switch bot on]"), |
469 | ahref(rawLink("dbUploads"), "db uploads"), |
470 | ); |
471 | |
472 | if (enableWorkerChat) |
473 | s += |
474 | " | " + ahref(rawBotLink(mainBotID, "workers-admin"), "workers admin") |
475 | + " | " + targetBlank(rawBotLink(mainBotID, "worker"), "worker chat"); |
476 | |
477 | ret s; |
478 | } |
479 | |
480 | sO mainBot() { |
481 | ret getBot(mainBotID); |
482 | } |
483 | |
484 | concept DBUpload { |
485 | S name; |
486 | S text; |
487 | } |
Began life as a copy of #1026409
download show line numbers debug dex old transpilations
Travelled to 8 computer(s): bhatertpkbcr, mowyntqkapby, mqqgnosmbjvj, pyentgdyhuwx, pzhvpgtvlbxg, tvejysmllsmz, vouqrxazstgt, xrpafgyirdlv
No comments. add comment
Snippet ID: | #1028280 |
Snippet name: | Web Bot Answers DB Include [latest] |
Eternal ID of this version: | #1028280/40 |
Text MD5: | a00d89fa8f8f6375161ed2ac5fb343fa |
Author: | stefan |
Category: | javax / web chat bots |
Type: | JavaX fragment (include) |
Public (visible to everyone): | Yes |
Archived (hidden from active list): | No |
Created/modified: | 2022-05-26 13:59:47 |
Source code size: | 15282 bytes / 487 lines |
Pitched / IR pitched: | No / No |
Views / Downloads: | 277 / 644 |
Version history: | 39 change(s) |
Referenced in: | [show references] |