Uses 911K of libraries. Click here for Pure Java version (22739L/129K).
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 | |
68 | //sS mainBotID = #1026411; |
69 | static int maxTypos = 1; |
70 | |
71 | sS adminTitle = "Tomii Boi Chat Bot Admin"; |
72 | sS shortLink; // catchy link to admin if available |
73 | |
74 | sS embedderLink; // where chat bot will go |
75 | sS goDaddyEmbedCode; |
76 | |
77 | static Cache<Scorer<ConsistencyError>> consistencyCheckResult = new(lambda0 consistencyCheck); |
78 | |
79 | concept Server { |
80 | S serverID, name; |
81 | |
82 | toString { ret name; } |
83 | } |
84 | |
85 | concept ServerToCategory { |
86 | Server server; |
87 | Category category; |
88 | bool enabled; |
89 | |
90 | sS _fieldOrder = "server category enabled"; |
91 | } |
92 | |
93 | concept Channel { |
94 | Server server; |
95 | S channelID; |
96 | S name; |
97 | bool botEnabled = true; |
98 | |
99 | toString { ret name; } |
100 | sS _fieldOrder = "botEnabled name server"; |
101 | } |
102 | |
103 | concept Category { |
104 | int index; |
105 | S name; |
106 | bool onByDefault = true; |
107 | |
108 | toString { ret name; } |
109 | } |
110 | |
111 | concept Language { |
112 | int index; |
113 | S code; |
114 | } |
115 | |
116 | concept QA { |
117 | int index; |
118 | S questions; // line by line |
119 | S patterns; // to be determined |
120 | S answers; // one answer, or "random { answers separated by empty lines }" |
121 | S category; // "business", "smalltalk" etc. |
122 | |
123 | transient MMOPattern parsedPattern; |
124 | |
125 | sS _fieldOrder = "category index questions patterns answers"; |
126 | |
127 | void change { |
128 | parsedPattern = null; |
129 | super.change(); |
130 | } |
131 | |
132 | MMOPattern parsedPattern() { |
133 | if (parsedPattern == null) |
134 | parsedPattern = mmo_parsePattern(patterns); |
135 | ret parsedPattern; |
136 | } |
137 | } |
138 | |
139 | // keep these because otherwise deserialization breaks |
140 | concept Settings {} |
141 | concept Defaults {} |
142 | |
143 | svoid init { |
144 | processConceptsOverwriteFile(); |
145 | //print("fieldOrder", getFieldOrder(QA)); |
146 | dbIndexing(Category, 'index, Channel, 'channelID); |
147 | print("QA count: " + countConcepts(QA)); |
148 | |
149 | // categories, default category |
150 | int i = 0; |
151 | for (S category : ll("business", "smalltalk", "btd")) |
152 | cset(uniq(Category, name := category), index := i++); |
153 | cset(conceptsWhere QA(category := null), category := "business"); |
154 | |
155 | onConceptsChange(r { consistencyCheckResult.clear(); }); |
156 | reindexQAs(); |
157 | } |
158 | |
159 | html { try { |
160 | print(+uri); |
161 | |
162 | // force auth |
163 | /*try answer callHtmlMethod(getBot(mainBotID), "/auth-only", mapPlus( |
164 | params, uri := rawLink(uri)));*/ |
165 | |
166 | if (eq(uri, "/download")) |
167 | ret serveText(conceptsStructure()); |
168 | |
169 | HCRUD_Concepts<QA> data = new HCRUD_Concepts<QA>(QA) { |
170 | S itemName() { ret "question"; } |
171 | |
172 | S fieldHelp(S field) { |
173 | if (eq(field, "index")) |
174 | ret "lower index = higher matching precedence"; |
175 | if (eq(field, "patterns")) |
176 | 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.]]; |
177 | if (eq(field, "answers")) |
178 | ret "Text of answer. Use random { ... } for multiple answers (separated from each other by an empty line)"; |
179 | null; |
180 | } |
181 | |
182 | Renderer getRenderer(S field) { |
183 | if (eqOneOf(field, 'questions, 'answers)) |
184 | ret new TextArea(80, 10); |
185 | if (eq(field, 'patterns)) |
186 | ret new TextField(80); |
187 | if (eq(field, 'category)) |
188 | ret new ComboBox(collect name(conceptsSortedByField(Category, 'index))); |
189 | ret super.getRenderer(field); |
190 | } |
191 | |
192 | // sort for display |
193 | L<QA> defaultSort(L<QA> l) { |
194 | ret sortedByCalculatedField(l, q -> pair(lower(q.category), q.index)); |
195 | } |
196 | }; |
197 | data.onCreateOrUpdate.add(qa -> reindexQAs()); |
198 | |
199 | HCRUD crud = new(rawLink(), data) { |
200 | S frame(S title, S contents) { |
201 | title = ahref(or2(shortLink, rawLink("")), adminTitle) + " | " + title; |
202 | ret hhtml(hhead_title_decode(title) |
203 | + hbody(h2(title) |
204 | + p(joinWithVBar( |
205 | targetBlank(rawLink("download"), "export brain"), |
206 | )) |
207 | + contents)); |
208 | } |
209 | |
210 | S renderTable(bool withCmds) { |
211 | Scorer<ConsistencyError> scorer = consistencyCheckResult!; |
212 | ret (empty(scorer.errors) |
213 | ? p("Pattern analysis: No problems found. " + nEntries(countConcepts(QA)) + ".") |
214 | : joinMap(scorer.errors, lambda1 renderErrorAsParagraph)) |
215 | + super.renderTable(withCmds); |
216 | } |
217 | |
218 | S renderErrorAsParagraph(ConsistencyError error) { |
219 | S fix = error.renderFix(); |
220 | ret p("Error: " + htmlEncode2(error.msg) + " " + |
221 | joinMap(error.items, qa -> ahref(editLink(qa.id), "[item]")) |
222 | + (empty(fix) ? "" : " " + fix)); |
223 | } |
224 | |
225 | S renderValue(S field, O value) { |
226 | S html = super.renderValue(field, value); |
227 | if (eq(field, "questions")) html = b(html); |
228 | ret html; |
229 | } |
230 | }; |
231 | |
232 | // actions |
233 | |
234 | if (eqGet(params, action := 'reindex)) { |
235 | long id = parseLong(params.get('id)); |
236 | int newIndex = parseInt(params.get('newIndex)); |
237 | QA qa = getConcept(QA, id); |
238 | if (qa == null) ret crud.refreshWithMsgs("Item not found"); |
239 | cset(qa, index := newIndex); |
240 | reindexQAs(); |
241 | ret crud.refreshWithMsgs("Index of item " + qa.id + " changed"); |
242 | } |
243 | |
244 | crud.tableClass = "responstable"; |
245 | ret hsansserif() + hcss_responstable() + crud.renderPage(params); |
246 | } catch print e { |
247 | ret "ERROR."; |
248 | } |
249 | } |
250 | |
251 | // main API function for other bots |
252 | sS answer(S s, O... _) { |
253 | optPar Server server; |
254 | QA qa = findMatchingQA(s, qasForServer(server)); |
255 | if (qa == null) null; |
256 | S raw = qa.answers; |
257 | S contents = extractKeywordPlusBracketed_keepComments("random", raw); |
258 | if (contents != null) |
259 | ret random(splitAtEmptyLines(contents)); |
260 | ret raw; |
261 | } |
262 | |
263 | static L<QA> qasForServer(Server server) { |
264 | Set<S> categories = asSet(categoriesForServer(server)); |
265 | ret sortQAs(filter(list(QA), qa -> contains(categories, qa.category))); |
266 | } |
267 | |
268 | static QA findQAWithTypos(S s, Cl<QA> qas) { |
269 | Lowest<QA> qa = findClosestQA(s, qas); |
270 | if (!qa.has()) null; |
271 | if (qa.score() > maxTypos) { |
272 | print("Rejecting " + nTypos((int) qa.score()) + ": " + s + " / " + qa->patterns); |
273 | null; |
274 | } |
275 | print("Accepting " + nTypos((int) qa.score()) + ": " + s + " / " + qa->patterns); |
276 | ret qa!; |
277 | } |
278 | |
279 | static QA findMatchingQA(S s, Cl<QA> qas, bool allowTypos) { |
280 | try object QA qa = findMatchingQA(s, qas); |
281 | if (allowTypos) |
282 | try object QA qa = findQAWithTypos(s, qas); |
283 | null; |
284 | } |
285 | |
286 | static Lowest<QA> findClosestQA(S s, Cl<QA> qas) { |
287 | time "findClosestQA" { |
288 | new Lowest<QA> qa; |
289 | findClosestQA(s, qa, qas); |
290 | } |
291 | ret qa; |
292 | } |
293 | |
294 | static L<QA> sortQAs(Cl<QA> qas) { |
295 | Map<S, Int> categoryToIndex = fieldToFieldIndex('name, 'index, list(Category)); |
296 | ret sortedByCalculatedField(qas, q -> pair(categoryToIndex.get(q.category), q.index)); |
297 | } |
298 | |
299 | svoid reindexQAs() { |
300 | int index = 1; |
301 | for (QA qa : sortQAs(list(QA))) |
302 | cset(qa, index := index++); |
303 | } |
304 | |
305 | static QA findMatchingQA(S s, Cl<QA> qas) { |
306 | for (QA qa : sortQAs(qas)) |
307 | if (mmo_match_parsedPattern(qa.parsedPattern(), s)) |
308 | ret qa; |
309 | null; |
310 | } |
311 | |
312 | svoid findClosestQA(S s, Lowest<QA> best, Cl<QA> qas) { |
313 | for (QA qa : sortQAs(qas)) { |
314 | Int score = mmo_levenWithSwapsScore_parsedPattern(qa.parsedPattern(), s); |
315 | if (score != null) |
316 | best.put(qa, score); |
317 | } |
318 | } |
319 | |
320 | sclass ConsistencyError { |
321 | S msg; |
322 | L<QA> items; |
323 | |
324 | *(S *msg, QA... items) { this.items = asList(items); } |
325 | |
326 | S renderFix() { null; } |
327 | } |
328 | |
329 | svoid checkLocalConsistency(QA qa, Scorer<ConsistencyError> scorer) { |
330 | LS questions = tlft(qa.questions); |
331 | for (S q : questions) |
332 | if (mmo_match_parsedPattern(qa.parsedPattern(), q)) |
333 | scorer.ok(); |
334 | else |
335 | scorer.error(ConsistencyError("Question " + quote(q) + " not matched by patterns " + quote(qa.patterns), qa)); |
336 | } |
337 | |
338 | svoid checkGlobalConsistency(QA qa, Cl<QA> qas, Scorer<ConsistencyError> scorer) { |
339 | for (S q : tlft(qa.questions)) { |
340 | QA found = findMatchingQA(q, qas); |
341 | if (found != null && found != qa) |
342 | scorer.error(new ConsistencyError("Question " + quote(q) + " (item " + qa.id + ") shadowed by patterns " + quote(found.patterns) + " (item " + found.id + ")", qa, found) { |
343 | S renderFix() { |
344 | ret ahref(rawLink("?action=reindex&id=" + qa.id + "&newIndex=" + (found.index-1)), "[fix by changing index]"); |
345 | } |
346 | }); |
347 | else |
348 | scorer.ok(); |
349 | } |
350 | } |
351 | |
352 | static Scorer<ConsistencyError> consistencyCheck() { |
353 | new Scorer<ConsistencyError> scorer; |
354 | scorer.collectErrors(); |
355 | for (QA qa) checkLocalConsistency(qa, scorer); |
356 | for (Server server) { |
357 | L<QA> qas = qasForServer(server); |
358 | for (QA qa : qas) |
359 | checkGlobalConsistency(qa, qas, scorer); |
360 | } |
361 | ret scorer; |
362 | } |
363 | |
364 | // server can be null |
365 | static Cl<S> categoriesForServer(Server server) { |
366 | Set<Category> set = asSet(conceptsWhere Category(onByDefault := true)); |
367 | if (server != null) for (ServerToCategory link : conceptsWhere(ServerToCategory, +server)) |
368 | addOrRemove(set, link.category, link.enabled); |
369 | ret collectAsSet name(set); |
370 | } |
371 | |
372 | |
373 | // API |
374 | |
375 | svoid importQA(virtual QA qa_external) { |
376 | QA qa = shallowCloneToUnlistedConcept QA(qa_external); |
377 | uniq QA(allConceptFieldsAsParams(qa)); |
378 | } |
379 | |
380 | sS rawLink() { ret "/"; } |
381 | sS rawLink(S uri) { ret addSlashPrefix(uri) ; } |
Began life as a copy of #1026409
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: | #1028036 |
Snippet name: | Tomii Boi Answers DB Module [for OS, use with #1026494] |
Eternal ID of this version: | #1028036/50 |
Text MD5: | 0aa86cc87a2029f24c313e7c3f090ccc |
Transpilation MD5: | 4e8b27f9083c487ebf58d8a2ce9b14d8 |
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-06-21 00:41:03 |
Source code size: | 10959 bytes / 381 lines |
Pitched / IR pitched: | No / No |
Views / Downloads: | 309 / 4260 |
Version history: | 49 change(s) |
Referenced in: | [show references] |