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.

do not include class And.
set flag OurSyncCollections.

sS mainBotID;
static int maxTypos = 1;

sS adminTitle = "Chat Bot Admin";
sS shortLink; // catchy link to admin if available
sbool enableWorkerChat;

sS embedderLink; // where chat bot will go
sS goDaddyEmbedCode;

static Cache<Scorer<ConsistencyError>> consistencyCheckResult = new(lambda0 consistencyCheck);

concept Category {
  int index;
  S name;
}

concept Language {
  int index;
  S code;
}

concept QA {
  int index;
  S language;
  S questions; // line by line
  S patterns; // to be determined
  S answers; // one answer, or "random { answers separated by empty lines }"
  S category; // "business", "smalltalk"

  transient MMOPattern parsedPattern;

  sS _fieldOrder = "language category index questions patterns answers";
  
  void change {
    parsedPattern = null;
    super.change();
  }
  
  MMOPattern parsedPattern() {
    if (parsedPattern == null)
      parsedPattern = mmo2_parsePattern(patterns);
    ret parsedPattern;
  }
}

concept Defaults {
  S lastLanguage;
}

concept Settings {
  bool botOn, botAutoOpen;
}

svoid pQADB {
  print("fieldOrder", getFieldOrder(QA));
  dbIndexing(Language, 'index, Category, 'index);
  indexSingletonConcept(Defaults);
  indexSingletonConcept(Settings);
  
  // languages, default language
  cset(conceptsWhere QA(language := null), language := 'en);
  uniq(Language, code := 'en);
  removeConceptsWhere(Language, code := 'de);
  
  // categories, default category
  int i = 0;
  for (S category : ll("business", "smalltalk"))
    cset(uniq(Category, name := category), index := i++);
  cset(conceptsWhere QA(category := null), category := "business");
  
  onConceptsChange(r { consistencyCheckResult.clear(); });
  reindexQAs();
}

html { try {
  if (swic(addSlash(uri), "/answer/")) {
    temp tempSetTL(opt_noDefault, valueIs1 noDefault(params));
    ret serveText(unnull(answer(params.get("q"), "en")));
  }
    
  // force auth
  try answer (S) callHtmlMethod2(mainBot(), "/auth-only", mapPlus(
params, uri := rawLink(uri)));
    
  if (eq(uri, "/demo"))
    ret hrefresh(appendQueryToURL(rawBotLink(mainBotID), _botDemo := 1, bot := 1));
    
  if (eq(uri, "/download"))
    ret subBot_serveText(conceptsStructure());
    
  if (eq(uri, "/embedCode"))
    ret hsansserif() + htitle_h2("Chat bot embed code")
      + (empty(goDaddyEmbedCode) ? "" :
        h3("GoDaddy Site Builder")
      
        + p("Add an HTML box with this code:")
      
        + pre(htmlEncode2(goDaddyEmbedCode))
        
        + h3("Other"))
        
      + p("Add this code in front of your " + tt(htmlEncode2("</body>")) + " tag:")
      
      + pre(htmlEncode2(hjavascript_src_withType(rawBotLink(mainBotID), defer := html_valueLessParam())));
  
  if (eq(uri, "/dbUploads/import")) {
    long id = parseLong(params.get("id"));
    DBUpload dbUpload = getConcept DBUpload(id);
    if (dbUpload == null) ret "Not found";
    
    Map<Long, O> qaMap = cast safeUnstructure(dbUpload.text);
    Map<S, QA> existingByPattern = indexByFieldCI patterns(list(QA));
    new LS msgs;
    msgs.add("Importing...");

    for (S _qaID : startingWith_dropPrefix("qa_", keys(params))) {
      long qaID = parseLong(_qaID);
      virtual QA qa = qaMap.get(qaID);
      if (qa == null) continue;

      S patterns = getString patterns(qa), answers = getString answers(qa);
      QA existing = existingByPattern.get(patterns);
      if (existing != null) {
        msgs.add("QA item exists for patterns: " + patterns);
        msgs.add("Answers are " + (eq(answers, existing.answers) ? "identical" : "different"));
      } else {
        QA imported = cnew QA();
        copyFields(qa, imported, "index", "language", "questions", "patterns", "answers", "category");
        msgs.add("Imported item " + qaID + " as " + imported.id + " (patterns: " + patterns + ")");
      }
    }

    ret htmlEncode2_nlToBr(lines(msgs));
  }
    
  if (eq(uri, "/dbUploads/view")) {
    long id = parseLong(params.get("id"));
    DBUpload dbUpload = getConcept DBUpload(id);

    S contents;
    if (dbUpload == null)
      contents = "Not found";
    else {
      new L<Map> data;
      Map<Long, O> qaMap = cast safeUnstructure(dbUpload.text);

      // filter and sort
      qaMap = filterByValuePredicate(qaMap, c -> dynShortNameIs QA(c));
      qaMap = mapSortedByFunctionOnValue(qaMap, c -> neqic(getString category(c), "smalltalk"));

      for (long qaID, O c : qaMap) {
        S category = getString category(c);
        data.add(litorderedmap(
          "ID" := qaID,
          "Select" := hcheckbox("qa_" + qaID, eqic(category, "smalltalk")),
          "Category" := htmlEncode2(category),
          "Questions" := htmlEncode2(getString questions(c)),
          "Patterns" := htmlEncode2(getString patterns(c)),
          "Answers" := htmlEncode2(getString answers(c)),
          ));
      }
      contents = hpostform(
          hhidden(+id)
        + p(hsubmit("Import selected questions"))
        + htmlTable2_noHtmlEncode(data),
        action := rawLink("dbUploads/import")
      );
    }

    S title = "Import questions";
    ret hhtml(hhead_title_decode(title)
      + hbody(h2(title)
      + p(makeNav())
      + contents));
    
  }
  
  if (eq(uri, "/dbUploads")) {
    HCRUD_Concepts<DBUpload> data = new HCRUD_Concepts<DBUpload>(DBUpload) {
      Renderer getRenderer(S field) {
        if (eq(field, "text"))
          ret new TextArea(80, 20);
        ret super.getRenderer(field);
      }
    };

    HCRUD crud = new(rawLink("dbUploads"), data) {
      S frame(S title, S contents) {
        title = ahref(or2(shortLink, rawLink("")), adminTitle) + " | " + title;
        ret hhtml(hhead_title_decode(title)
          + hbody(h2(title)
          + p(makeNav())
          + contents));
      }
      
      S renderCmds(MapSO item) {
        O id = item.get(data.idField());
        ret joinNemptiesWithVBar(super.renderCmds(item), ahref(baseLink + "/view?id=" + urlencode(str(id)), "View questions"));
      }
    };
    
    crud.tableClass = "responstable";
    ret hsansserif() + hcss_responstable() + crud.renderPage(params);
  }  
  
  // make QA CRUD_
  
  HCRUD_Concepts<QA> data = new HCRUD_Concepts<QA>(QA) {
    S itemName() { ret "question"; }
    
    S fieldHelp(S field) {
      if (eq(field, "category"))
        ret "smalltalk has a lower precedence than business";
      if (eq(field, "index"))
        ret "lower index = higher matching precedence";
      if (eq(field, "patterns"))
        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.]];
      if (eq(field, "answers"))
        ret "Text of answer. Use random { ... } for multiple answers (separated from each other by an empty line)";
      null;
    }
    
    Renderer getRenderer(S field) {
      if (eqOneOf(field, 'questions, 'answers))
        ret new TextArea(80, 10);
      if (eq(field, 'patterns))
        ret new TextField(80);
      if (eq(field, 'language))
        ret new ComboBox(collect code(conceptsSortedByField(Language, 'index)));
      if (eq(field, 'category))
        ret new ComboBox(collect name(conceptsSortedByField(Category, 'index)));
      ret super.getRenderer(field);
    }
    
    // sort by language first, then by priority (category + index)
    L<QA> defaultSort(L<QA> l) {
      ret sortByFieldDesc language(sortQAs(l));
    }
    
    Map<S, O> emptyObject() {
      ret mapPlus(super.emptyObject(), language := uniq(Defaults).lastLanguage);
    }
    
    O createObject(SS map, S fieldPrefix) {
      S language = cast map.get('language);
      if (language != null) cset(uniq(Defaults), lastLanguage := language);
      ret super.createObject(map, fieldPrefix);
    }
  };
  data.onCreateOrUpdate.add(qa -> reindexQAs());
  
  HCRUD crud = new(rawLink(), data) {
    S frame(S title, S contents) {
      S stats = (S) pcallOpt(mainBot(), 'dbStats);
      title = ahref(or2(shortLink, rawLink("")), adminTitle) + " | " + title;
      ret hhtml(hhead_title_decode(title)
        + hbody(h2(title)
        + pIfNempty(stats)
        + p(makeNav())
        + contents));
    }
    
    S renderTable(bool withCmds) {
      Scorer<ConsistencyError> scorer = consistencyCheckResult!;
      ret (empty(scorer.errors)
        ? p("Pattern analysis: No problems found. " + nEntries(countConcepts(QA)) + ".")
        : joinMap(scorer.errors, lambda1 renderErrorAsParagraph))
        + super.renderTable(withCmds);
    }
    
    S renderErrorAsParagraph(ConsistencyError error) {
      S fix = error.renderFix();
      ret p("Error: " + htmlEncode2(error.msg) + " " +
        joinMap(error.items, qa -> ahref(editLink(qa.id), "[item]"))
        + (empty(fix) ? "" : " " + fix));
    }
  };
  
  // actions

  if (eqGet(params, action := 'reindex)) {
    long id = parseLong(params.get('id));
    int newIndex = parseInt(params.get('newIndex));
    QA qa = getConcept(QA, id);
    if (qa == null) ret crud.refreshWithMsgs("Item not found");
    cset(qa, index := newIndex);
    reindexQAs();
    ret crud.refreshWithMsgs("Index of item " + qa.id + " changed");
  }

  if (eqGet(params, botOn := "1")) {
    cset(uniq(Settings), botOn := true);
    ret crud.refreshWithMsgs("Bot turned on!");
  }
  
  if (eqGet(params, botOff := "1")) {
    cset(uniq(Settings), botOn := false);
    ret crud.refreshWithMsgs("Bot turned off!");
  }
  
  if (nemptyGet botAutoOpen(params)) {
    cset(uniq(Settings), botAutoOpen := eq("1", params.get("botAutoOpen")));
    ret crud.refreshWithMsgs("Bot auto-open turned " + onOff(botAutoOpen()) + "!");
  }
  
  crud.tableClass = "responstable";
  ret hsansserif() + hcss_responstable() + crud.renderPage(params);
 } catch print e {
  ret "ERROR.";
 }
}

static new ThreadLocal<S> language_out;
static new ThreadLocal<Bool> opt_noDefault; // true = return null instead of #default text

// main API function for other bots
sS answer(S s, S language) {
  language_out.set(null);
  ret trim(answer_main(s, language));
}

sS answer_main(S s, S language) {
  S raw = findRawAnswer(s, language, true);
  S contents = extractKeywordPlusBracketed_keepComments("random", raw);
  if (contents != null)
    ret random(splitAtEmptyLines(contents));
  ret raw;
}

sS findRawAnswer(S s, S language, bool allowTypos) {
  ret selectQA(findMatchingQA(s, language, allowTypos));
}

static QA findQAWithTypos(S s, S language) {
  Lowest<QA> qa = findClosestQA(s, language);
  if (!qa.has()) null;
  if (qa.score() > maxTypos) {
    print("Rejecting " + nTypos((int) qa.score()) + ": " + s + " / " + qa->patterns);
    null;
  }
  print("Accepting " + nTypos((int) qa.score()) + ": " + s + " / " + qa->patterns);
  ret qa!;
}

static QA findMatchingQA(S s, S language, bool allowTypos) {
  try object QA qa = findMatchingQA(s, conceptsWhere(QA, +language));
  try object QA qa = findMatchingQA(s, filter(list(QA), q -> neq(q.language, language)));
  if (allowTypos)
    try object QA qa = findQAWithTypos(s, language);
  if (!eq(s, "#default") && !isTrue(opt_noDefault!)) ret findMatchingQA("#default", language, false);
  null;
}

static Lowest<QA> findClosestQA(S s, S language) {
  time "findClosestQA" {
    new Lowest<QA> qa;
    findClosestQA(s, qa, conceptsWhere(QA, +language));
    findClosestQA(s, qa, filter(list(QA), q -> neq(q.language, language)));
  }
  ret qa;
}

static L<QA> sortQAs(Cl<QA> qas) {
  Map<S, Int> categoryToIndex = fieldToFieldIndex('name, 'index, list(Category));
  ret sortedByCalculatedField(qas, q -> pair(categoryToIndex.get(q.category), q.index));
}

svoid reindexQAs() {
  for (Language lang) {
    int index = 1;
    for (QA qa : sortQAs(conceptsWhere(QA, language := lang.code)))
      cset(qa, index := index++);
  }
}

static QA findMatchingQA(S s, Cl<QA> qas) {
  for (QA qa : sortQAs(qas))
    if (mmo2_match(qa.parsedPattern(), s))
      ret qa;
  null;
}

svoid findClosestQA(S s, Lowest<QA> best, Cl<QA> qas) {
  for (QA qa : sortQAs(qas)) {
    Int score = mmo2_levenWithSwapsScore(qa.parsedPattern(), s);
    if (score != null)
      best.put(qa, score);
  }
}

// returns answers
sS selectQA(QA qa) {
  if (qa == null) null;
  language_out.set(qa.language);
  ret qa.answers;
}

sclass ConsistencyError {
  S msg;
  L<QA> items;
  
  *(S *msg, QA... items) { this.items = asList(items); }
  
  S renderFix() { null; }
}

svoid checkLocalConsistency(QA qa, Scorer<ConsistencyError> scorer) {
  LS questions = tlft(qa.questions);
  for (S q : questions)
    if (mmo2_match(qa.parsedPattern(), q))
      scorer.ok();
    else
      scorer.error(ConsistencyError("Question " + quote(q) + " not matched by patterns " + quote(qa.patterns), qa));
}

svoid checkGlobalConsistency(QA qa, Scorer<ConsistencyError> scorer) {
  for (S q : tlft(qa.questions)) {
    QA found = findMatchingQA(q, qa.language, false);
    if (found != null && found != qa)
      scorer.error(new ConsistencyError("Question " + quote(q) + " (item " + qa.id + ") shadowed by patterns " + quote(found.patterns) + " (item " + found.id + ")", qa, found) {
        S renderFix() {
          ret ahref(rawLink("?action=reindex&id=" + qa.id + "&newIndex=" + (found.index-1)), "[fix by changing index]");
        }
      });
    else
      scorer.ok();
  }
}

static Scorer<ConsistencyError> consistencyCheck() {
  new Scorer<ConsistencyError> scorer;
  scorer.collectErrors();
  for (QA qa) {
    checkLocalConsistency(qa, scorer);
    checkGlobalConsistency(qa, scorer);
  }
  ret scorer;
}

// API

sbool botOn() {
  ret uniq(Settings).botOn;
}

sbool botAutoOpen() {
  ret uniq(Settings).botAutoOpen;
}

svoid importQA(virtual QA qa_external) {
  QA qa = shallowCloneToUnlistedConcept QA(qa_external);
  uniq QA(allConceptFieldsAsParams(qa));
}

static swappable S makeNav() {
  S s = joinWithVBar(
    ahref("?logout=1", "log out"),
    targetBlank(relativeRawBotLink(mainBotID, "/logs"), "chat logs"),
    targetBlank(rawLink("demo?bot=1"), "demo"),
    targetBlank(rawLink("download"), "export brain"),
    ahref(rawLink("embedCode"), "show embed code"),
    botOn()
      ? "Bot is ON (appears on home page) " + ahrefWithConfirm("Switch bot off?", "?botOff=1", "[switch bot off]")
      : "Bot is OFF (appears only with " + targetBlank(appendQueryToURL(embedderLink, bot := 1), "?bot=1") + ") " + ahrefWithConfirm("Switch bot on?", "?botOn=1", "[switch bot on]"),
    ahref(rawLink("dbUploads"), "db uploads"),
  );
  
  if (enableWorkerChat)
    s +=
      " | " + ahref(rawBotLink(mainBotID, "workers-admin"), "workers admin")
    + " | " + targetBlank(rawBotLink(mainBotID, "worker"), "worker chat");
    
  ret s;
}

sO mainBot() {
  ret getBot(mainBotID);
}

concept DBUpload {
  S name;
  S text;
}

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: 278 / 644
Version history: 39 change(s)
Referenced in: #1028281 - ContractBox Answers DB [LIVE]
#1028421 - BookBetter Answers DB [LIVE]
#1028655 - Web Bot Answers DB Include [backup before test scripts]