//*********************************************************** // Copyright © 2008 Valentin Pavlyuchenko // // This file is part of Boltun. // // Boltun is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 2 of the License, or // (at your option) any later version. // // Boltun is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Boltun. If not, see . // //*********************************************************** #include "../stdafx.h" #ifdef _DEBUG //#define DEBUG_PREFIXES //#define DEBUG_SHOW_LEVEL //#define DEBUG_SHOW_VARIANTS //#define DEBUG_SHOW_SOLUTION_REASON #endif //Enabling next define will make a bot more stupid: //#define EXCLUDE_SPECIAL_WORDS #ifdef DEBUG_SHOW_VARIANTS extern void AddBotMessage(wstring s); #endif using namespace std; void TalkBot::UpdateStartChar(wstring &str) { if (!makeLowercase) return; size_t l = str.length(); if (l) { // Answers starting with ' ' must remain unchanged. if (str[0] == ' ') { str = str.substr(1); return; } wchar_t *strl = NEWWSTR_ALLOCA(str.c_str()), *stru = NEWWSTR_ALLOCA(str.c_str()); CharLower(strl); CharUpper(stru); for (size_t i = 0; i < l; i++) { wchar_t cl = strl[i]; wchar_t cu = stru[i]; if (i != l - 1) { // Do not react to BLONDE ANSWERS wchar_t ncl = strl[i+1]; wchar_t ncu = stru[i+1]; if (ncl != ncu && str[i+1] == ncu) break; } if (cl != cu) { str[i] = cl; break; } } } } TalkBot::TalkBot(const Mind& goodMind) :mind(goodMind), beSilent(false), makeLowercase(false), understandAlways(false) { contactDatas = new PerContactData(mind); } TalkBot::~TalkBot() { delete contactDatas; } wstring TalkBot::GetInitMessage(MCONTACT contact) { ContactData* d = contactDatas->GetData(contact); wstring s = d->initial.GetString(); contactDatas->PutData(contact); return s; } wstring TalkBot::ReplaceAliases(const wstring &message) { const wchar_t dividers[] = L" \t\n\r,./?\\|;:'\"~!#^&*()_-+=[{]}—\1"; wstring sentence = message; wstring result; map sm; //Find smiles for (size_t i = 0; i < sentence.length() - 1; i++) { unsigned max = (int)(sentence.length() - i); if (max > mind.GetData()->maxSmileLen) max = mind.GetData()->maxSmileLen; for (unsigned j = max; j > 0; j--) { wstring item = sentence.substr(i, j); if (mind.GetData()->smiles.find(item) != mind.GetData()->smiles.end()) { sm[i] = item; sentence.replace(i, j, L"\1"); break; } } } int len = (int)sentence.length(); bool hadQuestionSigns = false; int it = 0; while (it != len) { while (it != len && wcschr(dividers, sentence[it])) { if (sentence[it] == '?') hadQuestionSigns = true; map::iterator smit; if (sentence[it] == '\1') { smit = sm.find(it); result.append((*smit).second); } else result.push_back(sentence[it]); it++; } if (it == len) break; int start = it; while (true) { while (it != len && !wcschr(dividers, sentence[it])) it++; if (it == len || sentence[it] != '-') break; //If we have-a-word-with-minus, we shouldn't split it if (wcschr(dividers, sentence[it + 1])) break; it += 2; } wstring str = sentence.substr(start, it - start); map::const_iterator al = mind.GetData()->aliases.find(str); if (al != mind.GetData()->aliases.end()) result.append((*al).second); else result.append(str); } return result; } wstring TalkBot::AllReplies(const wstring &incomingMessage, ContactData *contactData, Level &maxValue, std::multimap &mm) { wstring res; //Part 1 if (FindExact(contactData, incomingMessage, mind.GetData()->widelyUsed, res)) //widelyUsed { return res; } //Part 2 if (FindExact(contactData, incomingMessage, mind.GetData()->study, res)) //study { #ifdef DEBUG_PREFIXES mm.insert(make_pair(LOOKSLIKE, L"(study_all) " + res)); #else mm.insert(make_pair(LOOKSLIKE, res)); #endif maxValue = LOOKSLIKE; } //Part 3 vector sentences; SplitSectences(incomingMessage, sentences); ValueChooser<> ch(sentences, true); //Using random order of sentences. while ((res = ch.GetString()) != L"") { //Part 4 if (FindExact(contactData, res, mind.GetData()->widelyUsed, res)) //widelyUsed { #ifdef DEBUG_PREFIXES mm.insert(make_pair(BEST, L"(widelyused_sent) " + res)); #else mm.insert(make_pair(BEST, res)); #endif if (maxValue > BEST) maxValue = BEST; } //Part 5 if (FindExact(contactData, res, mind.GetData()->study, res)) //study { #ifdef DEBUG_PREFIXES mm.insert(make_pair(LOOKSLIKE, L"(study_sent) " + res)); #else mm.insert(make_pair(LOOKSLIKE, res)); #endif if (maxValue > LOOKSLIKE) maxValue = LOOKSLIKE; } //Part 6 vector keywords, otherwords; bool isQuestion; SplitAndSortWords(res, keywords, otherwords, isQuestion); //Part 7, 8 res = L""; FindByKeywords(contactData, keywords, res/*, ures*/, isQuestion); //keywords if (res != L"") { #ifdef DEBUG_PREFIXES mm.insert(make_pair(LOOKSLIKE, L"(keywords) " + res)); #else mm.insert(make_pair(LOOKSLIKE, res)); #endif if (maxValue > LOOKSLIKE) maxValue = LOOKSLIKE; } /* if (ures != L"") { #ifdef DEBUG_PREFIXES mm.insert(make_pair(LOOKSLIKE2, L"(keywords_unstrict) "+ures)); #else mm.insert(make_pair(LOOKSLIKE2, ures)); #endif if (maxValue > LOOKSLIKE2) maxValue = LOOKSLIKE2; }*/ //Part 9 if (FindByOthers(contactData, otherwords, res, isQuestion)) //specialEscapes { #ifdef DEBUG_PREFIXES mm.insert(make_pair(BAD, L"(otherwords) " + res)); #else mm.insert(make_pair(BAD, res)); #endif if (maxValue > BAD) maxValue = BAD; } } if (!beSilent) { //Part 10 if (FindAny(contactData->escape, res)) //escape { #ifdef DEBUG_PREFIXES mm.insert(make_pair(FAIL, L"(escape) " + res)); #else mm.insert(make_pair(FAIL, res)); #endif if (maxValue > FAIL) maxValue = FAIL; } //Part 11 if (!understandAlways && FindAny(contactData->failure, res)) //failure { #ifdef DEBUG_PREFIXES mm.insert(make_pair(FAIL, L"(failure) " + res)); #else mm.insert(make_pair(FAIL, res)); #endif if (maxValue > FAIL) maxValue = FAIL; } } return wstring(); } TalkBot::MessageInfo* TalkBot::Reply(MCONTACT contact, wstring incomingMessage, bool saveChoice) { wchar_t* str = new wchar_t[incomingMessage.length() + 1]; mir_wstrcpy(str, incomingMessage.c_str()); CharLower(str); incomingMessage = str; delete[] str; ContactData *contactData = contactDatas->GetData(contact); if (incomingMessage == contactData->lastMessage && GetTickCount() < contactData->lastMessageTime + 30 * 60 * 1000) { MessageInfo *info; //only 2-3 repeats if (contactData->repeatCount < 2 || contactData->repeatCount == 2 && (rand() % 2)) { const vector& v = mind.GetData()->repeats; wstring res = v[rand() % v.size()]; #ifdef DEBUG_PREFIXES info = new MessageInfo(incomingMessage, L"(repeat_norm) " + res); #else info = new MessageInfo(incomingMessage, res); #endif } else #ifdef DEBUG_PREFIXES info = new MessageInfo(incomingMessage, L"(repeat_silence)"); #else info = new MessageInfo(incomingMessage, L""); #endif if (saveChoice) RecordAnswer(contactData, *info); contactDatas->PutData(contact); return info; } multimap mm; Level maxValue = NOTHING; wstring res = AllReplies(incomingMessage, contactData, maxValue, mm); if (!res.empty()) { UpdateStartChar(res); #ifdef DEBUG_PREFIXES MessageInfo *info = new MessageInfo(incomingMessage, L"(widelyused_all) " + res); #else MessageInfo *info = new MessageInfo(incomingMessage, res); #endif if (saveChoice) RecordAnswer(contactData, *info); contactDatas->PutData(contact); return info; } incomingMessage = ReplaceAliases(incomingMessage); res = AllReplies(incomingMessage, contactData, maxValue, mm); if (!res.empty()) { UpdateStartChar(res); #ifdef DEBUG_PREFIXES MessageInfo *info = new MessageInfo(incomingMessage, L"(widelyused_all) " + res); #else MessageInfo *info = new MessageInfo(incomingMessage, res); #endif if (saveChoice) RecordAnswer(contactData, *info); contactDatas->PutData(contact); return info; } //Also does Part 12 wstring final = ChooseResult(contactData, maxValue, mm); MessageInfo *info = new MessageInfo(incomingMessage, final); UpdateStartChar(final); if (saveChoice) RecordAnswer(contactData, *info); contactDatas->PutData(contact); return info; } bool TalkBot::FindExact(ContactData *contactData, const wstring &incomingMessage, const multimap &map, wstring &res) { int max = (int)map.count(incomingMessage); if (!max) { wchar_t c = incomingMessage[incomingMessage.length() - 1]; if (c != '?' && c != '.' && c != '!') return FindExact(contactData, incomingMessage + L'.', map, res); return false; } pair range = map.equal_range(incomingMessage); for (mm_cit it = range.first; it != range.second; ++it) contactData->chooser.AddChoice((*it).second); res = contactData->chooser.Choose(); return true; } void TalkBot::AnswerGiven(MCONTACT contact, const TalkBot::MessageInfo& info) { ContactData *contactData = contactDatas->GetData(contact); RecordAnswer(contactData, info); contactDatas->PutData(contact); } void TalkBot::RecordAnswer(ContactData *contactData, const TalkBot::MessageInfo& info) { contactData->chooser.SaveChoice(info.Answer); if (contactData->lastMessage == info.Question) contactData->repeatCount++; else contactData->repeatCount = 0; contactData->lastMessageTime = GetTickCount(); contactData->lastMessage = info.Question; } bool TalkBot::FindAny(ValueChooser<> &ch, wstring& res) { if (!ch.GetContainer().size()) return false; res = ch.GetString(); return true; } void TalkBot::SplitSectences(const wstring &incomingMessage, vector& vec) { //FIXME: (THINK ABOUT IT:-))these chars not always mark the end of sentence. const wchar_t symbols[] = L".?!"; int it = 0, len = (int)incomingMessage.length(); while (it != len) { while (it != len && iswspace(incomingMessage[it])) it++; int start = it; while (it != len) { if (wcschr(symbols, incomingMessage[it++])) { //Test for a :-! smile if (it > 2 && incomingMessage[it - 1] == '!' && incomingMessage[it - 2] == '-' && incomingMessage[it - 3] == ':') continue; while (it != len && wcschr(symbols, incomingMessage[it])) it++; break; } } vec.insert(vec.end(), incomingMessage.substr(start, it - start)); } } #ifdef _DEBUG wstring LevelToStr(TalkBot::Level target) { wstring lev; switch (target) { case TalkBot::BEST: lev = L"BEST(0)"; break; case TalkBot::LOOKSLIKE: lev = L"LOOKSLIKE(1)"; break; case TalkBot::BAD: lev = L"BAD(2)"; break; case TalkBot::FAIL: lev = L"FAIL(3)"; break; case TalkBot::NOTHING: lev = L"NOTHING(4)"; break; } return lev; } #endif wstring TalkBot::ChooseResult(ContactData *contactData, Level maxValue, const multimap &mm) { #ifdef DEBUG_SHOW_VARIANTS AddBotMessage(L">>Availabe:"); for (multimap::iterator it = mm.begin(); it != mm.end(); it++) AddBotMessage(LevelToStr((*it).first) + L": " + (*it).second); AddBotMessage(L">>Result:"); #endif if (maxValue == NOTHING) return L""; Level target = maxValue; typedef multimap::const_iterator lt_cit; pair range = mm.equal_range(target); for (lt_cit it = range.first; it != range.second; ++it) contactData->chooser.AddChoice((*it).second); #ifdef DEBUG_SHOW_LEVEL wstring lev = LevelToStr(target); return lev + L": " + contactData->chooser.Choose(); #else return contactData->chooser.Choose(); #endif } void TalkBot::FindByKeywords(ContactData *contactData, const vector &keywords, wstring& res/*, wstring& ures*/, bool isQuestion) { if (keywords.size() == 0) return; const multimap &keys = isQuestion ? mind.GetData()->qkeywords : mind.GetData()->keywords; for (multimap::const_iterator it = keys.begin(); it != keys.end(); ++it) { float prio; if ((*it).first.MatchesAll(keywords/*, strict*/, prio)) #ifdef DEBUG_SHOW_SOLUTION_REASON contactData->chooser.AddChoice((wstring)(*it).first + L": - " + (*it).second, prio); #else contactData->chooser.AddChoice((*it).second, prio); #endif } res = contactData->chooser.Choose(); } bool TalkBot::FindByOthers(ContactData *contactData, const vector &otherwords, wstring& res, bool isQuestion) { //vector results; const multimap &specs = isQuestion ? mind.GetData()->qspecialEscapes : mind.GetData()->specialEscapes; for (multimap::const_iterator it = specs.begin(); it != specs.end(); ++it) if ((*it).first.MatchesAny(otherwords)) { #ifdef DEBUG_SHOW_SOLUTION_REASON contactData->chooser.AddChoice((wstring)(*it).first + L": - " + (*it).second); #else contactData->chooser.AddChoice((*it).second); #endif } res = contactData->chooser.Choose(); if (res.empty()) return false; return true; } const Mind& TalkBot::GetMind() const { return mind; } void TalkBot::SplitAndSortWords(wstring sentence, vector& keywords, vector& otherwords, bool& isQuestion) { const wchar_t dividers[] = L" \t\n\r,./?\\|;:'\"~!#^&*()_-+=[{]}—"; int len = (int)sentence.length(); vector words; map sm; //Find smiles for (size_t i = 0; i < sentence.length() - 1; i++) { unsigned max = (int)(sentence.length() - i); if (max > mind.GetData()->maxSmileLen) max = mind.GetData()->maxSmileLen; for (unsigned j = max; j > 0; j--) { wstring item = sentence.substr(i, j); if (mind.GetData()->smiles.find(item) != mind.GetData()->smiles.end()) { sm[i] = item; sentence.replace(i, j, L" "); break; } } } len = (int)sentence.length(); bool hadQuestionSigns = false; for (int it = 0; it != len;) { while (it != len && wcschr(dividers, sentence[it])) { if (sentence[it] == '?') hadQuestionSigns = true; map::iterator smit; if (iswspace(sentence[it]) && (smit = sm.find(it)) != sm.end()) words.push_back((*smit).second); it++; } if (it == len) break; hadQuestionSigns = false; int start = it; while (true) { while (it != len && !wcschr(dividers, sentence[it])) it++; if (it == len || sentence[it] != '-') break; // If we have-a-word-with-minus, we shouldn't split it if (wcschr(dividers, sentence[it + 1])) break; it += 2; } wstring str = sentence.substr(start, it - start); words.push_back(str); } isQuestion = hadQuestionSigns; for (vector::iterator it = words.begin(); it != words.end(); ++it) { if (!isQuestion) if (mind.GetData()->question.find(*it) != mind.GetData()->question.end()) isQuestion = true; if (mind.GetData()->special.find(*it) != mind.GetData()->special.end()) otherwords.push_back(*it); #ifdef EXCLUDE_SPECIAL_WORDS else #endif keywords.push_back(*it); } } void TalkBot::SetSilent(const bool isSilent) { beSilent = isSilent; } void TalkBot::SetLowercase(const bool isLowercase) { makeLowercase = isLowercase; } void TalkBot::SetUnderstandAlways(const bool _understandAlways) { this->understandAlways = _understandAlways; }