knowrob  2.1.0
A Knowledge Base System for Cognition-enabled Robots
terminal.cpp
Go to the documentation of this file.
1 /*
2  * This file is part of KnowRob, please consult
3  * https://github.com/knowrob/knowrob for license details.
4  */
5 
6 #include <termios.h>
7 // STD
8 #include <exception>
9 #include <iostream>
10 #include <algorithm>
11 #include <memory>
12 #include <list>
13 // BOOST
14 #include <boost/program_options/options_description.hpp>
15 #include <boost/program_options/variables_map.hpp>
16 #include <boost/program_options/parsers.hpp>
17 #include <boost/property_tree/json_parser.hpp>
18 #include <boost/property_tree/ptree.hpp>
19 #include <boost/archive/text_oarchive.hpp>
20 #include <boost/archive/text_iarchive.hpp>
21 #include <utility>
22 // KnowRob
23 #include <knowrob/knowrob.h>
24 #include <knowrob/Logger.h>
25 #include <knowrob/KnowledgeBase.h>
26 #include "knowrob/formulas/Predicate.h"
27 #include "knowrob/queries/QueryParser.h"
28 #include "knowrob/semweb/PrefixRegistry.h"
29 #include "knowrob/queries/QueryError.h"
30 #include "knowrob/queries/QueryTree.h"
31 #include "knowrob/queries/Answer.h"
32 #include "knowrob/queries/AnswerYes.h"
33 #include "knowrob/queries/FormulaQuery.h"
34 #include "knowrob/integration/InterfaceUtils.h"
35 
36 using namespace knowrob;
37 namespace po = boost::program_options;
38 
39 static const char *PROMPT = "?- ";
40 
41 namespace knowrob {
42  class QueryHistory {
43  public:
44  explicit QueryHistory()
45  : selection_(data_.end()),
46  maxHistoryItems_(100),
47  pos_(-1) {}
48 
49  void append(const std::string &queryString) {
50  data_.push_front(queryString);
51  reset();
52  }
53 
54  void reset() {
55  selection_ = data_.end();
56  pos_ = -1;
57  }
58 
59  void save(const std::string &historyFile) {
60  std::string tmpFileName = historyFile + ".tmp";
61  std::ofstream file(tmpFileName);
62  if (file.good()) {
63  boost::archive::text_oarchive oa(file);
64  oa << data_.size();
65  for (auto &x: data_) oa << x;
66  // the history file will be corrupted in case the program is terminated during writing,
67  // so better first write to a temporary file and then rename it.
68  std::filesystem::rename(tmpFileName, historyFile);
69  } else {
70  KB_WARN("unable to write history to file '{}'", historyFile);
71  }
72  }
73 
74  void load(const std::string &historyFile) {
75  std::ifstream file(historyFile);
76  if (file.good()) {
77  boost::archive::text_iarchive ia(file);
78  std::list<std::string>::size_type size;
79  ia >> size;
80  size = std::min(maxHistoryItems_, size);
81  for (std::size_t i = 0; i < size; ++i) {
82  std::string queryString;
83  ia >> queryString;
84  data_.push_back(queryString);
85  }
86  }
87  }
88 
89  const std::string &getSelection() { return *selection_; }
90 
91  bool hasSelection() { return selection_ != data_.end(); }
92 
93  void nextItem() {
94  if (pos_ == -1) {
95  selection_ = data_.begin();
96  pos_ = 0;
97  } else if (pos_ == 0) {
98  selection_++;
99  }
100  if (selection_ == data_.end()) {
101  pos_ = 1;
102  }
103  }
104 
105  void previousItem() {
106  if (selection_ == data_.begin()) {
107  selection_ = data_.end();
108  pos_ = -1;
109  } else if (pos_ == 0 || pos_ == 1) {
110  selection_--;
111  pos_ = 0;
112  }
113  }
114 
115  protected:
116  std::list<std::string> data_;
117  std::list<std::string>::iterator selection_;
118  unsigned long maxHistoryItems_;
119  int pos_;
120  };
121 
122  template<class T>
123  class TerminalCommand {
124  public:
125  using CommandFunction = std::function<bool(const std::vector<T> &arguments)>;
126 
127  TerminalCommand(std::string functor, uint32_t arity, CommandFunction function)
128  : functor_(std::move(functor)), arity_(arity), function_(std::move(function)) {}
129 
130  bool runCommand(const std::vector<T> &arguments) {
131  if (arguments.size() != arity_) {
132  throw QueryError("Wrong number of arguments for terminal command '{}/{}'. "
133  "Actual number of arguments: {}.", functor_, arity_, arguments.size());
134  }
135  return function_(arguments);
136  }
137 
138  protected:
139  const std::string functor_;
140  const uint32_t arity_;
141  const CommandFunction function_;
142  };
143 }
144 
145 class KnowRobTerminal {
146 public:
147  explicit KnowRobTerminal(const boost::property_tree::ptree &config)
148  : kb_(KnowledgeBase::create(config)),
149  has_stop_request_(false),
150  numSolutions_(0),
151  cursor_(0),
152  historyFile_("history.txt") {
153  try {
154  history_.load(historyFile_);
155  }
156  catch (boost::archive::archive_exception &e) {
157  KB_WARN("A 'boost::archive' exception occurred "
158  "when loading history file ({}) of the terminal: {}. "
159  "It might be that the file is corrupted for some reason.",
160  historyFile_, e.what());
161  }
162  // define some terminal commands
163  registerCommand("exit", 0,
164  [this](const std::vector<TermPtr> &) { return exitTerminal(); });
165  registerCommand("assert", 1,
166  [this](const std::vector<FormulaPtr> &x) { return InterfaceUtils::assertStatements(kb_, x); });
167  registerCommand("tell", 1,
168  [this](const std::vector<FormulaPtr> &x) { return InterfaceUtils::assertStatements(kb_, x); });
169  }
170 
171  static char getch() {
172  static struct termios old, current;
173  static char buf = 0;
174  // read termios settings
175  tcgetattr(0, &old);
176  // apply modified settings
177  current = old;
178  current.c_lflag &= ~ICANON; /* disable buffered i/o */
179  current.c_lflag &= ~ECHO; /* set no echo mode */
180  tcsetattr(0, TCSANOW, &current);
181  // get the next char
182  if (read(0, &buf, 1) < 0)
183  perror("read()");
184  // reset old settings
185  tcsetattr(0, TCSANOW, &old);
186  return buf;
187  }
188 
189  void registerCommand(const std::string &functor,
190  uint32_t arity,
191  const TerminalCommand<TermPtr>::CommandFunction &function) {
192  firstOrderCommands_.emplace(functor, TerminalCommand(functor, arity, function));
193  }
194 
195  void registerCommand(const std::string &functor,
196  uint32_t arity,
197  const TerminalCommand<FormulaPtr>::CommandFunction &function) {
198  higherOrderCommands_.emplace(functor, TerminalCommand(functor, arity, function));
199  }
200 
201  // Override QueryResultHandler
202  bool pushQueryResult(const AnswerPtr &solution) {
203  std::cout << *solution;
204  numSolutions_ += 1;
205  return !has_stop_request_;
206  }
207 
208  bool runHigherOrderCommand(const std::string &functor, const std::string &queryString) {
209  auto argsFormula = QueryParser::parse(queryString);
210  auto needle = higherOrderCommands_.find(functor);
211  if (needle == higherOrderCommands_.end()) {
212  throw QueryError("Ignoring unknown higher-order command '{}'", functor);
213  }
214  if (argsFormula->type() == FormulaType::CONJUNCTION) {
215  return needle->second.runCommand(((Conjunction *) argsFormula.get())->formulae());
216  } else {
217  return needle->second.runCommand({argsFormula});
218  }
219  }
220 
221  void runQuery(const std::string &queryString) {
222  auto ctx = std::make_shared<QueryContext>(
224  //| QUERY_FLAG_UNIQUE_SOLUTIONS
225  );
226  try {
227  bool isQueryHandled = false;
228  // make a lookahead if the query string starts with a functor of a registered
229  // higher order command.
230  // NOTE: this is needed because the query parser might not accept formula as argument of a predicate.
231  size_t pos = queryString.find_first_of('(');
232  if (pos != std::string::npos) {
233  auto functor = queryString.substr(0, pos);
234  auto needle = higherOrderCommands_.find(functor);
235  if (needle != higherOrderCommands_.end()) {
236  runHigherOrderCommand(functor, queryString.substr(pos));
237  isQueryHandled = true;
238  }
239  }
240 
241  // parse query
242  if (!isQueryHandled) {
243  auto phi = QueryParser::parse(queryString);
244  auto query = std::make_shared<FormulaQuery>(phi, ctx);
245  if (query->formula()->type() == FormulaType::PREDICATE) {
246  auto p = std::dynamic_pointer_cast<Predicate>(query->formula());
247  auto needle = firstOrderCommands_.find(p->functor()->stringForm());
248  if (needle != firstOrderCommands_.end()) {
249  needle->second.runCommand(p->arguments());
250  isQueryHandled = true;
251  }
252  }
253  if (!isQueryHandled) {
254  runQuery(query);
255  }
256  }
257  }
258  catch (std::exception &e) {
259  std::cout << e.what() << std::endl;
260  }
261  // add query to history
262  history_.append(queryString);
263  history_.save(historyFile_);
264  }
265 
266  void runQuery(const std::shared_ptr<const FormulaQuery> &query) {
267  // evaluate query in hybrid QA system
268  auto resultStream = kb_->submitQuery(query->formula(), query->ctx());
269  auto resultQueue = resultStream->createQueue();
270 
271  numSolutions_ = 0;
272  while (true) {
273  auto nextResult = resultQueue->pop_front();
274 
275  if (nextResult->indicatesEndOfEvaluation()) {
276  break;
277  } else if (nextResult->tokenType() == TokenType::ANSWER_TOKEN) {
278  auto answer = std::static_pointer_cast<const Answer>(nextResult);
279 
280  if (answer->isPositive()) {
281  auto positiveAnswer = std::static_pointer_cast<const AnswerYes>(answer);
282  if (positiveAnswer->substitution()->empty()) {
283  std::cout << "yes." << std::endl;
284  numSolutions_ += 1;
285  break;
286  } else {
287  pushQueryResult(positiveAnswer);
288  numSolutions_ += 1;
289  }
290  } else {
291  std::cout << *answer;
292  numSolutions_ += 1;
293  }
294  }
295  }
296 
297  if (numSolutions_ == 0) {
298  std::cout << "no." << std::endl;
299  }
300  }
301 
302  void enter() {
303  std::cout << std::endl;
304  runQuery(currentQuery_);
305  if (!has_stop_request_) {
306  std::cout << std::endl << PROMPT << std::flush;
307  currentQuery_.clear();
308  cursor_ = 0;
309  }
310  }
311 
312  void insert(char c) {
313  if (cursor_ < currentQuery_.length()) {
314  auto afterInsert = currentQuery_.substr(cursor_);
315  std::cout << c << afterInsert <<
316  "\033[" << afterInsert.length() << "D" <<
317  std::flush;
318  currentQuery_.insert(currentQuery_.begin() + cursor_, c);
319  } else {
320  std::cout << c << std::flush;
321  currentQuery_ += c;
322  }
323  cursor_ += 1;
324  }
325 
326  void insert(const std::string &str) {
327  if (cursor_ < currentQuery_.length()) {
328  auto afterInsert = currentQuery_.substr(cursor_);
329  std::cout << str << afterInsert <<
330  "\033[" << afterInsert.length() << "D" <<
331  std::flush;
332  currentQuery_.insert(currentQuery_.begin() + cursor_, str.begin(), str.end());
333  } else {
334  std::cout << str << std::flush;
335  currentQuery_ += str;
336  }
337  cursor_ += str.length();
338  }
339 
340  void setQuery(const std::string &queryString) {
341  auto oldLength = currentQuery_.length();
342  auto newLength = queryString.length();
343  // move to cursor pos=0 and insert the new query
344  std::cout << "\r" << PROMPT << queryString;
345  // overwrite remainder of old query string with spaces
346  for (auto counter = oldLength; counter > newLength; --counter) {
347  std::cout << ' ';
348  }
349  // move back cursor
350  if (oldLength > newLength) {
351  std::cout << "\033[" << (oldLength - newLength) << "D";
352  }
353  std::cout << std::flush;
354  currentQuery_ = queryString;
355  cursor_ = currentQuery_.length();
356  }
357 
358  void tabulator() {
359  // extract last typed word
360  auto itr = currentQuery_.rbegin();
361  while (itr != currentQuery_.rend() && isalpha(*itr)) { ++itr; }
362  auto lastWord = std::string(itr.base(), currentQuery_.end());
363 
364  // if not the first word, check if preceded by "$ns:"
365  std::optional<std::string> namespaceAlias;
366  if (itr != currentQuery_.rend() && *itr == ':') {
367  // read the namespace alias
368  ++itr;
369  auto aliasEnd = itr;
370  while (itr != currentQuery_.rend() && isalpha(*itr)) { ++itr; }
371  namespaceAlias = std::string(itr.base(), aliasEnd.base());
372  }
373 
374  if (namespaceAlias.has_value()) {
375  autoCompleteLocal(lastWord, namespaceAlias.value());
376  } else {
377  autoCompleteGlobal(lastWord);
378  }
379  }
380 
381  bool autoCompleteCurrentWord(const std::string &word, const std::string_view &completion) {
382  auto mismatch = std::mismatch(word.begin(), word.end(), completion.begin());
383  if (mismatch.first == word.end()) {
384  // insert remainder
385  auto remainder = std::string(mismatch.second, completion.end());
386  insert(remainder);
387  return true;
388  }
389  return false;
390  }
391 
392  bool autoCompleteGlobal(const std::string &word) {
393  std::vector<std::string_view> aliases;
394  if (word.empty()) {
396  } else {
397  aliases = PrefixRegistry::getAliasesWithPrefix(word);
398  }
399 
400  if (aliases.size() == 1) {
401  // only one possible completion
402  if (autoCompleteCurrentWord(word, aliases[0])) {
403  insert(':');
404  return true;
405  }
406  } else if (aliases.size() > 1) {
407  displayOptions(aliases);
408  return true;
409  }
410 
411  return false;
412  }
413 
414  bool autoCompleteLocal(const std::string &word, const std::string &nsAlias) {
415  auto uri = PrefixRegistry::aliasToUri(nsAlias);
416  if (uri.has_value()) {
417  auto partialIRI = uri.value().get() + word;
418  auto propertyOptions = kb_->vocabulary()->getDefinedPropertyNamesWithPrefix(partialIRI);
419  auto classOptions = kb_->vocabulary()->getDefinedClassNamesWithPrefix(partialIRI);
420  size_t namePosition = uri.value().get().length() + 1;
421 
422  // create options array holding only the name of entities.
423  // note that strings are not copied by using string_view
424  std::vector<std::string_view> options(propertyOptions.size() + classOptions.size());
425  uint32_t index = 0;
426  for (auto &iri: propertyOptions) options[index++] = iri.substr(namePosition);
427  for (auto &iri: classOptions) options[index++] = iri.substr(namePosition);
428 
429  if (options.size() == 1) {
430  // only one possible completion
431  if (autoCompleteCurrentWord(word, options[0])) {
432  return true;
433  }
434  } else if (options.size() > 1) {
435  displayOptions(options);
436  return true;
437  }
438  } else {
439  KB_WARN("the namespace alias '{}' is unknown.", nsAlias);
440  }
441  return false;
442  }
443 
444  static std::string getCommonPrefix(const std::vector<std::string_view> &options) {
445  if (options.empty()) return "";
446 
447  std::string_view commonPrefix = options[0];
448  for (const auto &option: options) {
449  auto mismatchPair = std::mismatch(
450  commonPrefix.begin(),
451  commonPrefix.end(),
452  option.begin(),
453  option.end());
454  commonPrefix = std::string_view(
455  commonPrefix.begin(),
456  mismatchPair.first - commonPrefix.begin());
457  }
458  return commonPrefix.data();
459  }
460 
461  void displayOptions(const std::vector<std::string_view> &options) {
462  // auto-complete up to common prefix among options before displaying
463  auto commonPrefix = getCommonPrefix(options);
464  if (!commonPrefix.empty()) {
465  // auto-complete up to common prefix
466  insert(commonPrefix);
467  }
468 
469  std::string optionsStr = "\n";
470  for (const auto &option: options) {
471  optionsStr += std::string(option) + "\n";
472  }
473  // print options to the terminal
474  std::cout << optionsStr << PROMPT << currentQuery_ << std::flush;
475  }
476 
477  void backspace() {
478  if (cursor_ == 0) {
479  return;
480  } else if (cursor_ < currentQuery_.length()) {
481  auto afterDelete = currentQuery_.substr(cursor_);
482  std::cout << '\b' << afterDelete << ' ' <<
483  "\033[" << (afterDelete.length() + 1) << "D" << std::flush;
484  currentQuery_.erase(cursor_ - 1, 1);
485  } else {
486  std::cout << '\b' << ' ' << '\b' << std::flush;
487  currentQuery_.pop_back();
488  }
489  cursor_ -= 1;
490  }
491 
492  void moveToBegin() {
493  if (cursor_ > 0) {
494  std::cout << "\033[" << cursor_ << "D" << std::flush;
495  cursor_ = 0;
496  }
497  }
498 
499  void moveToEnd() {
500  if (cursor_ < currentQuery_.length()) {
501  std::cout << "\033[" << (currentQuery_.length() - cursor_) << "C" << std::flush;
502  cursor_ = currentQuery_.length();
503  }
504  }
505 
506  void moveLeft() {
507  if (cursor_ > 0) {
508  std::cout << "\033[1D" << std::flush;
509  cursor_ -= 1;
510  }
511  }
512 
513  void moveRight() {
514  if (cursor_ < currentQuery_.length()) {
515  std::cout << "\033[1C" << std::flush;
516  cursor_ += 1;
517  }
518  }
519 
520  void moveUp() {
521  history_.nextItem();
522  if (history_.hasSelection()) {
523  setQuery(history_.getSelection());
524  }
525  }
526 
527  void moveDown() {
528  history_.previousItem();
529  setQuery(history_.hasSelection() ? history_.getSelection() : "");
530  }
531 
532  void handleEscapeSequence() {
533  // the escape code \033 is followed by 2-3 more bytes
534  if (getch() == '[') {
535  switch (getch()) {
536  case 'A': // "\033[A" --> UP ARROW
537  moveUp();
538  break;
539  case 'B': // "\033[B" --> DOWN ARROW
540  moveDown();
541  break;
542  case 'C': // "\033[C" --> RIGHT ARROW
543  moveRight();
544  break;
545  case 'D': // "\033[D" --> LEFT_ARROW
546  moveLeft();
547  break;
548  case 'F': // "\033[F" --> END
549  moveToEnd();
550  break;
551  case 'H': // "\033[H" --> POS1
552  moveToBegin();
553  break;
554  }
555  }
556  }
557 
558  bool exitTerminal() {
559  has_stop_request_ = true;
560  return true;
561  }
562 
563  int run() {
564  std::cout << "Welcome to KnowRob." << '\n' <<
565  "For online help and background, visit http://knowrob.org/" << '\n' <<
566  '\n';
567 
568  std::cout << PROMPT << std::flush;
569  while (!has_stop_request_) {
570  const auto c = getch();
571  switch (c) {
572  case -1:
573  break;
574  case 27:
575  handleEscapeSequence();
576  break;
577  case 9:
578  tabulator();
579  break;
580  case 10:
581  enter();
582  break;
583  case 127:
584  backspace();
585  break;
586  default:
587  insert(c);
588  break;
589  }
590  }
591 
592  return EXIT_SUCCESS;
593  }
594 
595 protected:
596  KnowledgeBasePtr kb_;
597  std::atomic<bool> has_stop_request_;
598  int numSolutions_;
599  uint32_t cursor_;
600  std::string currentQuery_;
601  std::string historyFile_;
602  QueryHistory history_;
603  std::map<std::string, TerminalCommand<TermPtr>, std::less<>> firstOrderCommands_;
604  std::map<std::string, TerminalCommand<FormulaPtr>> higherOrderCommands_;
605 };
606 
607 
608 int run(int argc, char **argv) {
609  po::options_description general("General options");
610  general.add_options()
611  ("help", "produce a help message")
612  ("verbose", "print informational messages")
613  ("config-file", po::value<std::string>()->required(), "a configuration file in JSON format")
614  ("version", "output the version number");
615  // Declare an options description instance which will be shown
616  // to the user
617  po::options_description visible("Allowed options");
618  visible.add(general);
619  // parse command line arguments
620  po::variables_map vm;
621  po::store(po::parse_command_line(argc, argv, visible), vm);
622 
623  if (vm.count("help")) {
624  std::cout << visible;
625  return EXIT_SUCCESS;
626  }
627 
628  // read settings
629  boost::property_tree::ptree config;
630  if (vm.count("config-file")) {
631  boost::property_tree::read_json(
632  vm["config-file"].as<std::string>(),
633  config);
634  } else {
635  std::cout << "'config-file' commandline argument is missing" << std::endl;
636  return EXIT_FAILURE;
637  }
638 
639  // configure logging
640  auto log_config = config.get_child_optional("logging");
641  if (log_config) {
642  Logger::loadConfiguration(log_config.value());
643  }
644  // overwrite console logger level (default: prevent messages being printed, only print warnings and errors)
646  vm.count("verbose") ? spdlog::level::debug : spdlog::level::warn);
647 
648  return KnowRobTerminal(config).run();
649 }
650 
651 
652 int main(int argc, char **argv) {
653  InitKnowRob(argc, argv);
654  int status;
655  try {
656  status = run(argc, argv);
657  }
658  catch (std::exception &e) {
659  KB_ERROR("a '{}' exception occurred in main loop: {}.", typeid(e).name(), e.what());
660  status = EXIT_FAILURE;
661  }
662  ShutdownKnowRob();
663  return status;
664 }
#define KB_ERROR
Definition: Logger.h:28
#define KB_WARN
Definition: Logger.h:27
static bool assertStatements(const KnowledgeBasePtr &kb, const std::vector< FormulaPtr > &args)
static void setSinkLevel(SinkType sinkType, spdlog::level::level_enum log_level)
Definition: Logger.cpp:130
static void loadConfiguration(boost::property_tree::ptree &config)
Definition: Logger.cpp:76
static std::vector< std::string_view > getAliasesWithPrefix(std::string_view prefix)
static OptionalStringRef aliasToUri(std::string_view alias)
static FormulaPtr parse(const std::string &queryString)
Definition: QueryParser.cpp:33
FunctionRule & function()
Definition: terms.cpp:140
TermRule & option()
Definition: terms.cpp:110
TermRule & string()
Definition: terms.cpp:63
TermRule & options()
Definition: terms.cpp:114
std::shared_ptr< KnowledgeBase > KnowledgeBasePtr
@ QUERY_FLAG_ALL_SOLUTIONS
Definition: QueryFlag.h:15
void InitKnowRob(int argc, char **argv, bool initPython=true)
Definition: knowrob.cpp:96
std::shared_ptr< const Answer > AnswerPtr
Definition: Answer.h:129
IRIAtomPtr iri(std::string_view ns, std::string_view name)
Definition: IRIAtom.cpp:62
void ShutdownKnowRob()
Definition: knowrob.cpp:123
int main(int argc, char **argv)
Definition: terminal.cpp:652
int run(int argc, char **argv)
Definition: terminal.cpp:608