├── .gitignore ├── LICENSE ├── README.md ├── build.sbt ├── out.xml ├── project ├── build.properties └── plugins.sbt ├── requirements.txt └── src ├── main └── scala │ ├── Main.scala │ ├── bot │ ├── connections │ │ ├── Acquaintances.scala │ │ ├── Attribute.scala │ │ └── Person.scala │ ├── handler │ │ ├── AppliedFunctions.scala │ │ ├── MessageHandler.scala │ │ └── SessionInformation.scala │ ├── learn │ │ ├── BotReply.scala │ │ ├── Message.scala │ │ ├── MessageTemplate.scala │ │ └── RepliesLearner.scala │ └── memory │ │ ├── Characteristic.scala │ │ ├── Trie.scala │ │ ├── TrieLogic │ │ ├── Utils.scala │ │ ├── definition │ │ ├── Definition.scala │ │ ├── Node.scala │ │ └── PartOfSentence.scala │ │ ├── part │ │ └── of │ │ │ └── speech │ │ │ ├── GrammaticalNumber.scala │ │ │ ├── PartOfSpeech.scala │ │ │ └── VerbTense.scala │ │ └── storage │ │ ├── BotStorage.scala │ │ └── Printer.scala │ └── example │ ├── Bot.scala │ └── brain │ ├── BrainFunctions.scala │ ├── Manager.scala │ ├── definitions │ └── Definitions.scala │ └── modules │ ├── Age.scala │ ├── Attributes.scala │ ├── Characteristics.scala │ ├── Greeting.scala │ ├── Job.scala │ └── MasterModule.scala └── test └── scala └── bot ├── handler └── Handler.scala ├── learner └── Learner.scala └── memory └── Memory.scala /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.log 3 | .idea/* 4 | project/target/* 5 | target/* 6 | out/* -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Robert Butacu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ScalaBot - Work in progress 2 | 3 | ScalaBot is a lightweight Scala Library that can be used to create ChatBots. 4 | 5 | Among its features, it includes: 6 | - a wide variety of custom replies for user input 7 | - ability to recognize synonyms or typos made by the person chatting to 8 | - chat history 9 | - details about the person currently talking to 10 | - session-based memory 11 | - previous sessions memory 12 | - ability to recognize certain topics => TBI 13 | 14 | The bot.actors._ module supports the following features: 15 | - ability to learn and lookup messages in a concurrent way => TBI 16 | - ability to provide parallelization for all standard operations => TBI 17 | - ability to split the memory into multiple parts - faster lookup for bigger bots => TBI 18 | 19 | *TBI - to be implemented 20 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | name := "scala-bot" 2 | 3 | version := "0.1" 4 | 5 | scalaVersion := "2.12.4" 6 | scalacOptions += "-Ypartial-unification" 7 | 8 | libraryDependencies ++= { 9 | val akkaVersion = "2.4.12" 10 | Seq( 11 | "org.scala-lang.modules" %% "scala-xml_2.12" % "1.0.5", 12 | "org.scalactic" %% "scalactic" % "3.0.4", 13 | "org.scalatest" %% "scalatest" % "3.0.4" % "test", 14 | "org.scalamock" %% "scalamock" % "4.0.0" % Test, 15 | "com.typesafe.akka" %% "akka-actor" % akkaVersion, 16 | "org.typelevel" %% "cats-core" % "1.5.0", 17 | "org.typelevel" %% "cats-effect" % "1.3.0" 18 | ) 19 | } -------------------------------------------------------------------------------- /out.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | guitar20 5 | 6 | 40 7 | 8 | pc20 9 | 10 | programming20 11 | 12 | programming20 13 | 14 | scala20 15 | scala20 16 | 17 | thesky20 18 | 19 | 20 | 21 | refactoring20 22 | 23 | guitarAndSports50 24 | 25 | 22 26 | 27 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version = 1.0.4 -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robertbutacu/scala-bot/618f12c70530461380ded6f2dfea991943d96941/project/plugins.sbt -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Stages of development for ScalaBot: 2 | Done : 1. return responds to messages 3 | Will use a trie/map to keep in memory a client/bot message exchange. 4 | No regexes at this stage. 5 | 6 | 7 | 2. provide continuity of conversation 8 | Done : For a certain bot answer, the bot might want to expect a certain response 9 | Done : Or send a "disapproval" message otherwise, while still answering to the client's question/wtv. 10 | 11 | DONE - 12 | 3. provide regexes in messages and memory 13 | For a given response, the bot might want to remember certain details of a particular object. 14 | 15 | This will be done using a trie whose nodes are occupied by regexes OR words. 16 | And the final node is a list of possible replies. 17 | 18 | 4. customized responses depending on memory ( consider also including intervals ) 19 | A template of the user will be composed of 20 | List[(partOfSentence, attribNameIfNecessary: Option[Attribute])] 21 | Attribute will be a trait which will be extended to create new attributes. 22 | attribNameIfNecessary will be a regex if set and will become part of the memory. 23 | 24 | The data structure holding the brain will be a Trie composed of 25 | partOfReply will be words - better for scaling rather than having a part of sentence 26 | Example: "This is a sentence" will be trie-rized as this -> is -> a -> sentence, rather than 27 | "this is a sentence" -> . 28 | 29 | Node((partOfReply: Regex, attribName = Option[String]), children: Set[Node], leafs: set[Leaf]) 30 | Leaf(possibleReplies: List[String]) 31 | 32 | In order to have customized replies depending on arguments, there will exist a method called 33 | get() which will receive a list of functions receiving an attribute and a boolean expression, 34 | returning a string in case of true. 35 | 36 | get(f: (a: Atrib) => Boolean): String 37 | 38 | Steps: 39 | DONE - 1. Trie data structure 40 | DONE - 2. Add/Find methods. No need for remove/edit for now. 41 | DONE - 3. Connect it with the learner. 42 | DONE - 4. Provide method to memories attributes. 43 | DONE - 5. Connect it with the handler. 44 | DONE - 6. Provide methods to provide customised messages. 45 | DONE - [Logic Bug] 7. Change trie so it remembers the previous message 46 | and a function that returns a Set[String]. 47 | Done - [Logic ] 8. Add support so that previous bot message can be selected from a set[string] 48 | which is returned by a function. 49 | 50 | FOR NOW, IT WILL RECALL EVENTS FROM THE CURRENT SESSION. 51 | 5. previous sessions' memory - DONE 52 | 53 | One huge file will all the previous conversations' details. 54 | If there is to identify one person from the List, its details will be updated at the end of the convo 55 | and persisted. 56 | 57 | 6. topics 58 | The bot might categorise different topics and have an honest opinion about any of them, or learn more. 59 | For example: 60 | Might know that scala is FP language, that it's awesome, and it's current version is 2.12. 61 | 62 | 7. maybe implement a whole actor system to make everything concurrent - DOING 63 | 64 | 8. Definitions - add definitions to words and create the trie upon that - DONE 65 | - the ideal case would be to store in the trie ONLY the possible definitions from which to choose from 66 | - think its done... ? 67 | 68 | 9. having being given an unknown word, question about it and then learn the word :) 69 | -------------------------------------------------------------------------------- /src/main/scala/Main.scala: -------------------------------------------------------------------------------- 1 | import example.Bot 2 | import cats.instances.all._ 3 | import scala.util.Try 4 | 5 | object Main extends App { 6 | val bot = Bot[Try](10) 7 | 8 | bot.startDemo 9 | 10 | // println(Utils.findReplacements("underage", List("As", "a", "14", "years", "of", "age", "Im", "underage"), Definitions.get())) 11 | } 12 | -------------------------------------------------------------------------------- /src/main/scala/bot/connections/Acquaintances.scala: -------------------------------------------------------------------------------- 1 | package bot.connections 2 | 3 | import java.io.{File, FileNotFoundException} 4 | 5 | import cats.Monad 6 | import example.brain.modules.{AgeAttr, JobAttr, NameAttr, PassionAttr} 7 | import cats.syntax.all._ 8 | import scala.language.{higherKinds, postfixOps} 9 | import scala.util.{Failure, Success, Try} 10 | import scala.xml.XML 11 | 12 | trait Acquaintances[M[_]] { 13 | type CharacteristicName = String 14 | type Weight = String 15 | type Value = String 16 | 17 | def persist(people: List[Person]): M[Unit] 18 | 19 | def forget(person: Map[Attribute, Value]): M[Unit] 20 | 21 | def add(person: Map[Attribute, Value]): M[Unit] 22 | 23 | def remember(): M[Try[List[List[(CharacteristicName, Weight, Value)]]]] 24 | 25 | def tryMatch(person: List[(Attribute, Value)], 26 | minThreshold: Int): M[List[Map[Attribute, Value]]] 27 | } 28 | 29 | object Acquaintances { 30 | implicit def xmlStorage[F[_]](filename: String)(implicit M: Monad[F]): Acquaintances[F] = new Acquaintances[F] { 31 | /** Receiving a list of people traits and a filename, it will store all the information about them in an XML file. 32 | * 33 | * @param people - all the people from all convo that have been persisted previously +- current session 34 | */ 35 | def persist(people: List[Person]): F[Unit] = { 36 | val file = new File(filename) 37 | 38 | if (file.exists()) 39 | file.delete() 40 | 41 | val peopleXml = people map (_.toXml) 42 | 43 | val serialized = 44 | 45 | {peopleXml} 46 | 47 | 48 | XML.save(filename, serialized, "UTF-8", xmlDecl = true, null) 49 | M.pure(()) 50 | } 51 | 52 | /** 53 | * The function is useful when the bot chats with a previously met person and it is discovered, thus it is 54 | * required to update the info about that person if necessary ( he/she might disclose new details ). 55 | * 56 | * @param person - person to forget 57 | * @return - a list of people traits excluding the person 58 | */ 59 | def forget(person: Map[Attribute, Value]): F[Unit] = { 60 | val people: F[List[Map[Attribute, String]]] = M.map(remember()) { 61 | case Success(p) => p.view.map(translate).map(_.flatten.toMap).toList 62 | case Failure(e) => println(s"There seem to be a problem loading up my memory: $e ..."); List.empty 63 | } 64 | 65 | val updatedPeople = M.map(people)(ps => ps.filterNot(_ == person).map(Person)) 66 | 67 | M.flatTap(updatedPeople)(people => persist(people)).map(_ => ()) 68 | } 69 | 70 | 71 | /** At the end of a convo, it might be required for the bot to persist the person he/she just talked to. 72 | * @param person - person to remember 73 | * @return 74 | */ 75 | def add(person: Map[Attribute, Value]): F[Unit] = { 76 | val people: F[List[Map[Attribute, String]]] = M.map(remember()) { 77 | case Success(p) => p.view.map(translate).map(_.flatten.toMap).toList 78 | case Failure(e) => println(s"There seem to be a problem loading up my memory: $e ..."); List.empty 79 | } 80 | 81 | people.flatMap(ps => persist((ps :+ person).map(Person))) 82 | } 83 | 84 | 85 | /** 86 | * Since reflection doesn't work on traits, it is required to do all the "translation" manually using the 87 | * translate() method. This function return a triple of Strings, each representing: 88 | * _1 : Attribute Name 89 | * _2 : Attribute weight 90 | * _3 : Attribute value 91 | * 92 | * @return - a list of lists containing all the information about all the people. 93 | */ 94 | def remember(): F[Try[List[List[(CharacteristicName, Weight, Value)]]]] = { 95 | M.pure { 96 | Try { 97 | val peopleXML = XML.loadFile(filename) 98 | 99 | (peopleXML \\ "person").toList 100 | .view 101 | .map(node => node \\ "attribute") 102 | .map(e => 103 | e.toList.map(n => (n \\ "@type" text: CharacteristicName, n \\ "@weight" text: Weight, n.text: Value))) 104 | .toList 105 | }.orElse(Failure(new FileNotFoundException("Inexisting file!"))) 106 | } 107 | } 108 | 109 | /** 110 | * @param person - current person talking to 111 | * @param minThreshold - a minimum amount of attribute weights sum 112 | * @return - a list of possible matching people 113 | */ 114 | def tryMatch(person: List[(Attribute, Value)], 115 | minThreshold: Int): F[List[Map[Attribute, Value]]] = { 116 | def isMatch(person: List[(Attribute, Value)]): Boolean = 117 | sum(person) >= minThreshold 118 | 119 | def sum(person: List[(Attribute, Value)]): Int = 120 | person.foldLeft(0)((total, curr) => total + curr._1.weight) 121 | 122 | 123 | val peopleXML: F[Try[List[List[(CharacteristicName, Weight, Value)]]]] = remember() 124 | 125 | val people = M.map(peopleXML) { 126 | case Success(p) => p.view.map(translate).map(_.flatten.toMap).toList 127 | case Failure(e) => println(s"There seem to be a problem loading up my memory: $e ..."); List.empty 128 | } 129 | 130 | val initialMatches = M.map(people)(p => p filter (p => person.forall(p.toList.contains))) 131 | 132 | val result = M.map(initialMatches)(matches => matches.filter(p => isMatch(p.toList)) 133 | .sortWith((p1, p2) => sum(p1.toList) > sum(p2.toList))) 134 | 135 | result 136 | } 137 | 138 | 139 | /** The triple represents: 140 | * _1 : Attribute name 141 | * _2 : Attribute weight 142 | * _3 : Attribute value 143 | * 144 | * @param people - a list where every single element represent a person with all their traits 145 | * @return - every item from the list converted to a map of Attribute, String 146 | */ 147 | private def translate(people: List[(String, String, String)]): List[Map[Attribute, String]] = { 148 | val applier: PartialFunction[(String, String, String), Map[Attribute, String]] = { 149 | case ("AgeAttr", weight, ageValue) => Map(Attribute(AgeAttr, weight.toInt) -> ageValue) 150 | case ("NameAttr", weight, nameValue) => Map(Attribute(NameAttr, weight.toInt) -> nameValue) 151 | case ("PassionAttr", weight, passionValue) => Map(Attribute(PassionAttr, weight.toInt) -> passionValue) 152 | case ("Job", weight, jobValue) => Map(Attribute(JobAttr, weight.toInt) -> jobValue) 153 | } 154 | 155 | people collect applier 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/main/scala/bot/connections/Attribute.scala: -------------------------------------------------------------------------------- 1 | package bot.connections 2 | 3 | import bot.memory.Characteristic 4 | 5 | case class Attribute(characteristic: Characteristic, weight: Int) 6 | -------------------------------------------------------------------------------- /src/main/scala/bot/connections/Person.scala: -------------------------------------------------------------------------------- 1 | package bot.connections 2 | 3 | import scala.xml.Elem 4 | 5 | class Trait(attribute: Attribute, value: String) { 6 | def toXml: Elem = 7 | {value} 8 | 9 | } 10 | 11 | case class Person(traits: Map[Attribute, String]) { 12 | val serialized: List[Elem] = traits.toList.map(e => new Trait(e._1, e._2).toXml) 13 | 14 | def toXml: Elem = 15 | 16 | {serialized.map(e => e)} 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/scala/bot/handler/AppliedFunctions.scala: -------------------------------------------------------------------------------- 1 | package bot.handler 2 | 3 | import bot.learn.PossibleReply 4 | 5 | /** 6 | * Since the possible replies and previous messages are all functions which return strings, 7 | * there was a need for a data which hold the actual results 8 | * Slight optimisation for cases where the function is expensive, otherwise there shouldn't make any difference. 9 | * @param previousBotMsgs - ??? 10 | * @param appliedFunctions - ??? 11 | */ 12 | protected[this] case class AppliedFunctions(previousBotMsgs: Set[String], 13 | appliedFunctions: Set[String]){ 14 | def isAnswerToPreviousBotMessage(previousBotMessage: String): Boolean = 15 | previousBotMsgs.contains(previousBotMessage) 16 | 17 | def hasNoPreviousBotMessage: Boolean = previousBotMsgs.isEmpty 18 | } 19 | 20 | object AppliedFunctions { 21 | def toAppliedFunctions(p: PossibleReply) = 22 | AppliedFunctions(p.previousBotMessage.map(_.provideReply()).toSet.flatten, p.possibleReply.flatMap(_.provideReply())) 23 | } -------------------------------------------------------------------------------- /src/main/scala/bot/handler/MessageHandler.scala: -------------------------------------------------------------------------------- 1 | package bot.handler 2 | 3 | import bot.connections.Attribute 4 | import bot.learn.PossibleReply 5 | import bot.memory.Trie 6 | import bot.memory.storage.BotStorage 7 | 8 | import scala.collection.mutable 9 | import scala.util.Random 10 | 11 | trait MessageHandler { 12 | def sessionInformation: SessionInformation 13 | var currentSessionInformation: mutable.Map[Attribute, String] = mutable.Map[Attribute, String]() 14 | 15 | def handle[T](storage: T, 16 | msg: String, 17 | sessionInformation: SessionInformation)(implicit lookup: BotStorage[T]): String = { 18 | val response = lookup.search(storage, msg) 19 | 20 | if (response.possibleReplies.isEmpty) { 21 | sessionInformation.randomUnknownMessageReply 22 | } 23 | else { 24 | currentSessionInformation ++= response.attributesFound 25 | 26 | //just in case it's the first message and there are no previous bot messages 27 | val lastBotMessage = sessionInformation.lastBotMessage.fold("")(_.message) 28 | 29 | provideResponse(response.possibleReplies, lastBotMessage) 30 | } 31 | } 32 | 33 | def isDisapproved(brain: Trie, msg: String): String = { 34 | "" 35 | } 36 | 37 | /** 38 | * First off, all the functions are being applied so that all the replies are known. 39 | * After that, it is tried to find a match for the last bot message. 40 | * In that case, it is provided a reply for that specific case. 41 | * Otherwise, all the possible replies that are dependent on the last message the bot sent are 42 | * disregarded and all the other are flatMapped 43 | * and sent as a parameter to a function which will arbitrarily 44 | * choose a reply. 45 | * 46 | * @param possibleReplies = a set of optional functions 47 | * who return a set of string representing the last message the bot sent, 48 | * and a set of functions returning a string representing possible replies. 49 | * @return a message suitable for the last input the client gave. 50 | */ 51 | private def provideResponse(possibleReplies: Set[PossibleReply], 52 | lastBotMsg: String): String = { 53 | 54 | val appliedFunctions = possibleReplies map AppliedFunctions.toAppliedFunctions 55 | 56 | lazy val noPreviousBotMessageMatches = appliedFunctions 57 | .withFilter(_.hasNoPreviousBotMessage) 58 | .flatMap(_.appliedFunctions) 59 | 60 | appliedFunctions 61 | .find(_.isAnswerToPreviousBotMessage(lastBotMsg)) 62 | .fold(provideReply(noPreviousBotMessageMatches))(reply => provideReply(reply.appliedFunctions)) 63 | } 64 | 65 | def getAttribute(attribute: Attribute): Option[String] = currentSessionInformation.get(attribute) 66 | 67 | private def provideReply(replies: Set[String]): String = 68 | if (replies.isEmpty) sessionInformation.randomUnknownMessageReply 69 | else Random.shuffle(replies).head 70 | 71 | } 72 | -------------------------------------------------------------------------------- /src/main/scala/bot/handler/SessionInformation.scala: -------------------------------------------------------------------------------- 1 | package bot.handler 2 | 3 | import bot.connections.Attribute 4 | import bot.handler.Source.{Bot, Human} 5 | import bot.memory.Trie 6 | import cats.data.NonEmptyList 7 | 8 | import scala.util.Random 9 | 10 | case class SessionInformation(brain: Trie, 11 | unknownMessages: NonEmptyList[String] = NonEmptyList("", List.empty), 12 | disapprovalMessages: Set[String] = Set(""), 13 | knownCharacteristics: Map[Attribute, String] = Map.empty, 14 | log: List[ConversationLine] = List.empty) { 15 | def lastHumanMessage: Option[ConversationLine] = log.find(_.source == Human) 16 | def lastBotMessage: Option[ConversationLine] = log.find(_.source == Bot) 17 | 18 | def randomUnknownMessageReply: String = Random.shuffle(unknownMessages.toList).head 19 | 20 | def addBotMessage(message: String): SessionInformation = { 21 | this.copy(log = this.log.+:(ConversationLine(message, Bot))) 22 | } 23 | 24 | def addHumanMessage(message: String): SessionInformation = { 25 | this.copy(log = this.log.+:(ConversationLine(message, Human))) 26 | } 27 | } 28 | 29 | case class ConversationLine(message: String, source: Source) 30 | 31 | sealed trait Source 32 | 33 | object Source { 34 | case object Bot extends Source 35 | case object Human extends Source 36 | } 37 | -------------------------------------------------------------------------------- /src/main/scala/bot/learn/BotReply.scala: -------------------------------------------------------------------------------- 1 | package bot.learn 2 | 3 | trait BotReply { 4 | def provideReply(): Set[String] 5 | } 6 | -------------------------------------------------------------------------------- /src/main/scala/bot/learn/Message.scala: -------------------------------------------------------------------------------- 1 | package bot.learn 2 | 3 | import bot.connections.Attribute 4 | import scala.util.matching.Regex 5 | 6 | case class Message(pattern: Regex, attribute: Option[Attribute]) 7 | 8 | case class SearchResponses(attributesFound: Map[Attribute, String], 9 | possibleReplies: Set[PossibleReply] = Set().empty) 10 | 11 | case class PossibleReply(previousBotMessage: Option[BotReply], 12 | possibleReply: Set[BotReply]) 13 | 14 | object PossibleReply { 15 | def apply(reply: MessageTemplate): PossibleReply = PossibleReply(reply.humanMessage.previousBotReply, reply.botReplies) 16 | } 17 | -------------------------------------------------------------------------------- /src/main/scala/bot/learn/MessageTemplate.scala: -------------------------------------------------------------------------------- 1 | package bot.learn 2 | 3 | /** 4 | * @param previousBotReply - an optional function that returns a set of strings => 5 | * possible previous messages from the bot 6 | * @param message - human's actual message, composed of multiple words/regexes. 7 | */ 8 | case class HumanMessage(previousBotReply: Option[BotReply], 9 | message: List[Message]) 10 | 11 | /** 12 | * 13 | * @param humanMessage - last message sent by the human, restricted by the last message sent by the bot possibly. 14 | * @param botReplies - a set of functions which return a string 15 | */ 16 | case class MessageTemplate(humanMessage: HumanMessage, 17 | botReplies: Set[BotReply]) 18 | -------------------------------------------------------------------------------- /src/main/scala/bot/learn/RepliesLearner.scala: -------------------------------------------------------------------------------- 1 | package bot.learn 2 | 3 | import bot.memory.Trie 4 | import bot.memory.definition.{Definition, PartOfSentence} 5 | import bot.memory.storage.BotStorage 6 | 7 | object RepliesLearner { 8 | def learn(trie: Trie, 9 | acquired: List[MessageTemplate], 10 | dictionary: Set[Definition])(implicit storer: BotStorage[Trie]): Trie = { 11 | acquired.foldLeft(trie)((t, w) => storer.add(t, toWords(w.humanMessage.message), PossibleReply(w), dictionary)) 12 | } 13 | 14 | /** 15 | * The anonymous function creates a List of Lists of (Regex, Some(Attribute)), 16 | * which will be flattened => a list of words. The list is then filtered to not contain any 17 | * empty words ( "" ). 18 | * 19 | * @param message - message to be added in the trie that needs to be split 20 | * @return a list of words that could be either a string with no attr set, 21 | * or a regex with an attribute 22 | */ 23 | private def toWords(message: List[Message]): List[PartOfSentence] = 24 | message flatMap { w => 25 | w.pattern.toString 26 | .split(" ") 27 | .toList 28 | .withFilter(_ != "") 29 | .map(p => PartOfSentence(p.r, w.attribute)) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/scala/bot/memory/Characteristic.scala: -------------------------------------------------------------------------------- 1 | package bot.memory 2 | 3 | trait Characteristic 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/main/scala/bot/memory/Trie.scala: -------------------------------------------------------------------------------- 1 | package bot.memory 2 | 3 | import bot.learn.PossibleReply 4 | import bot.memory.definition.{Definition, Node, PartOfSentence, SimpleWord} 5 | 6 | /** 7 | * 8 | * @param information - current node, containing a word( or a regex), and an optional attribute 9 | * which is to be stored in map if necessary 10 | * Regex - current word , part of the message 11 | * Option[Attribute] - in case it is something to be remembered 12 | * @param children - next parts of a possible message from a client. 13 | * @param replies - replies to the message starting from the top of the trie all the way down to curr. 14 | * Option[() => Set[String] - a function returning a possible last bot message 15 | * Set[f: Any => Set[String] ] - a set of functions returning a set of possible replies 16 | * => It is done that way so that the replies are generated dynamically, 17 | * depending on the already existing/non-existing attributes. 18 | **/ 19 | case class Trie(information: Node, 20 | children: Set[Trie] = Set.empty, 21 | replies: Set[PossibleReply] = Set.empty) 22 | 23 | object Trie { 24 | def apply(node: PartOfSentence, 25 | sentence: List[PartOfSentence], 26 | dictionary: Set[Definition]): Trie = Trie(Node(node, sentence, dictionary)) 27 | 28 | def empty: Trie = Trie(SimpleWord("".r)) 29 | } 30 | 31 | -------------------------------------------------------------------------------- /src/main/scala/bot/memory/TrieLogic: -------------------------------------------------------------------------------- 1 | SOOO 2 | this is gonna be a big mess to implement 3 | 4 | Given a dictionary ( a list of definitions ), and a list of sentences ( a list of SentenceWord 5 | = (Regex, Option[Attribute]), 6 | the function returns a trie looking like: 7 | Set[NodeWord] 8 | 9 | A function, given a dictionary and a word, will return a list of NodeWord representing the possible replacements. 10 | -------------------------------------------------------------------------------- /src/main/scala/bot/memory/Utils.scala: -------------------------------------------------------------------------------- 1 | package bot.memory 2 | 3 | import bot.memory.definition.{Definition, Synonym, Word} 4 | 5 | object Utils { 6 | def findReplacements(word: String, 7 | sentence: List[String], 8 | dictionary: Set[Definition]): Set[Word] = { 9 | // For the input word, find out how many synonyms there are by: 10 | // checking for each synonym how many context words there are 11 | // a synonym can go either way: definition => synonym, as well as synonym => definition 12 | val cleanedSentence = sentence.filterNot(_ == word) 13 | 14 | def contextCountForSynonym(synonym: Synonym): Int = cleanedSentence.count(s => synonym.contextWords.exists(w => w.matches(s))) 15 | def findMatchingDefinition: Set[Definition] = dictionary.filter(d => d.matches(word)) 16 | def wordAsSynonym: Set[Word] = dictionary.withFilter(d => d.isSynonym(word, sentence)).map(_.word) 17 | 18 | //TODO this is pretty dumb, as any synonym with at least 1 context word gets picked up 19 | val allSynonyms = findMatchingDefinition.flatMap(d => d.getMatchingSynonyms(word, sentence)) 20 | .withFilter(s => contextCountForSynonym(s) > 0) 21 | .map(_.definition) 22 | 23 | allSynonyms ++ wordAsSynonym 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/scala/bot/memory/definition/Definition.scala: -------------------------------------------------------------------------------- 1 | package bot.memory.definition 2 | 3 | import bot.memory.part.of.speech.{Irrelevant, PartOfSpeech} 4 | 5 | case class Definition(word: Word, 6 | synonyms: Set[Synonym] = Set.empty) { 7 | // Inverse relationship where the word is found as a synonym for the current definition, 8 | // and thus the current definition for the word is valid 9 | def isSynonym(word: String, sentence: List[String]): Boolean = { 10 | val matchingSynonyms = synonyms.filter(s => s.definition.word == word) 11 | 12 | matchingSynonyms.exists { 13 | s => 14 | s.contextWords.count(w => sentence.exists(s => w.matches(s))) > 0 15 | } 16 | } 17 | 18 | // Direct relationship where the synonyms from a definition are extracted using the current word and the context 19 | def getMatchingSynonyms(word: String, sentence: List[String]): Set[Synonym] = { 20 | synonyms.filter { s => 21 | sentence.contains(s.definition.word) || sentence.exists(w => s.contextWords.exists(ww => ww.matches(w))) 22 | } 23 | } 24 | 25 | def matches(w: String): Boolean = word.matches(w) 26 | } 27 | 28 | case class Synonym(definition: Word, 29 | contextWords: Set[Word] = Set.empty) 30 | 31 | case class Word(word: String, 32 | otherAcceptableForms: Set[AcceptableForm] = Set.empty, 33 | partOfSpeech: PartOfSpeech = Irrelevant) { 34 | // TODO this should be @tailrec 35 | def matches(other: String): Boolean = { 36 | word == other || otherAcceptableForms.exists(w => w.word == other) 37 | } 38 | 39 | def getWords: Set[AcceptableForm] = otherAcceptableForms + AcceptableForm(this.word) 40 | } 41 | 42 | object Definition { 43 | def getWords(definition: Definition): Set[Word] = definition.synonyms.map(_.definition) + definition.word 44 | 45 | 46 | def find(word: String, 47 | definitions: List[Definition]): Boolean = 48 | definitions exists (_.word.word == word) 49 | 50 | 51 | def addSynonyms(word: Definition, 52 | synonyms: Set[Synonym]): Definition = 53 | word.copy(synonyms = word.synonyms ++ synonyms) 54 | } 55 | 56 | case class AcceptableForm(word: String) -------------------------------------------------------------------------------- /src/main/scala/bot/memory/definition/Node.scala: -------------------------------------------------------------------------------- 1 | package bot.memory.definition 2 | 3 | import bot.connections.Attribute 4 | import bot.memory.part.of.speech.{Irrelevant, PartOfSpeech} 5 | import bot.memory.{Utils, definition} 6 | 7 | import scala.util.matching.Regex 8 | 9 | sealed trait Node { 10 | def informationMatches(p: PartOfSentence): Boolean 11 | 12 | def wordMatches(w: String): Boolean 13 | 14 | def addToAttributes(value: String, 15 | information: Map[Attribute, String]): Map[Attribute, String] 16 | } 17 | 18 | object Node { 19 | def apply(p: PartOfSentence, sentence: List[PartOfSentence], dictionary: Set[Definition]): Node = { 20 | def constructSimpleNode(): SimpleWord = { 21 | val word = p.word 22 | val otherAcceptableForms = dictionary.find(_.word.matches(p.word.toString())).fold(Set.empty[AcceptableForm])(w => w.word.otherAcceptableForms) 23 | // TODO inconsistency between List[PartOfSentence] and findReplacements 24 | val synonyms = Utils.findReplacements(p.word.toString(), sentence.map(_.word.toString()), dictionary) 25 | 26 | definition.SimpleWord(word, otherAcceptableForms, Irrelevant, synonyms) 27 | } 28 | 29 | p.attribute match { 30 | case None => constructSimpleNode() 31 | case Some(a) => WithAttributeInformation(p.word, a) 32 | } 33 | } 34 | } 35 | 36 | 37 | //TODO maybe word and otherAcceptableForms should be regexes??? More flexibility 38 | case class SimpleWord(word: Regex = "".r, 39 | otherAcceptableForms: Set[AcceptableForm] = Set.empty, 40 | partOfSpeech: PartOfSpeech = Irrelevant, 41 | synonyms: Set[Word] = Set.empty) extends Node { 42 | override def informationMatches(p: PartOfSentence): Boolean = { 43 | def anyMatch(word: String): Boolean = 44 | this.word.toString() == word || otherAcceptableForms.contains(AcceptableForm(word)) || synonyms.exists(w => w.matches(word)) 45 | 46 | p.attribute match { 47 | case None => anyMatch(p.word.toString()) 48 | case Some(_) => false 49 | } 50 | } 51 | 52 | override def addToAttributes(p: String, information: Map[Attribute, String]): Map[Attribute, String] = information 53 | 54 | override def wordMatches(w: String): Boolean = { 55 | word.pattern.matcher(w).matches() || otherAcceptableForms.contains(AcceptableForm(w)) || synonyms.exists(s => s.matches(w)) 56 | } 57 | } 58 | 59 | case class WithAttributeInformation(word: Regex = "".r, 60 | attribute: Attribute) extends Node { 61 | override def informationMatches(p: PartOfSentence): Boolean = { 62 | p.attribute match { 63 | case None => false 64 | case Some(attr) => this.word.toString() == p.word.toString() && attribute == attr 65 | } 66 | } 67 | 68 | override def addToAttributes(p: String, 69 | information: Map[Attribute, String]): Map[Attribute, String] = 70 | information + (this.attribute -> p) 71 | 72 | override def wordMatches(w: String): Boolean = word.pattern.matcher(w).matches() 73 | } 74 | 75 | 76 | -------------------------------------------------------------------------------- /src/main/scala/bot/memory/definition/PartOfSentence.scala: -------------------------------------------------------------------------------- 1 | package bot.memory.definition 2 | 3 | import bot.connections.Attribute 4 | 5 | import scala.util.matching.Regex 6 | 7 | case class PartOfSentence(word: Regex = "".r, 8 | attribute: Option[Attribute] = None) { 9 | def matchesWord(toMatch: Word): Boolean = 10 | this.word.pattern.pattern().matches(toMatch.word) || toMatch.otherAcceptableForms.exists(af => this.word.pattern.pattern().matches(af.word)) 11 | } -------------------------------------------------------------------------------- /src/main/scala/bot/memory/part/of/speech/GrammaticalNumber.scala: -------------------------------------------------------------------------------- 1 | package bot.memory.part.of.speech 2 | 3 | sealed trait GrammaticalNumber 4 | 5 | case object Singular extends GrammaticalNumber 6 | case object Plural extends GrammaticalNumber 7 | -------------------------------------------------------------------------------- /src/main/scala/bot/memory/part/of/speech/PartOfSpeech.scala: -------------------------------------------------------------------------------- 1 | package bot.memory.part.of.speech 2 | 3 | sealed trait PartOfSpeech 4 | 5 | case class Noun(grammaticalNumber: GrammaticalNumber = Singular) extends PartOfSpeech 6 | case class Adjective(grammaticalNumber: GrammaticalNumber = Singular) extends PartOfSpeech 7 | case class Pronoun(grammaticalNumber: GrammaticalNumber = Singular) extends PartOfSpeech 8 | 9 | case object Adverb extends PartOfSpeech 10 | case class Verb(tense: VerbTense = Present) extends PartOfSpeech 11 | 12 | case object Preposition extends PartOfSpeech 13 | case object Conjunction extends PartOfSpeech 14 | case object Interjection extends PartOfSpeech 15 | case object Irrelevant extends PartOfSpeech 16 | 17 | -------------------------------------------------------------------------------- /src/main/scala/bot/memory/part/of/speech/VerbTense.scala: -------------------------------------------------------------------------------- 1 | package bot.memory.part.of.speech 2 | 3 | sealed trait VerbTense 4 | 5 | case object Past extends VerbTense 6 | case object Present extends VerbTense 7 | case object Future extends VerbTense -------------------------------------------------------------------------------- /src/main/scala/bot/memory/storage/BotStorage.scala: -------------------------------------------------------------------------------- 1 | package bot.memory.storage 2 | 3 | import bot.connections.Attribute 4 | import bot.learn.{PossibleReply, SearchResponses} 5 | import bot.memory.Trie 6 | import bot.memory.definition.{Definition, PartOfSentence} 7 | 8 | import scala.annotation.tailrec 9 | 10 | trait BotStorage[T] { 11 | def search(storage: T, message: String): SearchResponses 12 | def add(storage: T, message: List[PartOfSentence], replies: PossibleReply, dictionary: Set[Definition]): T 13 | } 14 | 15 | object BotStorage { 16 | implicit def trieLookup: BotStorage[Trie] = new BotStorage[Trie] { 17 | /** 18 | * The algorithm describes the search of a message in a trie, by parsing every word and matching it, 19 | * thus returning a Set of possible replies depending on bot's previous replies. 20 | * 21 | * @param message - the sentence that is to be found, or not 22 | * @return - returns a Set of (previousMessageFromBot, Set[functions returning possible replies]), 23 | * from which another algorithm will pick the best choice. 24 | */ 25 | override def search(storage: Trie, message: String): SearchResponses = { 26 | @tailrec 27 | def go(message: List[String], 28 | trie: Trie, 29 | attributes: Map[Attribute, String]): SearchResponses = { 30 | if (message.isEmpty) 31 | SearchResponses(attributes, trie.replies) //completely ran over all the words 32 | else { 33 | val head = message.head 34 | 35 | trie.children.find(t => t.information.wordMatches(head)) match { 36 | case None => SearchResponses(attributes) //word wasn't found in the trie 37 | case Some(nextNode) => go(message.tail, nextNode, nextNode.information.addToAttributes(head, attributes)) 38 | } 39 | } 40 | } 41 | 42 | go(toPartsOfSentence(message), storage, Map[Attribute, String]().empty) 43 | } 44 | 45 | /** 46 | * For a current Trie, the algorithm returns another trie with the message added. 47 | * The pattern matching does the following: 48 | * 1. in case of None => current word isn't in the set => add it, and call the function with the same node 49 | * 2. in case of Some(next) => next node has been found => remove it from the Set, since its gonna be different  50 | * it would double stack otherwise 51 | */ 52 | override final def add(storage: Trie, 53 | message: List[PartOfSentence], 54 | replies: PossibleReply, 55 | dictionary: Set[Definition]): Trie = { 56 | def go(curr: Trie, words: List[PartOfSentence]): Trie = { 57 | def ifEmpty(currWord: PartOfSentence): Trie = { 58 | val newTrie = go(Trie(currWord, message, dictionary), words.tail) 59 | curr.copy(children = curr.children + newTrie) 60 | } 61 | 62 | def addToExisting(trie: Trie, rest: List[PartOfSentence]): Trie = { 63 | val updatedTrie = go(trie, words.tail) 64 | curr.copy(children = curr.children - trie + updatedTrie) 65 | } 66 | 67 | if (words.isEmpty) 68 | this.addReplies(curr, replies) 69 | else { 70 | val currWord = words.head 71 | val next = curr.children.find(t => t.information.informationMatches(currWord)) 72 | 73 | next.fold(ifEmpty(currWord))(t => addToExisting(t, words.tail)) 74 | } 75 | } 76 | 77 | go(storage, message) 78 | } 79 | 80 | /** 81 | * As part of the "storing" journey, this is at the very end - where the replies have to be added. 82 | * 83 | * @param replies - replies that are to be added 84 | * @return - new leafs which also contain the new replies 85 | * There are 2 cases: 86 | * 1. when the replies depend on a previous bot message ( or lack of ) also stored: 87 | * the replies are appended to the already existing replies. 88 | * 2. when they aren't stored at all: 89 | * they are registered as new replies with their attribute. 90 | */ 91 | private def addReplies(trie: Trie, replies: PossibleReply): Trie = 92 | trie.replies 93 | .find(_.previousBotMessage == replies.previousBotMessage) 94 | .fold(trie.copy(replies = trie.replies + replies))(rep => updateReplies(trie, rep, replies)) 95 | 96 | private def updateReplies(trie: Trie, 97 | to: PossibleReply, 98 | newReplies: PossibleReply): Trie = 99 | trie.copy(replies = trie.replies - to + to.copy(possibleReply = to.possibleReply ++ newReplies.possibleReply)) 100 | 101 | private def toPartsOfSentence(msg: String): List[String] = 102 | msg.split(' ') 103 | .toList 104 | .filter(!_.isEmpty) } 105 | } 106 | -------------------------------------------------------------------------------- /src/main/scala/bot/memory/storage/Printer.scala: -------------------------------------------------------------------------------- 1 | package bot.memory.storage 2 | 3 | import bot.memory.Trie 4 | 5 | trait Printer[T] { 6 | def print(t: T): Unit 7 | } 8 | 9 | object Printer { 10 | implicit def triePrinter: Printer[Trie] = (t: Trie) => { 11 | def go(trie: Trie, tabs: Int): Unit = { 12 | println("\t" * tabs + "Node " + trie.information + " ") 13 | trie.replies.foreach(r => println("\t" * (tabs + 1) + " Leaf " + r)) 14 | trie.children.foreach(t => go(t, tabs + 1)) 15 | } 16 | 17 | go(t, 0) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/scala/example/Bot.scala: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | import bot.connections.{Acquaintances, Attribute} 4 | import bot.handler.{MessageHandler, SessionInformation} 5 | import cats.Monad 6 | import cats.data.NonEmptyList 7 | import cats.effect.IO 8 | import cats.syntax.all._ 9 | import example.brain.Manager 10 | 11 | import scala.annotation.tailrec 12 | import scala.language.higherKinds 13 | 14 | case class Bot[F[_]](minKnowledgeThreshold: Int) extends Manager with MessageHandler { 15 | type Matcher = (Option[Map[Attribute, String]], SessionInformation) 16 | 17 | implicit def acquaintances(implicit M: Monad[F]): Acquaintances[F] = Acquaintances.xmlStorage[F]("out.xml") 18 | 19 | def startDemo(implicit M: Monad[F]): Unit = { 20 | def go(sessionInformation: SessionInformation): Unit = { 21 | val message = scala.io.StdIn.readLine() 22 | 23 | val sessionInformationWithHumanMessage = sessionInformation.addHumanMessage(message) 24 | 25 | message match { 26 | case "QUIT" => acquaintances.add(currentSessionInformation.toMap) 27 | case "Do you remember me?" => 28 | val possibleMatches = acquaintances.tryMatch(currentSessionInformation.toList, minKnowledgeThreshold) 29 | 30 | possibleMatches 31 | .map(pm => matcher(pm, sessionInformationWithHumanMessage)) 32 | .map { 33 | case (None, si) => go(si) 34 | case (Some(p), si) => 35 | currentSessionInformation = currentSessionInformation.empty ++ p 36 | acquaintances.forget(p).map(_ => go(si)) 37 | } 38 | case _ => 39 | val updatedSessionInformation = sessionInformation.addBotMessage(handle(masterBrain, message, sessionInformationWithHumanMessage)) 40 | updatedSessionInformation.lastBotMessage.foreach(cl => println(cl.message)) 41 | 42 | go(updatedSessionInformation) 43 | } 44 | } 45 | 46 | go(sessionInformation) 47 | } 48 | 49 | //TODO this is rather tricky - have maybe a separate brain module which deals with remembering people ...? More generic this way 50 | @tailrec 51 | final def matcher(people: List[Map[Attribute, String]], 52 | sessionInformation: SessionInformation): Matcher = { 53 | if (people.isEmpty) { 54 | val response = "Sorry, I do not seem to remember you." 55 | println(response) 56 | (None, sessionInformation.addBotMessage(response)) 57 | } 58 | else { 59 | val botMsg = "Does this represent you: " + people.head 60 | .filterNot(currentSessionInformation.toList.contains) 61 | .maxBy(_._1.weight)._2 62 | println(botMsg) 63 | 64 | val userMsg = scala.io.StdIn.readLine() 65 | 66 | if (userMsg == "Yes") { 67 | println("Ah, welcome back!") 68 | (Some(people.head), sessionInformation.addHumanMessage(userMsg).addBotMessage(botMsg)) 69 | } 70 | else 71 | matcher(people.tail, sessionInformation.addHumanMessage(userMsg).addBotMessage(botMsg)) 72 | } 73 | } 74 | 75 | override def sessionInformation: SessionInformation = 76 | SessionInformation(masterBrain, 77 | NonEmptyList("Not familiar with this", List.empty), 78 | Set("", "", "Changed the subject...")) 79 | } 80 | -------------------------------------------------------------------------------- /src/main/scala/example/brain/BrainFunctions.scala: -------------------------------------------------------------------------------- 1 | package example.brain 2 | 3 | import bot.handler.MessageHandler 4 | import bot.learn.BotReply 5 | import example.brain.modules.Attributes 6 | 7 | trait BrainFunctions extends MessageHandler with Attributes { 8 | def ageReply(): BotReply = new BotReply { 9 | override def provideReply(): Set[String] = { 10 | getAttribute(age) match { 11 | case None => Set("Unknown age", "You havent told me your age") 12 | case Some(a) => provideReplies(a.toInt) 13 | } 14 | } 15 | } 16 | 17 | private def provideReplies(age: Int): Set[String] = 18 | age match { 19 | case _ if age > 50 => Set("quite old", "old afff") 20 | case _ if age > 24 => Set("you're an adult", "children?") 21 | case _ if age > 18 => Set("programming", "is", "fun") 22 | case _ if age > 0 && age < 18 => Set("Underage", "Minor") 23 | case _ if age < 0 => Set(s"""It appears your age is $age . What a lie. Tell me your real age please.""") 24 | case _ => Set("got me there", "lost") 25 | } 26 | 27 | def passionReply(): BotReply = new BotReply { 28 | override def provideReply(): Set[String] = { 29 | getAttribute(passion) match { 30 | case None => Set("No passions") 31 | case Some(pas) => Set(s"""Passionate about $pas""") 32 | } 33 | } 34 | } 35 | 36 | def passionReplies(): BotReply = new BotReply { 37 | override def provideReply(): Set[String] = { 38 | getAttribute(passion) match { 39 | case None => Set("You're not passionate about anything") 40 | case Some(p) => Set(s"""You're passionate about $p""") 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/scala/example/brain/Manager.scala: -------------------------------------------------------------------------------- 1 | package example.brain 2 | 3 | import bot.learn.RepliesLearner._ 4 | import bot.memory.Trie 5 | import bot.memory.definition.SimpleWord 6 | import example.brain.definitions.Definitions 7 | import example.brain.modules.MasterModule 8 | 9 | trait Manager extends MasterModule { 10 | val masterBrain: Trie = learn(Trie(SimpleWord("".r)), List(jobs, ages, greetings).flatten, Definitions.get()) 11 | } 12 | -------------------------------------------------------------------------------- /src/main/scala/example/brain/definitions/Definitions.scala: -------------------------------------------------------------------------------- 1 | package example.brain.definitions 2 | 3 | import bot.memory.definition.{AcceptableForm, Definition, Synonym, Word} 4 | 5 | import scala.language.implicitConversions 6 | import scala.util.matching.Regex 7 | 8 | object Definitions { 9 | 10 | def get(): Set[Definition] = { 11 | 12 | implicit def convertString(s: String): Regex = s.r 13 | implicit def convertDefinitionToWord(d: Definition): Word = d.word 14 | implicit def convertDefinition(d: Definition): Synonym = Synonym(d) 15 | implicit def convertToWord(d: String): Word = Word(d) 16 | implicit def convertToAcceptableForm(s: String): AcceptableForm = AcceptableForm(s) 17 | 18 | val underage: Definition = Definition(Word("underage")) 19 | val minor: Definition = Definition(Word("minor", Set("Minor", "mic"))) 20 | val adolescent: Definition = Definition(Word("adolescent")) 21 | 22 | val old: Definition = Definition(Word("old")) 23 | val ageOld: Definition = Definition(Word("age-old"), Set(old)) 24 | 25 | val passionate: Definition = Definition(Word("passionate")) 26 | val ardent: Definition = Definition(Word("ardent")) 27 | val keen: Definition = Definition(Word("keen")) 28 | 29 | val greetings: Definition = Definition(Word("greetings")) 30 | val hi: Definition = Definition(Word("hi")) 31 | val hello: Definition = Definition(Word("hello")) 32 | val whatsup: Definition = Definition(Word("hello")) 33 | 34 | Set(Definition.addSynonyms(underage, Set(Synonym(minor, Set(old, ageOld)))), 35 | Definition.addSynonyms(minor, Set(Synonym(underage, Set(old, ageOld)))), 36 | Definition.addSynonyms(adolescent, Set(Synonym(underage, Set(old, ageOld)))), 37 | Definition.addSynonyms(old, Set(Synonym(ageOld, Set(Word("years"))))), 38 | Definition.addSynonyms(ageOld, Set(old)), 39 | Definition.addSynonyms(passionate, Set(ardent, keen)), 40 | Definition.addSynonyms(ardent, Set(passionate, keen)), 41 | Definition.addSynonyms(keen, Set(passionate, ardent)), 42 | Definition.addSynonyms(greetings, Set(hi, hello, whatsup)), 43 | Definition.addSynonyms(hi, Set(greetings, hello, whatsup)), 44 | Definition.addSynonyms(hello, Set(greetings, hi, whatsup)), 45 | Definition.addSynonyms(whatsup, Set(greetings, hi, hello)) 46 | ) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/scala/example/brain/modules/Age.scala: -------------------------------------------------------------------------------- 1 | package example.brain.modules 2 | 3 | import bot.learn.{HumanMessage, Message, MessageTemplate} 4 | import example.brain.BrainFunctions 5 | 6 | 7 | trait Age extends BrainFunctions with Attributes { 8 | val ages: List[MessageTemplate] = List( 9 | MessageTemplate( 10 | HumanMessage(None, 11 | List(Message("Im ".r, None), 12 | Message("[0-9]+".r, Some(age)), 13 | Message(" years old".r, None))), 14 | Set(ageReply()) 15 | ), 16 | MessageTemplate( 17 | HumanMessage(None, 18 | List(Message("Im passionate about".r, None), 19 | Message("[a-zA-Z]+".r, Some(passion)))), 20 | Set(passionReply()) 21 | ), 22 | MessageTemplate( 23 | HumanMessage(Some(passionReply()), 24 | List(Message("What am i passionate about".r, None))), 25 | Set(passionReplies()) 26 | ) 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /src/main/scala/example/brain/modules/Attributes.scala: -------------------------------------------------------------------------------- 1 | package example.brain.modules 2 | 3 | import bot.connections.Attribute 4 | 5 | 6 | trait Attributes { 7 | val age = Attribute(AgeAttr, 10) 8 | val name = Attribute(NameAttr, 10) 9 | val passion = Attribute(PassionAttr, 15) 10 | val job = Attribute(JobAttr, 5) 11 | } 12 | -------------------------------------------------------------------------------- /src/main/scala/example/brain/modules/Characteristics.scala: -------------------------------------------------------------------------------- 1 | package example.brain.modules 2 | 3 | import bot.memory.Characteristic 4 | 5 | case object NameAttr extends Characteristic 6 | case object AgeAttr extends Characteristic 7 | case object PassionAttr extends Characteristic 8 | case object JobAttr extends Characteristic -------------------------------------------------------------------------------- /src/main/scala/example/brain/modules/Greeting.scala: -------------------------------------------------------------------------------- 1 | package example.brain.modules 2 | 3 | import bot.learn.{HumanMessage, Message, MessageTemplate} 4 | import example.brain.BrainFunctions 5 | 6 | trait Greeting extends BrainFunctions { 7 | val greetings: List[MessageTemplate] = List( 8 | MessageTemplate(HumanMessage(None, List(Message("Greetings".r, None))), Set(ageReply)) 9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /src/main/scala/example/brain/modules/Job.scala: -------------------------------------------------------------------------------- 1 | package example.brain.modules 2 | 3 | import bot.learn.{HumanMessage, Message, MessageTemplate} 4 | import example.brain.BrainFunctions 5 | 6 | trait Job extends BrainFunctions { 7 | val jobs: List[MessageTemplate] = List( 8 | MessageTemplate(HumanMessage(None, List(Message("I'm a programmer".r, None))), Set(passionReply())), 9 | MessageTemplate(HumanMessage(None, List(Message("I dont have a job".r, None))), Set(ageReply())) 10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /src/main/scala/example/brain/modules/MasterModule.scala: -------------------------------------------------------------------------------- 1 | package example.brain.modules 2 | 3 | trait MasterModule extends Age 4 | with Greeting 5 | with Job { 6 | } 7 | -------------------------------------------------------------------------------- /src/test/scala/bot/handler/Handler.scala: -------------------------------------------------------------------------------- 1 | package bot.handler 2 | 3 | import org.scalatest.FlatSpec 4 | 5 | class Handler extends FlatSpec { 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/test/scala/bot/learner/Learner.scala: -------------------------------------------------------------------------------- 1 | package bot.learner 2 | 3 | import bot.handler.MessageHandler 4 | import example.brain.modules.Attributes 5 | import org.scalatest.FlatSpec 6 | 7 | class Learner extends FlatSpec with Attributes { 8 | def ageReply(): Set[String] = { 9 | Set("Ok", "Test") 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/test/scala/bot/memory/Memory.scala: -------------------------------------------------------------------------------- 1 | package bot.memory 2 | 3 | import org.scalatest.FlatSpec 4 | 5 | class Memory extends FlatSpec { 6 | 7 | } 8 | --------------------------------------------------------------------------------