eliza.go 4.8 KB

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