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: | 541 / 950 |
| Version history: | 39 change(s) |
| Referenced in: | [show references] |