Not logged in.  Login/Logout/Register | List snippets | | Create snippet | Upload image | Upload data

487
LINES

< > BotCompany Repo | #1028280 // Web Bot Answers DB Include [latest]

JavaX fragment (include) [tags: use-pretranspiled]

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  
}

Author comment

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]