eliza.go 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  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. // InteractiveBot This is the interface to comunicate with the bot
  14. type InteractiveBot interface {
  15. ReplyTo(statement string) (string, *Personality)
  16. }
  17. // Chatbot Defines data to be used by a chatbot personality - essentially
  18. // Its personality script and execution context
  19. type Chatbot struct {
  20. Personality *Personality
  21. Context *ChatbotContext
  22. }
  23. // ChatbotContext is the white board structure
  24. type ChatbotContext struct {
  25. EngineVersion string
  26. Session SessionData
  27. }
  28. // ChatbotInteraction defines a individual question/answer interaction with a caller
  29. type ChatbotInteraction struct {
  30. Time string `json:"time,omitempty" yaml:"time,omitempty"`
  31. Question string `json:"question,omitempty" yaml:"question,omitempty"`
  32. Answer string `json:"answer,omitempty" yaml:"answer,omitempty"`
  33. }
  34. // SessionData defines information about the current bot and its interaction within a specific session
  35. type SessionData struct {
  36. SessionID string `json:"sessionID" yaml:"sessionID"`
  37. StartTime string `json:"startTime,omitempty" yaml:"startTime,omitempty"`
  38. User string `json:"user,omitempty" yaml:"user,omitempty"`
  39. Bot string `json:"bot,omitempty" yaml:"bot,omitempty"`
  40. BotVersion string `json:"botVersion,omitempty" yaml:"botVersion,omitempty"`
  41. Conversation []ChatbotInteraction `json:"conversation,omitempty" yaml:"conversation,omitempty"`
  42. }
  43. // NewBotPersonality is a utility method to create a bot instance from a personality and its context
  44. func NewBotPersonality(personality *Personality, context *ChatbotContext) *Chatbot {
  45. return &Chatbot{personality, context}
  46. }
  47. // Greetings will return a random introductory sentence for ELIZA.
  48. func (p *Chatbot) Greetings() string {
  49. return p.randChoice(p.Personality.Introductions)
  50. }
  51. // GoodbyeResponse will return a random goodbye sentence for ELIZA.
  52. func (p *Chatbot) GoodbyeResponse() string {
  53. return p.randChoice(p.Personality.Goodbyes)
  54. }
  55. // ReplyTo will construct a reply for a given statement using ELIZA's rules.
  56. func (p *Chatbot) ReplyTo(statement string) string {
  57. // First, preprocess the statement for more effective matching
  58. statement = p.preprocess(statement)
  59. // Then, we check if this is a quit statement
  60. if p.IsQuitStatement(statement) {
  61. return p.GoodbyeResponse()
  62. }
  63. // Next, we try to match the statement to a statement that ELIZA can
  64. // recognize, and construct a pre-determined, appropriate response.
  65. for _, similarQuestionResponse := range p.Personality.Psychobabble {
  66. for _, question := range similarQuestionResponse.SimilarQuestions {
  67. re := regexp.MustCompile(question)
  68. matches := re.FindStringSubmatch(statement)
  69. // If the statement matched any recognizable statements.
  70. if len(matches) > 0 {
  71. // If we matched a regex group in parentheses, get the first match.
  72. // The matched regex group will match a "fragment" that will form
  73. // part of the response, for added realism.
  74. var fragment string
  75. if len(matches) > 1 {
  76. fragment = p.reflect(matches[1])
  77. }
  78. // Choose a random appropriate response, and format it with the
  79. // fragment, if needed.
  80. response := p.randChoice(similarQuestionResponse.Responses)
  81. if strings.Contains(response, "%s") {
  82. response = fmt.Sprintf(response, fragment)
  83. }
  84. // fmt.Printf("For Statement \"%s\" got a hit with pattern \"%s\" Responded With \"%s\"\n", statement, pattern, response)
  85. return p.replacePlaceHolders(response)
  86. }
  87. }
  88. }
  89. // If no patterns were matched, return a default response.
  90. return p.replacePlaceHolders(p.randChoice(p.Personality.DefaultResponses))
  91. }
  92. // IsQuitStatement returns if the statement is a quit statement
  93. func (p *Chatbot) IsQuitStatement(statement string) bool {
  94. statement = p.preprocess(statement)
  95. for _, quitResponse := range p.Personality.QuitResponses {
  96. if statement == quitResponse {
  97. return true
  98. }
  99. }
  100. return false
  101. }
  102. // preprocess will do some normalization on a statement for better regex matching
  103. func (p *Chatbot) preprocess(statement string) string {
  104. statement = strings.TrimRight(statement, "\n.!")
  105. statement = strings.ToLower(statement)
  106. return statement
  107. }
  108. // reflect flips a few words in an input fragment (such as "I" -> "you").
  109. func (p *Chatbot) reflect(fragment string) string {
  110. words := strings.Split(fragment, " ")
  111. for i, word := range words {
  112. if reflectedWord, ok := p.Personality.ReflectedWords[word]; ok {
  113. words[i] = reflectedWord
  114. }
  115. }
  116. return strings.Join(words, " ")
  117. }
  118. // randChoice returns a random element in an (string) array.
  119. func (p *Chatbot) randChoice(list []string) string {
  120. // Added for truly random generation of numbers with seeds
  121. if len(list) == 0 {
  122. return ""
  123. }
  124. rand.Seed(time.Now().UnixNano())
  125. randIndex := rand.Intn(len(list))
  126. return list[randIndex]
  127. }
  128. /**
  129. * Replaces conversation templates (names etc) in reply with
  130. */
  131. func (p *Chatbot) replacePlaceHolders(answer string) string {
  132. var tBuffer bytes.Buffer
  133. t := template.Must(template.New("answer").Parse(answer))
  134. t.Execute(&tBuffer, p.Context)
  135. return tBuffer.String()
  136. }