├── .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 |
--------------------------------------------------------------------------------