eliza.go 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. // Author: Matthew Shiel
  2. // Code adapted from https://github.com/kennysong/goeliza/
  3. package eliza
  4. import (
  5. "bytes"
  6. "fmt"
  7. "html/template"
  8. "math/rand"
  9. "regexp"
  10. "strings"
  11. "time"
  12. )
  13. // NewBotPersonality is a utility method to create a bot instance from a personality and its context
  14. func NewBotPersonality(personality *Personality, context *ChatbotContext) *Chatbot {
  15. return &Chatbot{personality, context}
  16. }
  17. func NewChatbotInteraction(question, patterngroup, rawanswer, answer string) ChatbotInteraction {
  18. return ChatbotInteraction{Time: time.Now().UTC().String(), Question: question, PatternGroup: patterngroup, RawAnswer: rawanswer, Answer: answer}
  19. }
  20. func NewChatbotInteractionWithPattern(question, patterngroup, pattern, rawanswer, answer string) ChatbotInteraction {
  21. return ChatbotInteraction{Time: time.Now().UTC().String(), Question: question, PatternGroup: patterngroup, Pattern: pattern, RawAnswer: rawanswer, Answer: answer}
  22. }
  23. // ReplyTo will construct a reply for a given statement using ELIZA's rules.
  24. func (p *Chatbot) ReplyTo(statement string) ChatbotInteraction {
  25. // First, preprocess the statement for more effective matching
  26. statement = p.preprocess(statement)
  27. var rawResponse string
  28. // Then, we check if this is a quit statement
  29. if p.IsQuitStatement(statement) {
  30. rawResponse = p.GoodbyeResponse()
  31. return NewChatbotInteraction(statement, "QuitResponses", rawResponse, rawResponse)
  32. }
  33. // Next, we try to match the statement to a statement that ELIZA can
  34. // recognize, and construct a pre-determined, appropriate response.
  35. for _, similarQuestionResponse := range p.Personality.Psychobabble {
  36. for _, questionPattern := range similarQuestionResponse.SimilarQuestions {
  37. re := regexp.MustCompile(questionPattern)
  38. matches := re.FindStringSubmatch(statement)
  39. // If the statement matched any recognizable statements.
  40. if len(matches) > 0 {
  41. // If we matched a regex group in parentheses, get the first match. The matched regex group will match a "fragment" that will form
  42. // part of the response, for added realism.
  43. var fragment string
  44. if len(matches) > 1 {
  45. fragment = p.reflect(matches[1])
  46. }
  47. // Choose a random appropriate response, and format it with the
  48. // fragment, if needed.
  49. response := p.randChoice(similarQuestionResponse.Responses)
  50. if strings.Contains(response, "%s") {
  51. response = fmt.Sprintf(response, fragment)
  52. }
  53. // fmt.Printf("For Statement \"%s\" got a hit with pattern \"%s\" Responded With \"%s\"\n", statement, pattern, response)
  54. return NewChatbotInteractionWithPattern(statement, "Psychobabble", questionPattern, response, p.replacePlaceHolders(response))
  55. }
  56. }
  57. }
  58. // If no patterns were matched, return a default response.
  59. rawResponse = p.randChoice(p.Personality.DefaultResponses)
  60. return NewChatbotInteraction(statement, "DefaultResponses", rawResponse, p.replacePlaceHolders(rawResponse))
  61. }
  62. // Greetings will return a random introductory sentence for ELIZA.
  63. func (p *Chatbot) Greetings() string {
  64. return p.randChoice(p.Personality.Introductions)
  65. }
  66. // GoodbyeResponse will return a random goodbye sentence for ELIZA.
  67. func (p *Chatbot) GoodbyeResponse() string {
  68. return p.randChoice(p.Personality.Goodbyes)
  69. }
  70. // IsQuitStatement returns if the statement is a quit statement
  71. func (p *Chatbot) IsQuitStatement(statement string) bool {
  72. statement = p.preprocess(statement)
  73. for _, quitResponse := range p.Personality.QuitResponses {
  74. if statement == quitResponse {
  75. return true
  76. }
  77. }
  78. return false
  79. }
  80. // preprocess will do some normalization on a statement for better regex matching
  81. func (p *Chatbot) preprocess(statement string) string {
  82. statement = strings.TrimRight(statement, "\n.!")
  83. statement = strings.ToLower(statement)
  84. return statement
  85. }
  86. // reflect flips a few words in an input fragment (such as "I" -> "you").
  87. func (p *Chatbot) reflect(fragment string) string {
  88. words := strings.Split(fragment, " ")
  89. for i, word := range words {
  90. if reflectedWord, ok := p.Personality.ReflectedWords[word]; ok {
  91. words[i] = reflectedWord
  92. }
  93. }
  94. return strings.Join(words, " ")
  95. }
  96. // randChoice returns a random element in an (string) array.
  97. func (p *Chatbot) randChoice(list []string) string {
  98. // Added for truly random generation of numbers with seeds
  99. if len(list) == 0 {
  100. return ""
  101. }
  102. rand.Seed(time.Now().UnixNano())
  103. maxAttempts := 2 * len(list)
  104. // try looking for an unspoken response at random which has not been used recently
  105. // 1st hash previous responses
  106. previousResponseHash := make(map[uint32]bool)
  107. for _, r := range p.Context.Session.Conversation {
  108. previousResponseHash[hash(r.RawAnswer)] = true
  109. }
  110. var candidate string
  111. for i := 1; i <= maxAttempts; i++ {
  112. randIndex := rand.Intn(len(list))
  113. candidate = list[randIndex]
  114. h := hash(candidate)
  115. _, known := previousResponseHash[h]
  116. // if not known - use it
  117. // fmt.Println(randIndex, known, candidate)
  118. if !known {
  119. return candidate
  120. }
  121. }
  122. // if here then use default
  123. return candidate
  124. }
  125. /**
  126. * Replaces conversation templates (names etc) in reply with
  127. */
  128. func (p *Chatbot) replacePlaceHolders(answer string) string {
  129. var tBuffer bytes.Buffer
  130. funcsMap := template.FuncMap{
  131. "dayOfWeek": dayOfWeek,
  132. "fullDate": date,
  133. "year": year,
  134. }
  135. rawTemplate := template.New("answer")
  136. tmpl := rawTemplate.Funcs(funcsMap)
  137. t, _ := tmpl.Parse(answer)
  138. t.Execute(&tBuffer, p.Context)
  139. return tBuffer.String()
  140. }