//***********************************************************
// 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 "TalkEngine.h"
#include
#include
#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(tstring s);
#endif
using namespace std;
void TalkBot::UpdateStartChar(tstring& str)
{
if (!makeLowercase)
return;
size_t l = str.length();
if (l)
{
//Answers starting with ' ' must remain unchanged.
if (str[0] == _T(' '))
{
str = str.substr(1);
return;
}
for (size_t i = 0; i < l; i++)
{
TCHAR cl = (TCHAR)CharLower((LPTSTR)(void*)(long)str[i]);
TCHAR cu = (TCHAR)CharUpper((LPTSTR)(void*)(long)str[i]);
if (i != l - 1)
{
//Do not react to BLONDE ANSWERS
TCHAR ncl = (TCHAR)CharLower((LPTSTR)(void*)(long)str[i + 1]);
TCHAR ncu = (TCHAR)CharUpper((LPTSTR)(void*)(long)str[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;
}
tstring TalkBot::GetInitMessage(void* contact)
{
ContactData* d = contactDatas->GetData(contact);
tstring s = d->initial.GetString();
contactDatas->PutData(contact);
return s;
}
tstring TalkBot::ReplaceAliases(const tstring &message)
{
const TCHAR dividers[] = _T(" \t\n\r,./?\\|;:'\"~!#^&*()_-+=[{]}—\1");
tstring sentence = message;
tstring result;
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--)
{
tstring item = sentence.substr(i, j);
if (mind.GetData()->smiles.find(item)
!= mind.GetData()->smiles.end())
{
sm[i] = item;
sentence.replace(i, j, _T("\1"));
break;
}
}
}
len = (int)sentence.length();
bool hadQuestionSigns = false;
int it = 0;
while (it != len)
{
while (it != len && _tcschr(dividers, sentence[it]))
{
if (sentence[it] == _T('?'))
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 && !_tcschr(dividers, sentence[it]))
it++;
if (it == len || sentence[it] != _T('-'))
break;
//If we have-a-word-with-minus, we shouldn't split it
if (_tcschr(dividers, sentence[it + 1]))
break;
it += 2;
}
tstring 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;
}
tstring TalkBot::AllReplies(const tstring &incomingMessage, ContactData* contactData, Level &maxValue, std::multimap &mm)
{
tstring 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, _T("(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()) != _T(""))
{
//Part 4
if (FindExact(contactData, res, mind.GetData()->widelyUsed, res)) //widelyUsed
{
#ifdef DEBUG_PREFIXES
mm.insert(make_pair(BEST, _T("(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, _T("(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 = _T("");
FindByKeywords(contactData, keywords, res/*, ures*/, isQuestion); //keywords
if (res != _T(""))
{
#ifdef DEBUG_PREFIXES
mm.insert(make_pair(LOOKSLIKE, _T("(keywords) ")+res));
#else
mm.insert(make_pair(LOOKSLIKE, res));
#endif
if (maxValue > LOOKSLIKE)
maxValue = LOOKSLIKE;
}
/* if (ures != _T(""))
{
#ifdef DEBUG_PREFIXES
mm.insert(make_pair(LOOKSLIKE2, _T("(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, _T("(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, _T("(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, _T("(failure) ") + res));
#else
mm.insert(make_pair(FAIL, res));
#endif
if (maxValue > FAIL)
maxValue = FAIL;
}
}
return tstring();
}
TalkBot::MessageInfo* TalkBot::Reply(void* contact, tstring incomingMessage, bool saveChoice)
{
TCHAR* str = new TCHAR[incomingMessage.length()+1];
_tcscpy(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;
tstring res = v[rand() % v.size()];
#ifdef DEBUG_PREFIXES
info = new MessageInfo(incomingMessage, _T("(repeat_norm) ") + res);
#else
info = new MessageInfo(incomingMessage, res);
#endif
}
else
#ifdef DEBUG_PREFIXES
info = new MessageInfo(incomingMessage, _T("(repeat_silence)"));
#else
info = new MessageInfo(incomingMessage, _T(""));
#endif
if (saveChoice)
RecordAnswer(contactData, *info);
contactDatas->PutData(contact);
return info;
}
multimap mm;
Level maxValue = NOTHING;
tstring res = AllReplies(incomingMessage, contactData, maxValue, mm);
if (!res.empty())
{
UpdateStartChar(res);
#ifdef DEBUG_PREFIXES
MessageInfo *info = new MessageInfo(incomingMessage, _T("(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, _T("(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
tstring 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 tstring &incomingMessage,
const multimap& map, tstring& res)
{
int max = (int)map.count(incomingMessage);
if (!max)
{
TCHAR c = incomingMessage[incomingMessage.length() - 1];
if (c != _T('?') && c != _T('.') && c != _T('!'))
return FindExact(contactData, incomingMessage + _T('.'), 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(void* 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, tstring& res)
{
if (!ch.GetContainer().size())
return false;
res = ch.GetString();
return true;
}
void TalkBot::SplitSectences(const tstring &incomingMessage, vector& vec)
{
//FIXME: (THINK ABOUT IT:-))these chars not always mark the end of sentence.
const TCHAR symbols[] = _T(".?!");
int it = 0, len = (int)incomingMessage.length();
while (it != len)
{
while (it != len && _istspace(incomingMessage[it]))
it++;
int start = it;
while (it != len)
{
if (_tcschr(symbols, incomingMessage[it++]))
{
//Test for a :-! smile
if (it > 2 && incomingMessage[it-1] == _T('!')
&& incomingMessage[it-2] == _T('-')
&& incomingMessage[it-3] == _T(':'))
continue;
while (it != len && _tcschr(symbols, incomingMessage[it]))
it++;
break;
}
}
vec.insert(vec.end(), incomingMessage.substr(start, it - start));
}
}
#ifdef _DEBUG
tstring LevelToStr(TalkBot::Level target)
{
tstring lev;
switch (target)
{
case TalkBot::BEST: lev = _T("BEST(0)"); break;
case TalkBot::LOOKSLIKE: lev = _T("LOOKSLIKE(1)"); break;
case TalkBot::BAD: lev = _T("BAD(2)"); break;
case TalkBot::FAIL: lev = _T("FAIL(3)"); break;
case TalkBot::NOTHING: lev = _T("NOTHING(4)"); break;
}
return lev;
}
#endif
tstring TalkBot::ChooseResult(ContactData* contactData, Level maxValue, const multimap &mm)
{
#ifdef DEBUG_SHOW_VARIANTS
AddBotMessage(_T(">>Availabe:"));
for (multimap::iterator it = mm.begin(); it != mm.end(); it++)
AddBotMessage(LevelToStr((*it).first) + _T(": ") + (*it).second);
AddBotMessage(_T(">>Result:"));
#endif
if (maxValue == NOTHING)
return _T("");
Level target = maxValue;
int num = (int)mm.count(target);
/* if (!num)
{
target = maxValue;
num = mm.count(target);
}*/
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
tstring lev = LevelToStr(target);
return lev + _T(": ") + contactData->chooser.Choose();
#else
return contactData->chooser.Choose();
#endif
}
void TalkBot::FindByKeywords(ContactData* contactData, const vector &keywords, tstring& res/*, tstring& 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((tstring)(*it).first + _T(": - ") + (*it).second, prio);
#else
contactData->chooser.AddChoice((*it).second, prio);
#endif
}
res = contactData->chooser.Choose();
}
bool TalkBot::FindByOthers(ContactData* contactData, const vector &otherwords, tstring& 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((tstring)(*it).first + _T(": - ") + (*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(tstring sentence, vector& keywords,
vector& otherwords, bool& isQuestion)
{
const TCHAR dividers[] = _T(" \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--)
{
tstring item = sentence.substr(i, j);
if (mind.GetData()->smiles.find(item)
!= mind.GetData()->smiles.end())
{
sm[i] = item;
sentence.replace(i, j, _T(" "));
break;
}
}
}
len = (int)sentence.length();
bool hadQuestionSigns = false;
int it = 0;
while (it != len)
{
while (it != len && _tcschr(dividers, sentence[it]))
{
if (sentence[it] == _T('?'))
hadQuestionSigns = true;
map::iterator smit;
if (_istspace(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 && !_tcschr(dividers, sentence[it]))
it++;
if (it == len || sentence[it] != _T('-'))
break;
//If we have-a-word-with-minus, we shouldn't split it
if (_tcschr(dividers, sentence[it + 1]))
break;
it += 2;
}
tstring 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;
}