// Author: Matthew Shiel // Code adapted from https://github.com/kennysong/goeliza/ package eliza import ( "bytes" "fmt" "html/template" "math/rand" "regexp" "strings" "time" ) // InteractiveBot This is the interface to comunicate with the bot type InteractiveBot interface { ReplyTo(statement string) (string, *Personality) } // Chatbot Defines data to be used by a chatbot personality - essentially // Its personality script and execution context type Chatbot struct { Personality *Personality Context *ChatbotContext } // ChatbotContext is the white board structure type ChatbotContext struct { EngineVersion string Session SessionData } // ChatbotInteraction defines a individual question/answer interaction with a caller type ChatbotInteraction struct { Time string `json:"time,omitempty" yaml:"time,omitempty"` Question string `json:"question,omitempty" yaml:"question,omitempty"` PatternGroup string `json:"patternGroup,omitempty" yaml:"patternGroup,omitempty"` Pattern string `json:"pattern,omitempty" yaml:"pattern,omitempty"` Answer string `json:"answer,omitempty" yaml:"answer,omitempty"` } // SessionData defines information about the current bot and its interaction within a specific session type SessionData struct { SessionID string `json:"sessionID" yaml:"sessionID"` StartTime string `json:"startTime,omitempty" yaml:"startTime,omitempty"` User string `json:"user,omitempty" yaml:"user,omitempty"` Bot string `json:"bot,omitempty" yaml:"bot,omitempty"` BotVersion string `json:"botVersion,omitempty" yaml:"botVersion,omitempty"` Conversation []ChatbotInteraction `json:"conversation,omitempty" yaml:"conversation,omitempty"` } // NewBotPersonality is a utility method to create a bot instance from a personality and its context func NewBotPersonality(personality *Personality, context *ChatbotContext) *Chatbot { return &Chatbot{personality, context} } func NewChatbotInteraction(question, patterngroup, answer string) ChatbotInteraction { return ChatbotInteraction{Time: time.Now().UTC().String(), Question: question, PatternGroup: patterngroup, Answer: answer} } func NewChatbotInteractionWithPattern(question, patterngroup, pattern, answer string) ChatbotInteraction { return ChatbotInteraction{Time: time.Now().UTC().String(), Question: question, PatternGroup: patterngroup, Pattern: pattern, Answer: answer} } // ReplyTo will construct a reply for a given statement using ELIZA's rules. func (p *Chatbot) ReplyTo(statement string) ChatbotInteraction { // First, preprocess the statement for more effective matching statement = p.preprocess(statement) // Then, we check if this is a quit statement if p.IsQuitStatement(statement) { return NewChatbotInteraction(statement, "QuitResponses", p.GoodbyeResponse()) } // Next, we try to match the statement to a statement that ELIZA can // recognize, and construct a pre-determined, appropriate response. for _, similarQuestionResponse := range p.Personality.Psychobabble { for _, questionPattern := range similarQuestionResponse.SimilarQuestions { re := regexp.MustCompile(questionPattern) matches := re.FindStringSubmatch(statement) // If the statement matched any recognizable statements. if len(matches) > 0 { // If we matched a regex group in parentheses, get the first match. The matched regex group will match a "fragment" that will form // part of the response, for added realism. var fragment string if len(matches) > 1 { fragment = p.reflect(matches[1]) } // Choose a random appropriate response, and format it with the // fragment, if needed. response := p.randChoice(similarQuestionResponse.Responses) if strings.Contains(response, "%s") { response = fmt.Sprintf(response, fragment) } // fmt.Printf("For Statement \"%s\" got a hit with pattern \"%s\" Responded With \"%s\"\n", statement, pattern, response) return NewChatbotInteractionWithPattern(statement, "Psychobabble", questionPattern, p.replacePlaceHolders(response)) } } } // If no patterns were matched, return a default response. return NewChatbotInteraction(statement, "DefaultResponses", p.replacePlaceHolders(p.randChoice(p.Personality.DefaultResponses))) } // Greetings will return a random introductory sentence for ELIZA. func (p *Chatbot) Greetings() string { return p.randChoice(p.Personality.Introductions) } // GoodbyeResponse will return a random goodbye sentence for ELIZA. func (p *Chatbot) GoodbyeResponse() string { return p.randChoice(p.Personality.Goodbyes) } // IsQuitStatement returns if the statement is a quit statement func (p *Chatbot) IsQuitStatement(statement string) bool { statement = p.preprocess(statement) for _, quitResponse := range p.Personality.QuitResponses { if statement == quitResponse { return true } } return false } // preprocess will do some normalization on a statement for better regex matching func (p *Chatbot) preprocess(statement string) string { statement = strings.TrimRight(statement, "\n.!") statement = strings.ToLower(statement) return statement } // reflect flips a few words in an input fragment (such as "I" -> "you"). func (p *Chatbot) reflect(fragment string) string { words := strings.Split(fragment, " ") for i, word := range words { if reflectedWord, ok := p.Personality.ReflectedWords[word]; ok { words[i] = reflectedWord } } return strings.Join(words, " ") } // randChoice returns a random element in an (string) array. func (p *Chatbot) randChoice(list []string) string { // Added for truly random generation of numbers with seeds if len(list) == 0 { return "" } rand.Seed(time.Now().UnixNano()) randIndex := rand.Intn(len(list)) return list[randIndex] } /** * Replaces conversation templates (names etc) in reply with */ func (p *Chatbot) replacePlaceHolders(answer string) string { var tBuffer bytes.Buffer funcsMap := template.FuncMap{ "dayOfWeek": dayOfWeek, "fullDate": date, "year": year, } rawTemplate := template.New("answer") tmpl := rawTemplate.Funcs(funcsMap) t, _ := tmpl.Parse(answer) t.Execute(&tBuffer, p.Context) return tBuffer.String() }