1 | sS mainBotID; |
2 | static int maxTypos = 1; |
3 | |
4 | sS adminTitle = "Chat Bot Admin"; |
5 | sS shortLink; // catchy link to admin if available |
6 | sbool enableWorkerChat; |
7 | |
8 | sS embedderLink; // where chat bot will go |
9 | sS goDaddyEmbedCode; |
10 | |
11 | static Cache<Scorer<ConsistencyError>> consistencyCheckResult = new(lambda0 consistencyCheck); |
12 | |
13 | concept Category { |
14 | int index; |
15 | S name; |
16 | } |
17 | |
18 | concept Language { |
19 | int index; |
20 | S code; |
21 | } |
22 | |
23 | concept QA { |
24 | int index; |
25 | S language; |
26 | S questions; // line by line |
27 | S patterns; // to be determined |
28 | S answers; // one answer, or "random { answers separated by empty lines }" |
29 | S category; // "business", "smalltalk" |
30 | |
31 | transient MMOPattern parsedPattern; |
32 | |
33 | sS _fieldOrder = "language category index questions patterns answers"; |
34 | |
35 | void change { |
36 | parsedPattern = null; |
37 | super.change(); |
38 | } |
39 | |
40 | MMOPattern parsedPattern() { |
41 | if (parsedPattern == null) |
42 | parsedPattern = mmo2_parsePattern(patterns); |
43 | ret parsedPattern; |
44 | } |
45 | } |
46 | |
47 | concept Defaults { |
48 | S lastLanguage; |
49 | } |
50 | |
51 | concept Settings { |
52 | S contactFormViberID; |
53 | bool botOn, botAutoOpen; |
54 | } |
55 | |
56 | svoid pQADB { |
57 | print("fieldOrder", getFieldOrder(QA)); |
58 | dbIndexing(Language, 'index, Category, 'index); |
59 | indexSingletonConcept(Defaults); |
60 | indexSingletonConcept(Settings); |
61 | |
62 | // languages, default language |
63 | cset(conceptsWhere QA(language := null), language := 'en); |
64 | uniq(Language, code := 'en); |
65 | removeConceptsWhere(Language, code := 'de); |
66 | |
67 | // categories, default category |
68 | int i = 0; |
69 | for (S category : ll("business", "smalltalk")) |
70 | cset(uniq(Category, name := category), index := i++); |
71 | cset(conceptsWhere QA(category := null), category := "business"); |
72 | |
73 | onConceptsChange(r { consistencyCheckResult.clear(); }); |
74 | reindexQAs(); |
75 | } |
76 | |
77 | html { try { |
78 | if (swic(addSlash(uri), "/answer/")) { |
79 | temp tempSetTL(opt_noDefault, valueIs1 noDefault(params)); |
80 | ret serveText(unnull(answer(params.get("q"), "en"))); |
81 | } |
82 | |
83 | // force auth |
84 | try answer callHtmlMethod(getBot(mainBotID), "/auth-only", mapPlus( |
85 | params, uri := rawLink(uri))); |
86 | |
87 | if (eq(uri, "/demo")) |
88 | ret hrefresh(appendQueryToURL(rawBotLink(mainBotID), _botDemo := 1, bot := 1)); |
89 | |
90 | if (eq(uri, "/download")) |
91 | ret subBot_serveText(conceptsStructure()); |
92 | |
93 | if (eq(uri, "/embedCode")) |
94 | ret hsansserif() + htitle_h2("Chat bot embed code") |
95 | + (empty(goDaddyEmbedCode) ? "" : |
96 | h3("GoDaddy Site Builder") |
97 | |
98 | + p("Add an HTML box with this code:") |
99 | |
100 | + pre(htmlEncode2(goDaddyEmbedCode)) |
101 | |
102 | + h3("Other")) |
103 | |
104 | + p("Add this code in front of your " + tt(htmlEncode2("</body>")) + " tag:") |
105 | |
106 | + pre(htmlEncode2(hjavascript_src_withType(rawBotLink(mainBotID)))); |
107 | |
108 | HCRUD_Concepts<QA> data = new HCRUD_Concepts<QA>(QA) { |
109 | S itemName() { ret "question"; } |
110 | |
111 | S fieldHelp(S field) { |
112 | if (eq(field, "category")) |
113 | ret "smalltalk has a lower precedence than business"; |
114 | if (eq(field, "index")) |
115 | ret "lower index = higher matching precedence"; |
116 | if (eq(field, "patterns")) |
117 | 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.]]; |
118 | if (eq(field, "answers")) |
119 | ret "Text of answer. Use random { ... } for multiple answers (separated from each other by an empty line)"; |
120 | null; |
121 | } |
122 | |
123 | Renderer getRenderer(S field) { |
124 | if (eqOneOf(field, 'questions, 'answers)) |
125 | ret new TextArea(80, 10); |
126 | if (eq(field, 'patterns)) |
127 | ret new TextField(80); |
128 | if (eq(field, 'language)) |
129 | ret new ComboBox(collect code(conceptsSortedByField(Language, 'index))); |
130 | if (eq(field, 'category)) |
131 | ret new ComboBox(collect name(conceptsSortedByField(Category, 'index))); |
132 | ret super.getRenderer(field); |
133 | } |
134 | |
135 | // sort by language first, then by priority (category + index) |
136 | L<QA> defaultSort(L<QA> l) { |
137 | ret sortByFieldDesc language(sortQAs(l)); |
138 | } |
139 | |
140 | Map<S, O> emptyObject() { |
141 | ret mapPlus(super.emptyObject(), language := uniq(Defaults).lastLanguage); |
142 | } |
143 | |
144 | O createObject(SS map) { |
145 | S language = cast map.get('language); |
146 | if (language != null) cset(uniq(Defaults), lastLanguage := language); |
147 | ret super.createObject(map); |
148 | } |
149 | }; |
150 | data.onCreateOrUpdate.add(qa -> reindexQAs()); |
151 | |
152 | HCRUD crud = new(rawLink(), data) { |
153 | S frame(S title, S contents) { |
154 | title = ahref(or2(shortLink, rawLink("")), adminTitle) + " | " + title; |
155 | ret hhtml(hhead_title_decode(title) |
156 | + hbody(h2(title) |
157 | + p(makeNav()) |
158 | + contents)); |
159 | } |
160 | |
161 | S renderTable(bool withCmds) { |
162 | Scorer<ConsistencyError> scorer = consistencyCheckResult!; |
163 | ret (empty(scorer.errors) |
164 | ? p("Pattern analysis: No problems found. " + nEntries(countConcepts(QA)) + ".") |
165 | : joinMap(scorer.errors, lambda1 renderErrorAsParagraph)) |
166 | + super.renderTable(withCmds); |
167 | } |
168 | |
169 | S renderErrorAsParagraph(ConsistencyError error) { |
170 | S fix = error.renderFix(); |
171 | ret p("Error: " + htmlEncode2(error.msg) + " " + |
172 | joinMap(error.items, qa -> ahref(editLink(qa.id), "[item]")) |
173 | + (empty(fix) ? "" : " " + fix)); |
174 | } |
175 | }; |
176 | |
177 | // actions |
178 | |
179 | if (eqGet(params, action := 'reindex)) { |
180 | long id = parseLong(params.get('id)); |
181 | int newIndex = parseInt(params.get('newIndex)); |
182 | QA qa = getConcept(QA, id); |
183 | if (qa == null) ret crud.refreshWithMsgs("Item not found"); |
184 | cset(qa, index := newIndex); |
185 | reindexQAs(); |
186 | ret crud.refreshWithMsgs("Index of item " + qa.id + " changed"); |
187 | } |
188 | |
189 | if (eqGet(params, botOn := "1")) { |
190 | cset(uniq(Settings), botOn := true); |
191 | ret crud.refreshWithMsgs("Bot turned on!"); |
192 | } |
193 | |
194 | if (eqGet(params, botOff := "1")) { |
195 | cset(uniq(Settings), botOn := false); |
196 | ret crud.refreshWithMsgs("Bot turned off!"); |
197 | } |
198 | |
199 | if (nemptyGet botAutoOpen(params)) { |
200 | cset(uniq(Settings), botAutoOpen := eq("1", params.get("botAutoOpen"))); |
201 | ret crud.refreshWithMsgs("Bot auto-open turned " + onOff(botAutoOpen()) + "!"); |
202 | } |
203 | |
204 | crud.tableClass = "responstable"; |
205 | ret hsansserif() + hcss_responstable() + crud.renderPage(params); |
206 | } catch print e { |
207 | ret "ERROR."; |
208 | } |
209 | } |
210 | |
211 | static new ThreadLocal<S> language_out; |
212 | static new ThreadLocal<Bool> opt_noDefault; // true = return null instead of #default text |
213 | |
214 | // main API function for other bots |
215 | sS answer(S s, S language) { |
216 | language_out.set(null); |
217 | ret trim(answer_main(s, language)); |
218 | } |
219 | |
220 | sS answer_main(S s, S language) { |
221 | S raw = findRawAnswer(s, language, true); |
222 | S contents = extractKeywordPlusBracketed_keepComments("random", raw); |
223 | if (contents != null) |
224 | ret random(splitAtEmptyLines(contents)); |
225 | ret raw; |
226 | } |
227 | |
228 | sS findRawAnswer(S s, S language, bool allowTypos) { |
229 | ret selectQA(findMatchingQA(s, language, allowTypos)); |
230 | } |
231 | |
232 | static QA findQAWithTypos(S s, S language) { |
233 | Lowest<QA> qa = findClosestQA(s, language); |
234 | if (!qa.has()) null; |
235 | if (qa.score() > maxTypos) { |
236 | print("Rejecting " + nTypos((int) qa.score()) + ": " + s + " / " + qa->patterns); |
237 | null; |
238 | } |
239 | print("Accepting " + nTypos((int) qa.score()) + ": " + s + " / " + qa->patterns); |
240 | ret qa!; |
241 | } |
242 | |
243 | static QA findMatchingQA(S s, S language, bool allowTypos) { |
244 | try object QA qa = findMatchingQA(s, conceptsWhere(QA, +language)); |
245 | try object QA qa = findMatchingQA(s, filter(list(QA), q -> neq(q.language, language))); |
246 | if (allowTypos) |
247 | try object QA qa = findQAWithTypos(s, language); |
248 | if (!eq(s, "#default") && !isTrue(opt_noDefault!)) ret findMatchingQA("#default", language, false); |
249 | null; |
250 | } |
251 | |
252 | static Lowest<QA> findClosestQA(S s, S language) { |
253 | time "findClosestQA" { |
254 | new Lowest<QA> qa; |
255 | findClosestQA(s, qa, conceptsWhere(QA, +language)); |
256 | findClosestQA(s, qa, filter(list(QA), q -> neq(q.language, language))); |
257 | } |
258 | ret qa; |
259 | } |
260 | |
261 | static L<QA> sortQAs(Cl<QA> qas) { |
262 | Map<S, Int> categoryToIndex = fieldToFieldIndex('name, 'index, list(Category)); |
263 | ret sortedByCalculatedField(qas, q -> pair(categoryToIndex.get(q.category), q.index)); |
264 | } |
265 | |
266 | svoid reindexQAs() { |
267 | for (Language lang) { |
268 | int index = 1; |
269 | for (QA qa : sortQAs(conceptsWhere(QA, language := lang.code))) |
270 | cset(qa, index := index++); |
271 | } |
272 | } |
273 | |
274 | static QA findMatchingQA(S s, Cl<QA> qas) { |
275 | for (QA qa : sortQAs(qas)) |
276 | if (mmo2_match(qa.parsedPattern(), s)) |
277 | ret qa; |
278 | null; |
279 | } |
280 | |
281 | svoid findClosestQA(S s, Lowest<QA> best, Cl<QA> qas) { |
282 | for (QA qa : sortQAs(qas)) { |
283 | Int score = mmo2_levenWithSwapsScore(qa.parsedPattern(), s); |
284 | if (score != null) |
285 | best.put(qa, score); |
286 | } |
287 | } |
288 | |
289 | // returns answers |
290 | sS selectQA(QA qa) { |
291 | if (qa == null) null; |
292 | language_out.set(qa.language); |
293 | ret qa.answers; |
294 | } |
295 | |
296 | sclass ConsistencyError { |
297 | S msg; |
298 | L<QA> items; |
299 | |
300 | *(S *msg, QA... items) { this.items = asList(items); } |
301 | |
302 | S renderFix() { null; } |
303 | } |
304 | |
305 | svoid checkLocalConsistency(QA qa, Scorer<ConsistencyError> scorer) { |
306 | LS questions = tlft(qa.questions); |
307 | for (S q : questions) |
308 | if (mmo2_match(qa.parsedPattern(), q)) |
309 | scorer.ok(); |
310 | else |
311 | scorer.error(ConsistencyError("Question " + quote(q) + " not matched by patterns " + quote(qa.patterns), qa)); |
312 | } |
313 | |
314 | svoid checkGlobalConsistency(QA qa, Scorer<ConsistencyError> scorer) { |
315 | for (S q : tlft(qa.questions)) { |
316 | QA found = findMatchingQA(q, qa.language, false); |
317 | if (found != null && found != qa) |
318 | scorer.error(new ConsistencyError("Question " + quote(q) + " (item " + qa.id + ") shadowed by patterns " + quote(found.patterns) + " (item " + found.id + ")", qa, found) { |
319 | S renderFix() { |
320 | ret ahref(rawLink("?action=reindex&id=" + qa.id + "&newIndex=" + (found.index-1)), "[fix by changing index]"); |
321 | } |
322 | }); |
323 | else |
324 | scorer.ok(); |
325 | } |
326 | } |
327 | |
328 | static Scorer<ConsistencyError> consistencyCheck() { |
329 | new Scorer<ConsistencyError> scorer; |
330 | scorer.collectErrors(); |
331 | for (QA qa) { |
332 | checkLocalConsistency(qa, scorer); |
333 | checkGlobalConsistency(qa, scorer); |
334 | } |
335 | ret scorer; |
336 | } |
337 | |
338 | sS contactFormViberID() { |
339 | ret uniq(Settings).contactFormViberID; |
340 | } |
341 | |
342 | // API |
343 | |
344 | sbool botOn() { |
345 | ret uniq(Settings).botOn; |
346 | } |
347 | |
348 | sbool botAutoOpen() { |
349 | ret uniq(Settings).botAutoOpen; |
350 | } |
351 | |
352 | svoid importQA(virtual QA qa_external) { |
353 | QA qa = shallowCloneToUnlistedConcept QA(qa_external); |
354 | uniq QA(allConceptFieldsAsParams(qa)); |
355 | } |
356 | |
357 | static swappable S makeNav() { |
358 | S s = joinWithVBar( |
359 | ahref("?logout=1", "log out"), |
360 | targetBlank(relativeRawBotLink(mainBotID, "/logs"), "chat logs"), |
361 | targetBlank(rawLink("demo?bot=1"), "demo"), |
362 | targetBlank(rawLink("download"), "export brain"), |
363 | ahref(rawLink("embedCode"), "show embed code"), |
364 | botOn() |
365 | ? "Bot is ON (appears on home page) " + ahrefWithConfirm("Switch bot off?", "?botOff=1", "[switch bot off]") |
366 | : "Bot is OFF (appears only with " + targetBlank(appendQueryToURL(embedderLink, bot := 1), "?bot=1") + ") " + ahrefWithConfirm("Switch bot on?", "?botOn=1", "[switch bot on]") |
367 | ); |
368 | |
369 | if (enableWorkerChat) |
370 | s += |
371 | " | " + ahref(rawBotLink(mainBotID, "workers-admin"), "workers admin") |
372 | + " | " + targetBlank(rawBotLink(mainBotID, "worker"), "worker chat"); |
373 | |
374 | ret s; |
375 | } |
Began life as a copy of #1028280
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: | #1028655 |
Snippet name: | Web Bot Answers DB Include [backup before test scripts] |
Eternal ID of this version: | #1028655/1 |
Text MD5: | 01679977a9fe315c5e443542c918e1d6 |
Author: | stefan |
Category: | javax / web chat bots |
Type: | JavaX fragment (include) |
Public (visible to everyone): | Yes |
Archived (hidden from active list): | No |
Created/modified: | 2020-07-02 12:12:26 |
Source code size: | 11534 bytes / 375 lines |
Pitched / IR pitched: | No / No |
Views / Downloads: | 134 / 165 |
Referenced in: | [show references] |