-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy patheliza.go
165 lines (138 loc) · 4.86 KB
/
eliza.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
// Package eliza provides an implementation of the Eliza chatbot
package eliza
import (
"errors"
"fmt"
"os"
"regexp"
"sort"
"strings"
"github.com/necrophonic/log"
)
func init() {
log.InitFromString(os.Getenv("ELIZA_LOG_LEVEL"))
}
// Analyse performs psychoanalysis on the given sentance
func Analyse(this []byte) ([]byte, error) {
response, err := AnalyseString(string(this))
if err != nil {
return nil, err
}
return []byte(response), nil
}
// AnalyseString performs psychoanalysis on the given sentance string
func AnalyseString(this string) (string, error) {
// nb. These steps aren't necessarily the most efficient as some things
// could be combined - but they're laid out like this to more clearly
// document the alogrithm.
// Firstly split sentance into words separated by spaces
words := split(strings.Trim(this, "\n"))
// Second, perform pre-substitution in the word list
words = preSubstitute(words)
// Third, make a list of all keywords in the input words sorted into
// descending weight
keywords := identifyKeywords(words)
// Fourth, run through each keyword and match against decomposition
// sequences until a match is found. If a match is found then process
// the reassembly for that word and move to post processing, otherwise
// move to the next keyword.
// This will also post process any post-sub words.
words, err := processKeywords(keywords, words)
if err != nil {
return "", err
}
return strings.Join(words, " "), nil
}
func split(said string) []string {
words := strings.Split(said, " ")
for i, w := range words {
words[i] = strings.ToLower(strings.Trim(w, ".!?"))
}
return words
}
func preSubstitute(words []string) []string {
log.Trace("Running pre substitutions")
for i, w := range words {
if sub, ok := pre[w]; ok {
words[i] = sub
}
}
return words
}
func postSubstitute(words []string) []string {
log.Trace("Running post substitutions")
for i, w := range words {
if sub, ok := post[w]; ok {
words[i] = sub
}
}
return words
}
func chooseAssembly(d *decomp) string {
// Grab the next asseumbly and then
// increment (and loop if needed) the counter to
// call the next asseumbly next time around.
chosen := d.Assemblies[d.AssemblyNext]
d.AssemblyNext = (d.AssemblyNext + 1) % uint8(len(d.Assemblies))
if strings.HasPrefix(chosen, "goto ") {
// It's a jump command rather than an actual reassembly
// Find where to jump to and then retrieve the proper key
g := strings.TrimPrefix(chosen, "goto ")
chosen = chooseAssembly(keywordMap[g].Decompositions[0])
}
return chosen
}
// Sort keys by weight - implements sort.Interface
type byWeight []keyword
func (a byWeight) Len() int { return len(a) }
func (a byWeight) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a byWeight) Less(i, j int) bool { return a[i].Weight > a[j].Weight }
func identifyKeywords(words []string) (keys []keyword) {
log.Debug("Attempting to identify keywords")
for _, w := range words {
log.Tracef("Checking if '%s' is a keyword", w)
if k, ok := keywordMap[w]; ok {
log.Tracef("Identified keyword -> '%s'", w)
keys = append(keys, k)
}
}
// Sort in descending order and then append the default case to the end
sort.Sort(byWeight(keys))
keys = append(keys, keywordMap["xnone"])
return
}
func processKeywords(keywords []keyword, words []string) ([]string, error) {
for _, kw := range keywords {
// Get the pattern for the keyword and attempt to match it to the words we have
for _, d := range kw.Decompositions {
pattern := d.Pattern
// Deal with synonyms
// If we have a word in the pattern prefixed with a @ then it needs to be
// substituted with all possible synonyms.
// nb. May be more efficient to bake these directly into the pattern definitions
// but that negates ease of adding new synonyms in future.
for k := range synonyms {
synonymKey := "@" + k
if strings.Contains(pattern, synonymKey) {
pattern = strings.Replace(pattern, synonymKey, fmt.Sprintf("(?:%s)", strings.Join(synonyms[k], "|")), -1)
}
}
sentance := strings.Join(words, " ")
log.Tracef("Process keywords: Attempt to match pattern '%s' to '%s'\n", pattern, sentance)
re := regexp.MustCompile(pattern)
results := re.FindStringSubmatch(sentance)
if len(results) > 0 {
resassmbly := chooseAssembly(d)
log.Debugf("Process keywords: Matched regex [%s] -> now using assembly [%s]\n", pattern, resassmbly)
for i, match := range results {
// Before the matched text is subbed back in, it needs to post substituted
// replace "I" with "You" etc using the 'post' substitution list
match := strings.Join(postSubstitute(strings.Split(strings.Trim(match, " "), " ")), " ")
resassmbly = strings.Replace(resassmbly, fmt.Sprintf("(%d)", i), match, -1)
}
return strings.Split(resassmbly, " "), nil
}
}
}
return nil, errors.New("Failed to process keywords - no clauses matched")
}