├── .circleci └── config.yml ├── .gitignore ├── LICENSE ├── README.md ├── analytics.go ├── anaytics_processor.go ├── api.go ├── api_actions.go ├── api_chats.go ├── api_collections.go ├── api_dialogs.go ├── api_downloads.go ├── api_summary.go ├── api_views.go ├── auth.go ├── bot.go ├── channel.go ├── channels ├── STTS │ ├── channel.go │ ├── client.go │ ├── input.go │ ├── new.go │ ├── speech_text.go │ └── text_speech.go ├── facebook │ ├── channel.go │ ├── input.go │ ├── input_text.go │ ├── messenger │ │ ├── facebook.go │ │ ├── messages.go │ │ └── messenger.go │ ├── new.go │ ├── output.go │ ├── output_image_response.go │ ├── output_option_response.go │ ├── output_pause_response.go │ └── output_text_response.go ├── telegram │ └── channel.go └── terminal │ ├── channel.go │ ├── input.go │ ├── input_text.go │ └── new.go ├── cli ├── main.go └── tools.go ├── cognitive.go ├── cognitive ├── dialogflow │ └── cognitive.go ├── uselessbox │ ├── cognitive.go │ ├── input.go │ ├── input_text.go │ ├── output.go │ └── output_text.go └── watson │ ├── cognitive.go │ ├── input.go │ ├── input_text.go │ ├── normalizer.go │ ├── output.go │ ├── output_image.go │ ├── output_option.go │ ├── output_pause.go │ ├── output_text.go │ └── output_unknown.go ├── context.go ├── context_utils.go ├── cortex.go ├── cortex_configuration.go ├── cortex_creator.go ├── cortex_metrics.go ├── cortex_onmessage.go ├── defaults.go ├── dialog.go ├── entity.go ├── errors.go ├── examples ├── basic │ └── main.go ├── facebook │ └── main.go ├── facebook_watson │ └── main.go └── watson │ └── main.go ├── filler.go ├── garbage_collector.go ├── go.mod ├── go.sum ├── injection.go ├── input.go ├── intent.go ├── log.go ├── matcher.go ├── matcher_utils.go ├── node.go ├── options.go ├── output.go ├── output_adders.go ├── output_utils.go ├── pipeline.go ├── proto └── neocortex.proto ├── repositories ├── boltdb │ ├── operations.go │ └── repository.go ├── memory │ └── repo.go └── mongodb │ ├── operations.go │ └── repository.go ├── repository.go └── resolvers.go /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Golang CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-go/ for more details 4 | version: 2 5 | jobs: 6 | build: 7 | docker: 8 | # specify the version 9 | - image: circleci/golang:1.12 10 | environment: 11 | GO111MODULE: "on" 12 | # Specify service dependencies here if necessary 13 | # CircleCI maintains a library of pre-built images 14 | # documented at https://circleci.com/docs/2.0/circleci-images/ 15 | # - image: circleci/postgres:9.4 16 | 17 | #### TEMPLATE_NOTE: go expects specific checkout path representing url 18 | #### expecting it in the form of 19 | #### /go/src/github.com/circleci/go-tool 20 | #### /go/src/bitbucket.org/circleci/go-tool 21 | working_directory: /go/src/github.com/minskylab/neocortex 22 | steps: 23 | - checkout 24 | 25 | # specify any bash command here prefixed with `run: ` 26 | - run: go get -v -t -d ./... 27 | - run: go test -v ./... 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | examples_ 14 | .idea 15 | *.db 16 | .vscode 17 | .DS_Store 18 | cli -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Bregy Malpartida Ramos 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Neocortex 🧠 2 | 3 | Neocortex is a tool to connect your cognitive service with your web services and communication channels. 4 | 5 | The main goal of neocortex is offers a reliable and modern API to connect any kind of cognitive service* with any communication channel**. 6 | 7 | *Currently neocortex offers only two cognitive services: Watson and a simple Useless box as dummy service, you can collaborate to implement another cognitive service like DialogFlow or Amazon Lex, later I'm going to document how to implement this services but you can read the source code to understand how to. 8 | 9 | **Like cognitive services, I could only implement only two channels: Facebook Messenger and a simple Terminal chat (very simple to emulate a chat in your terminal), if you want you can collaborate implementing other channels like Slack, Whatsapp or Gmail, for example. 10 | 11 | *🚧 Neocortex is work in progress, it pretends to be a big collaborative project* 12 | 13 | ### TODO 14 | - [x] Think the paradigm and write the first types of neocortex. 15 | 16 | - [x] Describe a Cognitive Service interface (`type CognitiveService interface`) 17 | 18 | - [x] Describe a Communication channel interface (`type CommunicationChannel interface`) 19 | 20 | - [x] Implement the Watson cognitive service 21 | 22 | - [x] Implement the Facebook channel 23 | 24 | - [x] Implement persistence contexts and dialog sessions 25 | 26 | - [x] Implement analytics reports 27 | 28 | - [x] MongoDB repository driver 29 | 30 | - [ ] Write unit tests // working on 31 | 32 | - [ ] Make an iteration of the Communication channel's architecture 33 | 34 | - [ ] Think more in the Cognitive's orientation (paradigm, architecture, etc) 35 | 36 | - [ ] Improve the neocortex engine 37 | 38 | - [ ] Write the Gmail channel implementation 39 | 40 | - [ ] Write the Dialog flow service implementation 41 | 42 | - [ ] Improve facebook messenger API 43 | 44 | - [ ] Document more 45 | 46 | - [ ] Document more more! 47 | 48 | 49 | ## Install 50 | 51 | Install with: 52 | 53 | ```go get -u github.com/minskylab/neocortex``` 54 | 55 | Currently, neocortex has 2 implementations of Cognitive Services (Useless-box created by me and Watson Assistant based on the [watson official API v2](https://github.com/watson-developer-cloud/go-sdk)) and 2 implementations of Communication Channels (Terminal based UI written by me and Facebook Messenger forked from [Facebook-messenger API](https://github.com/mileusna/facebook-messenger) by [mileusna](https://github.com/mileusna)). 56 | 57 | ### Basic Example 58 | 59 | ```go 60 | package main 61 | 62 | import ( 63 | neo "github.com/minskylab/neocortex" 64 | "github.com/minskylab/neocortex/channels/terminal" 65 | "github.com/minskylab/neocortex/cognitive/uselessbox" 66 | "github.com/minskylab/neocortex/repositories/boltdb" 67 | ) 68 | 69 | // Example of use useless box with terminal channel 70 | func main() { 71 | box := uselessbox.NewCognitive() 72 | term := terminal.NewChannel(nil) 73 | 74 | repo, err := boltdb.New("neocortex.db") 75 | if err != nil { 76 | panic(err) 77 | } 78 | 79 | engine, err := neo.New(repo, box, term) 80 | if err != nil { 81 | panic(err) 82 | } 83 | 84 | engine.ResolveAny(term, func(in *neo.Input, out *neo.Output, response neo.OutputResponse) error { 85 | out.AddTextResponse("-----Watermark-----") 86 | return response(out) 87 | }) 88 | 89 | if err = engine.Run(); err != nil { 90 | panic(err) 91 | } 92 | } 93 | 94 | ``` 95 | You can see more examples [here](https://github.com/minskylab/neocortex/tree/master/examples). 96 | 97 | ## Paradigm 98 | 99 | Neocortex is like a middleware with a mux, with it you can catch your message input, pass to your cognitive service, inflate or modify them and respond. 100 | 101 | ### Concepts 102 | 103 | 1. **Cognitive Service** 104 | 105 | Represents any service that decodes and find intents and entities in a human message. In neocortex, this is described by a simple interface. 106 | 107 | ```go 108 | type CognitiveService interface { 109 | CreateNewContext(c *context.Context, info neocortex.PersonInfo) *neocortex.Context 110 | GetProtoResponse(in *neocortex.Input) (*neocortex.Output, error) 111 | } 112 | ``` 113 | 114 | you can see the implementation of a [Useless box](https://github.com/minskylab/neocortex/tree/master/cognitive/uselessbox) or [Watson Assistant](https://github.com/minskylab/neocortex/tree/master/cognitive/watson). 115 | 116 | 2. **Communication Channel** 117 | 118 | A Communication Channel is any human interface where a person can to send a message and receive a response. Currently, I think we need to work more in the paradigm behind Communication channels. In neocortex a communication channel is described by the following interface: 119 | 120 | ```go 121 | type CommunicationChannel interface { 122 | RegisterMessageEndpoint(handler neocortex.MiddleHandler) error 123 | ToHear() error 124 | GetContextFabric() neocortex.ContextFabric 125 | SetContextFabric(fabric neocortex.ContextFabric) 126 | OnNewContextCreated(callback func(c *neocortex.Context)) 127 | } 128 | ``` 129 | 130 | Please, read how to are implemented the [Terminal channel](https://github.com/minskylab/neocortex/tree/master/channels/terminal) or [Facebook Messenger Channel](https://github.com/minskylab/neocortex/tree/master/channels/facebook). 131 | 132 | 3. **Context** 133 | 134 | Neocortex's Context represents a "session" or "dialog" with a human, it contains essential information about the person with we're a conversation. 135 | 136 | ```go 137 | // Context represent the context of one conversation 138 | type Context struct { 139 | Context *context.Context // go native context implementation 140 | SessionID string 141 | Person neocortex.PersonInfo 142 | Variables map[string]interface{} //conversation context variables 143 | } 144 | ``` 145 | 146 | 4. **Input** 147 | 148 | An input is a message input, that's all. Input has a specified type and in the neocortex is a struct: 149 | 150 | ```go 151 | type Input struct { 152 | Context *neocortex.Context 153 | Data neocortex.InputData 154 | Entities []neocortex.Entity 155 | Intents []neocortex.Intent 156 | } 157 | ``` 158 | 159 | Intents and Entities define the message. 160 | 161 | 5. **Output** 162 | 163 | An output represents a response in a conversation, with this you can define the response to your communication channel (e.g. facebook messenger) and if your channel allows you can respond different types of response (e.g. Image, Audio, Attachment, etc). 164 | 165 | ```go 166 | type Output struct { 167 | Context *neocortex.Context 168 | Entities []neocortex.Entity 169 | Intents []neocortex.Intent 170 | VisitedNodes []*neocortex.DialogNode 171 | Logs []*neocortex.LogMessage 172 | Responses []neocortex.Response // A list of responses 173 | } 174 | 175 | type Response struct { 176 | IsTyping bool 177 | Type neocortex.ResponseType 178 | Value interface{} 179 | } 180 | ``` 181 | 182 | 6. **Engine** 183 | 184 | This is the core of neocortex it can to connect and manage your Cognitive service and your Communication channels. the engine has different methods for intercept a message and modifies it. 185 | 186 | ```go 187 | // Create a New neocortex Engine 188 | func New(cognitive CognitiveService, channels ...CommunicationChannel) (*Engine, error) {} 189 | 190 | // Register a new resolver, you need a matcher 191 | func (engine *Engine) Resolve(channel CommunicationChannel, matcher Matcher, handler HandleResolver) {} 192 | 193 | // Maatcher 194 | type Matcher struct { 195 | Entity Match 196 | Intent Match 197 | AND *Matcher 198 | OR *Matcher 199 | } 200 | // Match 201 | type Match struct { 202 | Is string 203 | Confidence float64 204 | } 205 | ``` 206 | 207 | 208 | 209 | 7. **Resolver** 210 | 211 | This is the core of the neocortex paradigm, with this you can intercept a message and modify or only bypass it. You need to pass a Matcher who is used to match with your message inputs, you can see below how looks like a Matcher struct. Above you can see two examples of a matcher: 212 | 213 | ```go 214 | // match if the input has a Regard or Goodbye intents 215 | match := neo.Matcher{Intent: neo.Match{Is: "REGARD"}, OR: &neo.Matcher{Intent: neo.Match{Is: "GOODBYE"}}} 216 | 217 | // match if the input is an insult intent and has a bad_word entity 218 | match := neo.Matcher{ 219 | Intent: neo.Match{Is:"INSULT", Confidence: 0.8}, 220 | AND: &neo.Matcher{ 221 | Entity: neo.Match{ 222 | Is: "bad_word", 223 | }, 224 | }, 225 | } 226 | ``` 227 | 228 | Register a new resolver is simple, see the following example above 229 | 230 | ```go 231 | match := neo.Matcher{Intent: neo.Match{Is:"HELLO", Confidence: 0.8}} 232 | engine.Resolve(fb, match, func(in *neo.Input, out *neo.Output, response neo.OutputResponse) error { 233 | out.AddTextResponse("Powered by neocortex") 234 | return response(out) 235 | }) 236 | ``` 237 | 238 | You can make another type of Resolves. 239 | 240 | ```go 241 | func (engine *Engine) ResolveAny(channel CommunicationChannel, handler HandleResolver) {} 242 | func (engine *Engine) Resolve(channel CommunicationChannel, matcher Matcher, handler HandleResolver) {} 243 | func (engine *Engine) ResolveMany(channels []CommunicationChannel, matcher Matcher, handler HandleResolver) {} 244 | func (engine *Engine) ResolveManyAny(channels []CommunicationChannel, handler HandleResolver) {} 245 | ``` 246 | 247 | 248 | 249 | 🚧 Work in progress documentation, if you want to help, only send me an email. 250 | 251 | 252 | ![love open source](https://github.com/bregydoc/torioux-hands/raw/master/I_love_opensource.png) 253 | 254 | -------------------------------------------------------------------------------- /analytics.go: -------------------------------------------------------------------------------- 1 | package neocortex 2 | 3 | type ViewStyle string 4 | 5 | const Line ViewStyle = "line" 6 | const Bars ViewStyle = "bars" 7 | const Pie ViewStyle = "pie" 8 | 9 | // const Map ViewStyle = "map" [DEPRECATED] 10 | 11 | type ViewClassType string 12 | 13 | const EntityClass ViewClassType = "entity" 14 | const IntentClass ViewClassType = "intent" 15 | const DialogNodeClass ViewClassType = "node" 16 | const ContextVarClass ViewClassType = "context_var" 17 | 18 | type ViewClass struct { 19 | Type ViewClassType `json:"type"` 20 | Value string `json:"value"` 21 | } 22 | 23 | type ActionVarType string 24 | 25 | const TextActionVar ActionVarType = "text" 26 | 27 | type View struct { 28 | ID string `json:"id"` 29 | Name string `json:"name"` 30 | FrequencyMode bool `json:"frequency_mode"` 31 | Styles []ViewStyle `json:"styles"` 32 | Classes []ViewClass `json:"classes"` 33 | Children []*View `json:"children"` 34 | } 35 | 36 | type ActionVariable struct { 37 | Name string `json:"name"` 38 | Type ActionVarType `json:"type"` 39 | Value []byte `json:"value"` 40 | } 41 | 42 | type UsersSummary struct { 43 | News int64 `json:"news"` 44 | Recurrents int64 `json:"recurrents"` 45 | } 46 | 47 | type Summary struct { 48 | TotalDialogs int64 `json:"total_dialogs"` 49 | TotalUsers int64 `json:"total_users"` 50 | RecurrentUsers int64 `json:"recurrent_users"` 51 | UsersByTimezone map[string]UsersSummary `json:"users_by_timezone"` 52 | PerformanceMean float64 `json:"performance_mean"` 53 | } 54 | 55 | type Analytics struct { 56 | performanceFunction func(dialog *Dialog) float64 57 | repo Repository 58 | } 59 | 60 | func newDefaultAnalytics(repo Repository, performanceFunction func(dialog *Dialog) float64) (*Analytics, error) { 61 | return &Analytics{ 62 | performanceFunction: performanceFunction, 63 | repo: repo, 64 | }, nil 65 | } 66 | -------------------------------------------------------------------------------- /anaytics_processor.go: -------------------------------------------------------------------------------- 1 | package neocortex 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "sort" 7 | "time" 8 | ) 9 | 10 | type MessageOwner string 11 | 12 | const Person MessageOwner = "person" 13 | const ChatBot MessageOwner = "bot" 14 | 15 | type MessageResponse struct { 16 | Type ResponseType `json:"type"` 17 | Value interface{} `json:"value"` 18 | } 19 | 20 | type Message struct { 21 | At time.Time `json:"at"` 22 | Owner MessageOwner `json:"owner"` 23 | Intents []Intent `json:"intents"` 24 | Entities []Entity `json:"entities"` 25 | Response MessageResponse `json:"response"` 26 | } 27 | 28 | type Chat struct { 29 | ID string `json:"id"` 30 | LastMessageAt time.Time `json:"last_message_at"` 31 | Person PersonInfo `json:"person"` 32 | Performance float64 `json:"performance"` 33 | Messages []Message `json:"messages"` 34 | } 35 | 36 | type byDate []Message 37 | 38 | func (messages byDate) Len() int { 39 | return len(messages) 40 | } 41 | func (messages byDate) Swap(i, j int) { 42 | messages[i], messages[j] = messages[j], messages[i] 43 | } 44 | func (messages byDate) Less(i, j int) bool { 45 | return messages[i].At.Before(messages[j].At) 46 | } 47 | 48 | type byLastMessageAt []*Chat 49 | 50 | func (chats byLastMessageAt) Len() int { 51 | return len(chats) 52 | } 53 | func (chats byLastMessageAt) Swap(i, j int) { 54 | chats[i], chats[j] = chats[j], chats[i] 55 | } 56 | func (chats byLastMessageAt) Less(i, j int) bool { 57 | return chats[i].LastMessageAt.After(chats[j].LastMessageAt) 58 | } 59 | 60 | func (analitycs *Analytics) processDialogs(dialogs []*Dialog) []*Chat { 61 | performances := map[string][]float64{} 62 | mapChats := map[string]*Chat{} 63 | lastMessages := map[string]time.Time{} 64 | 65 | for _, d := range dialogs { 66 | if len(d.Contexts) == 0 { 67 | continue 68 | } 69 | 70 | ctx := d.Contexts[0].Context 71 | personID := ctx.Person.ID 72 | if _, ok := mapChats[personID]; !ok { 73 | mapChats[personID] = new(Chat) 74 | mapChats[personID].Person = ctx.Person 75 | mapChats[personID].ID = personID 76 | mapChats[personID].Messages = []Message{} 77 | 78 | } 79 | 80 | if _, ok := performances[personID]; !ok { 81 | performances[personID] = []float64{} 82 | } 83 | 84 | if _, ok := lastMessages[personID]; !ok { 85 | lastMessages[personID] = time.Time{} 86 | } 87 | 88 | for _, i := range d.Ins { 89 | mapChats[personID].Messages = append(mapChats[personID].Messages, Message{ 90 | At: i.At, 91 | Owner: Person, 92 | Response: MessageResponse{ 93 | Type: Text, 94 | Value: i.Input.Data.Value, 95 | }, 96 | Entities: i.Input.Entities, 97 | Intents: i.Input.Intents, 98 | }) 99 | if i.At.After(lastMessages[personID]) { 100 | lastMessages[personID] = i.At 101 | } 102 | } 103 | 104 | for _, o := range d.Outs { 105 | for _, r := range o.Output.Responses { 106 | mapChats[personID].Messages = append(mapChats[personID].Messages, Message{ 107 | At: o.At, 108 | Owner: ChatBot, 109 | Response: MessageResponse{ 110 | Type: r.Type, 111 | Value: r.Value, 112 | }, 113 | Entities: o.Output.Entities, 114 | Intents: o.Output.Intents, 115 | }) 116 | } 117 | 118 | } 119 | 120 | if analitycs.performanceFunction != nil { 121 | performances[personID] = append(performances[personID], analitycs.performanceFunction(d)) 122 | } 123 | 124 | } 125 | 126 | chats := make([]*Chat, 0) 127 | for personID, chat := range mapChats { 128 | performance := 0.0 129 | for _, p := range performances[personID] { 130 | performance += p 131 | } 132 | 133 | performance /= float64(len(performances[personID])) 134 | 135 | sort.Sort(byDate(chat.Messages)) 136 | 137 | chats = append(chats, &Chat{ 138 | ID: chat.ID, 139 | Person: chat.Person, 140 | Messages: chat.Messages, 141 | Performance: performance, 142 | LastMessageAt: lastMessages[personID], 143 | }) 144 | } 145 | 146 | sort.Sort(byLastMessageAt(chats)) 147 | 148 | return chats 149 | } 150 | 151 | type TimeAnalysisResult struct { 152 | Timeline map[time.Time]map[string]float64 `json:"timeline"` 153 | TotalDialogs int `json:"total_dialogs"` 154 | TotalCounts int `json:"total_counts"` 155 | Individuals map[string]float64 `json:"individuals"` 156 | } 157 | 158 | func (analitycs *Analytics) timeAnalysis(viewID string, frame TimeFrame) (*TimeAnalysisResult, error) { 159 | log.Println("time analytic") 160 | view, err := analitycs.repo.GetViewByID(viewID) 161 | if err != nil { 162 | return nil, err 163 | } 164 | 165 | log.Println(viewID, fmt.Sprintf("%+v", view)) 166 | 167 | dialogs, err := analitycs.repo.DialogsByView(viewID, frame) 168 | if err != nil { 169 | return nil, err 170 | } 171 | 172 | timeline := map[time.Time]map[string]float64{} 173 | individuals := map[string]float64{} 174 | totalCounts := 0 175 | for _, dialog := range dialogs { 176 | valueName := "" 177 | for _, class := range view.Classes { 178 | switch class.Type { 179 | case EntityClass: 180 | if dialog.HasEntity(class.Value) { 181 | valueName = "E-" + class.Value 182 | if _, ok := individuals[class.Value]; !ok { 183 | individuals[class.Value] = 0 184 | } 185 | individuals[class.Value]++ 186 | } 187 | case IntentClass: 188 | if dialog.HasIntent(class.Value) { 189 | valueName = "I-" + class.Value 190 | if _, ok := individuals[class.Value]; !ok { 191 | individuals[class.Value] = 0 192 | } 193 | individuals[class.Value]++ 194 | } 195 | case DialogNodeClass: 196 | if dialog.HasDialogNode(class.Value) { 197 | valueName = "D-" + class.Value 198 | if _, ok := individuals[class.Value]; !ok { 199 | individuals[class.Value] = 0 200 | } 201 | individuals[class.Value]++ 202 | } 203 | case ContextVarClass: 204 | if dialog.HasContextVar(class.Value) { 205 | valueName = "V-" + class.Value 206 | if _, ok := individuals[class.Value]; !ok { 207 | individuals[class.Value] = 0 208 | } 209 | individuals[class.Value]++ 210 | } 211 | default: 212 | continue 213 | } 214 | } 215 | pitch := time.Date( 216 | dialog.StartAt.Year(), 217 | dialog.StartAt.Month(), 218 | dialog.StartAt.Day(), 219 | dialog.StartAt.Hour(), 220 | 0, 221 | 0, 222 | 0, 223 | dialog.StartAt.Location(), 224 | ) 225 | 226 | if valueName != "" { 227 | if _, ok := timeline[pitch]; !ok { 228 | timeline[pitch] = map[string]float64{} 229 | } 230 | if _, ok := timeline[pitch][valueName]; !ok { 231 | timeline[pitch][valueName] = 0.0 232 | } 233 | timeline[pitch][valueName]++ 234 | totalCounts++ 235 | } 236 | } 237 | 238 | return &TimeAnalysisResult{ 239 | Timeline: timeline, 240 | TotalCounts: totalCounts, 241 | Individuals: individuals, 242 | TotalDialogs: len(dialogs), 243 | }, nil 244 | 245 | } 246 | -------------------------------------------------------------------------------- /api.go: -------------------------------------------------------------------------------- 1 | package neocortex 2 | 3 | import ( 4 | "github.com/gin-contrib/cors" 5 | "github.com/gin-gonic/gin" 6 | ) 7 | 8 | type API struct { 9 | e *gin.Engine 10 | Port string 11 | repository Repository 12 | prefix string 13 | analytics *Analytics 14 | } 15 | 16 | func newCortexAPI(repo Repository, analytics *Analytics, prefix, port string) *API { 17 | return &API{ 18 | e: gin.Default(), 19 | Port: port, 20 | prefix: prefix, 21 | repository: repo, 22 | analytics: analytics, 23 | } 24 | } 25 | 26 | func (api *API) registerEndpoints(engine *Engine) { 27 | corsConf := cors.DefaultConfig() 28 | corsConf.AddAllowHeaders("Authorization") 29 | 30 | corsConf.AllowAllOrigins = true 31 | 32 | c := cors.New(corsConf) 33 | 34 | api.e.Use(c) 35 | 36 | authJWTMiddleware := getJWTAuth(engine, engine.secret) 37 | 38 | api.e.POST("/login", authJWTMiddleware.LoginHandler) 39 | 40 | api.e.GET("/token_refresh", authJWTMiddleware.RefreshHandler) 41 | 42 | api.e.Use(authJWTMiddleware.MiddlewareFunc()) 43 | 44 | r := api.e.Group(api.prefix) 45 | api.registerDialogsAPI(r) 46 | api.registerViewsAPI(r) 47 | api.registerActionsAPI(r) 48 | api.registerCollectionsAPI(r) 49 | api.registerSummaryAPI(r) 50 | api.registerChatsAPI(r) 51 | api.registerDownloadsAPI(r) 52 | } 53 | 54 | func (api *API) Launch(engine *Engine) error { 55 | api.registerEndpoints(engine) 56 | return api.e.Run(api.Port) 57 | } 58 | -------------------------------------------------------------------------------- /api_actions.go: -------------------------------------------------------------------------------- 1 | package neocortex 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | func (api *API) registerActionsAPI(r *gin.RouterGroup) { 10 | 11 | r.POST("/actions/env/:name", func(c *gin.Context) { 12 | name := c.Param("name") 13 | 14 | type bind struct { 15 | Value string `json:"value"` 16 | } 17 | 18 | value := new(bind) 19 | 20 | err := c.BindJSON(value) 21 | if err != nil { 22 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 23 | return 24 | } 25 | err = api.repository.SetActionVar(name, value.Value) 26 | if err != nil { 27 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 28 | return 29 | } 30 | c.JSON(http.StatusOK, gin.H{ 31 | "data": value, 32 | }) 33 | }) 34 | 35 | r.GET("/actions/env/:name", func(c *gin.Context) { 36 | name := c.Param("name") 37 | value, err := api.repository.GetActionVar(name) 38 | if err != nil { 39 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 40 | return 41 | } 42 | c.JSON(http.StatusOK, gin.H{ 43 | "data": value, 44 | }) 45 | }) 46 | } 47 | -------------------------------------------------------------------------------- /api_chats.go: -------------------------------------------------------------------------------- 1 | package neocortex 2 | 3 | import ( 4 | "net/http" 5 | "strconv" 6 | 7 | "github.com/araddon/dateparse" 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | func (api *API) registerChatsAPI(r *gin.RouterGroup) { 12 | r.GET("/chat/:id", func(c *gin.Context) { 13 | frame := TimeFrame{} 14 | preset := TimeFramePreset(c.Query("preset")) 15 | 16 | if preset != DayPreset && preset != WeekPreset && preset != MonthPreset && preset != YearPreset { 17 | from, err := dateparse.ParseAny(c.Query("from")) 18 | if err != nil { 19 | c.JSON(http.StatusInternalServerError, gin.H{"error": "from date not found"}) 20 | return 21 | } 22 | 23 | to, err := dateparse.ParseAny(c.Query("to")) 24 | if err != nil { 25 | c.JSON(http.StatusInternalServerError, gin.H{"error": "to date not found"}) 26 | return 27 | } 28 | 29 | frame.From = from 30 | frame.To = to 31 | } else { 32 | frame.Preset = preset 33 | } 34 | 35 | page, _ := strconv.Atoi(c.Query("page")) 36 | size, _ := strconv.Atoi(c.Query("size")) 37 | 38 | frame.PageNum = page 39 | frame.PageSize = size 40 | 41 | dialogs, err := api.repository.AllDialogs(frame) 42 | if err != nil { 43 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 44 | return 45 | } 46 | 47 | chats := api.analytics.processDialogs(dialogs) 48 | 49 | userID := c.Param("id") 50 | 51 | if userID == "" { 52 | c.JSON(http.StatusInternalServerError, gin.H{"error": "invalid user ID, please select one or all"}) 53 | return 54 | } 55 | 56 | if userID == "all" { 57 | timezone := c.Query("timezone") 58 | totalChats := []*Chat{} 59 | if timezone != "" { 60 | for _, c := range chats { 61 | if timezone == c.Person.Timezone { 62 | totalChats = append(totalChats, c) 63 | } 64 | } 65 | } else { 66 | totalChats = chats 67 | } 68 | 69 | c.JSON(http.StatusOK, gin.H{ 70 | "data": totalChats, 71 | }) 72 | return 73 | } 74 | 75 | chatFound := new(Chat) 76 | for _, c := range chats { 77 | if c.Person.ID == userID { 78 | chatFound = c 79 | } 80 | } 81 | 82 | if chatFound == nil { 83 | c.JSON(http.StatusInternalServerError, gin.H{"error": "user ID not found in your time frame"}) 84 | return 85 | } 86 | 87 | c.JSON(http.StatusOK, gin.H{ 88 | "data": chatFound, 89 | }) 90 | }) 91 | 92 | r.GET("/chats", func(c *gin.Context) { 93 | frame := TimeFrame{} 94 | preset := TimeFramePreset(c.Query("preset")) 95 | 96 | if preset != DayPreset && preset != WeekPreset && preset != MonthPreset && preset != YearPreset { 97 | from, err := dateparse.ParseAny(c.Query("from")) 98 | if err != nil { 99 | c.JSON(http.StatusInternalServerError, gin.H{"error": "from date not found"}) 100 | return 101 | } 102 | 103 | to, err := dateparse.ParseAny(c.Query("to")) 104 | if err != nil { 105 | c.JSON(http.StatusInternalServerError, gin.H{"error": "to date not found"}) 106 | return 107 | } 108 | 109 | frame.From = from 110 | frame.To = to 111 | } else { 112 | frame.Preset = preset 113 | } 114 | 115 | page, _ := strconv.Atoi(c.Query("page")) 116 | size, _ := strconv.Atoi(c.Query("size")) 117 | 118 | frame.PageNum = page 119 | frame.PageSize = size 120 | 121 | dialogs, err := api.repository.AllDialogs(frame) 122 | if err != nil { 123 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 124 | return 125 | } 126 | 127 | chats := api.analytics.processDialogs(dialogs) 128 | c.JSON(http.StatusOK, gin.H{ 129 | "data": chats, 130 | }) 131 | }) 132 | 133 | } 134 | -------------------------------------------------------------------------------- /api_collections.go: -------------------------------------------------------------------------------- 1 | package neocortex 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | func (api *API) registerCollectionsAPI(r *gin.RouterGroup) { 10 | 11 | r.GET("/collections/:type", func(c *gin.Context) { 12 | t := c.Param("type") 13 | 14 | if t != "all" { 15 | switch t { 16 | case "entity", "entities": 17 | ents := api.repository.Entities() 18 | c.JSON(http.StatusOK, gin.H{ 19 | "data": ents, 20 | }) 21 | return 22 | case "intent", "intents": 23 | ints := api.repository.Intents() 24 | c.JSON(http.StatusOK, gin.H{ 25 | "data": ints, 26 | }) 27 | return 28 | case "node", "dialog", "nodes", "dialog_nodes", "dialogs": 29 | nodes := api.repository.DialogNodes() 30 | c.JSON(http.StatusOK, gin.H{ 31 | "data": nodes, 32 | }) 33 | return 34 | case "context_vars", "vars": 35 | vars := api.repository.ContextVars() 36 | c.JSON(http.StatusOK, gin.H{ 37 | "data": vars, 38 | }) 39 | return 40 | default: 41 | c.JSON(http.StatusInternalServerError, gin.H{"error": "invalid type of collection"}) 42 | return 43 | } 44 | } 45 | 46 | ents := api.repository.Entities() 47 | ints := api.repository.Intents() 48 | nodes := api.repository.DialogNodes() 49 | vars := api.repository.ContextVars() 50 | 51 | c.JSON(http.StatusOK, gin.H{"data": gin.H{ 52 | "intents": ints, 53 | "entities": ents, 54 | "nodes": nodes, 55 | "context_vars": vars, 56 | }}) 57 | }) 58 | } 59 | -------------------------------------------------------------------------------- /api_dialogs.go: -------------------------------------------------------------------------------- 1 | package neocortex 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "strconv" 7 | 8 | "strings" 9 | 10 | "github.com/araddon/dateparse" 11 | "github.com/gin-gonic/gin" 12 | "github.com/rs/xid" 13 | ) 14 | 15 | func (api *API) registerDialogsAPI(r *gin.RouterGroup) { 16 | r.GET("/dialog/:id", func(c *gin.Context) { 17 | id := c.Param("id") 18 | dialog, err := api.repository.GetDialogByID(id) 19 | if err != nil { 20 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 21 | return 22 | } 23 | c.JSON(http.StatusOK, gin.H{ 24 | "data": dialog, 25 | }) 26 | }) 27 | 28 | r.GET("/dialogs/*view", func(c *gin.Context) { 29 | frame := TimeFrame{} 30 | preset := TimeFramePreset(c.Query("preset")) 31 | 32 | if preset != DayPreset && preset != WeekPreset && preset != MonthPreset && preset != YearPreset { 33 | from, err := dateparse.ParseAny(c.Query("from")) 34 | if err != nil { 35 | c.JSON(http.StatusInternalServerError, gin.H{"error": "from date not found"}) 36 | return 37 | } 38 | 39 | to, err := dateparse.ParseAny(c.Query("to")) 40 | if err != nil { 41 | c.JSON(http.StatusInternalServerError, gin.H{"error": "to date not found"}) 42 | return 43 | } 44 | frame.From = from 45 | frame.To = to 46 | } else { 47 | frame.Preset = preset 48 | } 49 | 50 | viewID := c.Param("view") 51 | 52 | page, _ := strconv.Atoi(c.Query("page")) 53 | size, _ := strconv.Atoi(c.Query("size")) 54 | 55 | frame.PageNum = page 56 | frame.PageSize = size 57 | 58 | if viewID == "" || viewID == "/" { 59 | dialogs, err := api.repository.AllDialogs(frame) 60 | if err != nil { 61 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 62 | return 63 | } 64 | 65 | c.JSON(http.StatusOK, gin.H{ 66 | "data": dialogs, 67 | }) 68 | return 69 | } 70 | 71 | viewID = strings.Trim(viewID, "/") 72 | 73 | log.Println("viewID, ", viewID) 74 | if _, err := xid.FromString(viewID); err != nil { 75 | log.Println(err.Error()) 76 | c.JSON(http.StatusInternalServerError, gin.H{"error": "invalid view id"}) 77 | return 78 | } 79 | 80 | dialogs, err := api.repository.DialogsByView(viewID, frame) 81 | 82 | if err != nil { 83 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 84 | return 85 | } 86 | 87 | c.JSON(http.StatusOK, gin.H{ 88 | "data": dialogs, 89 | }) 90 | return 91 | }) 92 | 93 | r.GET("/time/dialogs/*view", func(c *gin.Context) { 94 | frame := TimeFrame{} 95 | preset := TimeFramePreset(c.Query("preset")) 96 | 97 | if preset != DayPreset && preset != WeekPreset && preset != MonthPreset && preset != YearPreset { 98 | from, err := dateparse.ParseAny(c.Query("from")) 99 | if err != nil { 100 | c.JSON(http.StatusInternalServerError, gin.H{"error": "from date not found"}) 101 | return 102 | } 103 | 104 | to, err := dateparse.ParseAny(c.Query("to")) 105 | if err != nil { 106 | c.JSON(http.StatusInternalServerError, gin.H{"error": "to date not found"}) 107 | return 108 | } 109 | 110 | frame.From = from 111 | frame.To = to 112 | } else { 113 | frame.Preset = preset 114 | } 115 | 116 | viewID := c.Param("view") 117 | 118 | page, _ := strconv.Atoi(c.Query("page")) 119 | size, _ := strconv.Atoi(c.Query("size")) 120 | 121 | frame.PageNum = page 122 | frame.PageSize = size 123 | 124 | if viewID == "" || viewID == "/" { 125 | dialogs, err := api.repository.AllDialogs(frame) 126 | if err != nil { 127 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 128 | return 129 | } 130 | 131 | c.JSON(http.StatusOK, gin.H{ 132 | "data": dialogs, 133 | }) 134 | return 135 | } 136 | 137 | viewID = strings.Trim(viewID, "/") 138 | 139 | log.Println("viewID, ", viewID) 140 | if _, err := xid.FromString(viewID); err != nil { 141 | log.Println(err.Error()) 142 | c.JSON(http.StatusInternalServerError, gin.H{"error": "invalid view id"}) 143 | return 144 | } 145 | 146 | // ! BETA, please not use in production 147 | a := &Analytics{repo: api.repository} 148 | timeAnalysis, err := a.timeAnalysis(viewID, frame) 149 | if err != nil { 150 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 151 | return 152 | } 153 | 154 | c.JSON(http.StatusOK, gin.H{ 155 | "data": timeAnalysis, 156 | }) 157 | return 158 | }) 159 | } 160 | -------------------------------------------------------------------------------- /api_downloads.go: -------------------------------------------------------------------------------- 1 | package neocortex 2 | 3 | import ( 4 | "bytes" 5 | "encoding/csv" 6 | "net/http" 7 | 8 | "strconv" 9 | 10 | "io/ioutil" 11 | "os" 12 | 13 | "github.com/araddon/dateparse" 14 | "github.com/gin-gonic/gin" 15 | ) 16 | 17 | func (api *API) registerDownloadsAPI(r *gin.RouterGroup) { 18 | r.POST("/download/chat/:userID", func(c *gin.Context) { 19 | frame := TimeFrame{} 20 | preset := TimeFramePreset(c.Query("preset")) 21 | 22 | if preset != DayPreset && preset != WeekPreset && preset != MonthPreset && preset != YearPreset { 23 | from, err := dateparse.ParseAny(c.Query("from")) 24 | if err != nil { 25 | c.JSON(http.StatusInternalServerError, gin.H{"error": "from date not found"}) 26 | return 27 | } 28 | 29 | to, err := dateparse.ParseAny(c.Query("to")) 30 | if err != nil { 31 | c.JSON(http.StatusInternalServerError, gin.H{"error": "to date not found"}) 32 | return 33 | } 34 | 35 | frame.From = from 36 | frame.To = to 37 | } else { 38 | frame.Preset = preset 39 | } 40 | 41 | page, _ := strconv.Atoi(c.Query("page")) 42 | size, _ := strconv.Atoi(c.Query("size")) 43 | 44 | frame.PageNum = page 45 | frame.PageSize = size 46 | 47 | dialogs, err := api.repository.AllDialogs(frame) 48 | if err != nil { 49 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 50 | return 51 | } 52 | 53 | chats := api.analytics.processDialogs(dialogs) 54 | 55 | userID := c.Param("userID") 56 | 57 | if userID == "" { 58 | c.JSON(http.StatusInternalServerError, gin.H{"error": "invalid user ID, please select one or all"}) 59 | return 60 | } 61 | 62 | chatsFound := make([]*Chat, 0) 63 | 64 | if userID == "all" { 65 | timezone := c.Query("timezone") 66 | if timezone == "" { 67 | chatsFound = chats 68 | } else { 69 | for _, c := range chats { 70 | if c.Person.Timezone == timezone { 71 | chatsFound = append(chatsFound, c) 72 | } 73 | } 74 | } 75 | } else { 76 | for _, c := range chats { 77 | if c.Person.ID == userID { 78 | chatsFound = append(chatsFound, c) 79 | } 80 | } 81 | } 82 | 83 | if len(chatsFound) == 0 { 84 | c.JSON(http.StatusInternalServerError, gin.H{"error": "user ID not found in your time frame"}) 85 | return 86 | } 87 | 88 | table := [][]string{{ 89 | "id", 90 | "name", 91 | "timezone", 92 | "last_message", 93 | "performance", 94 | "message_at", 95 | "message_who", 96 | "message_message", 97 | "message_intent", 98 | "message_entities", 99 | }} 100 | 101 | lastID := "" 102 | row := []string{} 103 | for _, c := range chatsFound { 104 | if c.ID != lastID { 105 | if len(c.Messages) > 0 { 106 | r, ok := c.Messages[0].Response.Value.(string) 107 | if !ok { 108 | r = "" 109 | } 110 | row = []string{ 111 | c.ID, 112 | c.Person.Name, 113 | c.Person.Timezone, 114 | c.LastMessageAt.String(), 115 | strconv.FormatFloat(c.Performance, 'f', 10, 64), 116 | c.Messages[0].At.String(), 117 | string(c.Messages[0].Owner), 118 | r, 119 | "", 120 | "", 121 | } 122 | } 123 | lastID = c.ID 124 | table = append(table, row) 125 | row = []string{"", "", "", "", ""} 126 | } 127 | 128 | if len(c.Messages) > 1 { 129 | for _, m := range c.Messages[1:] { 130 | row = []string{"", "", "", "", ""} 131 | row = append(row, m.At.String()) 132 | row = append(row, string(m.Owner)) 133 | 134 | if m.Response.Type == "text" { 135 | r, ok := m.Response.Value.(string) 136 | if !ok { 137 | r = "" 138 | } 139 | row = append(row, r) 140 | } else { 141 | row = append(row, "") 142 | } 143 | 144 | intent := "" 145 | 146 | if m.Intents != nil { 147 | if len(m.Intents) > 0 { 148 | intent = m.Intents[0].Intent 149 | } 150 | } 151 | 152 | row = append(row, intent) 153 | 154 | entity := "" 155 | if m.Entities != nil { 156 | ents := "" 157 | for _, e := range m.Entities { 158 | ents = ents + "|" + e.Entity 159 | } 160 | entity = ents 161 | } 162 | row = append(row, entity) 163 | 164 | table = append(table, row) 165 | } 166 | } 167 | } 168 | 169 | file := bytes.NewBufferString("") 170 | err = csv.NewWriter(file).WriteAll(table) 171 | if err != nil { 172 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 173 | return 174 | } 175 | 176 | tempFile, err := ioutil.TempFile(os.TempDir(), "neo") 177 | if err != nil { 178 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 179 | return 180 | } 181 | _, err = tempFile.Write(file.Bytes()) 182 | if err != nil { 183 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 184 | return 185 | } 186 | 187 | c.Status(http.StatusOK) 188 | c.Header("Content-Type", "text/csv") 189 | c.File(tempFile.Name()) 190 | }) 191 | } 192 | -------------------------------------------------------------------------------- /api_summary.go: -------------------------------------------------------------------------------- 1 | package neocortex 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/araddon/dateparse" 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | func (api *API) registerSummaryAPI(r *gin.RouterGroup) { 11 | r.GET("/summary", func(c *gin.Context) { 12 | frame := TimeFrame{} 13 | preset := TimeFramePreset(c.Query("preset")) 14 | 15 | if preset != DayPreset && preset != WeekPreset && preset != MonthPreset && preset != YearPreset { 16 | from, err := dateparse.ParseAny(c.Query("from")) 17 | if err != nil { 18 | c.JSON(http.StatusInternalServerError, gin.H{"error": "from date not found"}) 19 | return 20 | } 21 | 22 | to, err := dateparse.ParseAny(c.Query("to")) 23 | if err != nil { 24 | c.JSON(http.StatusInternalServerError, gin.H{"error": "to date not found"}) 25 | return 26 | } 27 | 28 | frame.From = from 29 | frame.To = to 30 | } else { 31 | frame.Preset = preset 32 | } 33 | 34 | // fromScratch := time.Date(2019, 1, 1, 0, 0, 0, 0, time.Local) 35 | // fromScratchFrame := TimeFrame{From: fromScratch, To: frame.From, PageSize: frame.PageSize, PageNum: frame.PageNum} 36 | 37 | // pastSummary, err := api.repository.Summary(fromScratchFrame) 38 | // if err != nil { 39 | // c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 40 | // return 41 | // } 42 | 43 | summary, err := api.repository.Summary(frame) 44 | if err != nil { 45 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 46 | return 47 | } 48 | 49 | // summary.RecurrentUsers = pastSummary.RecurrentUsers - summary.RecurrentUsers 50 | 51 | // for timezone, rec := range pastSummary.UsersByTimezone { 52 | // summary.UsersByTimezone[timezone] = UsersSummary{ 53 | // News: rec.News - summary.UsersByTimezone[timezone].News, 54 | // Recurrents: rec.Recurrents - summary.UsersByTimezone[timezone].Recurrents, 55 | // } 56 | // } 57 | 58 | c.JSON(http.StatusOK, gin.H{ 59 | "data": summary, 60 | }) 61 | }) 62 | } 63 | -------------------------------------------------------------------------------- /api_views.go: -------------------------------------------------------------------------------- 1 | package neocortex 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | func (api *API) registerViewsAPI(r *gin.RouterGroup) { 10 | r.POST("/view", func(c *gin.Context) { 11 | view := new(View) 12 | 13 | if err := c.BindJSON(view); err != nil { 14 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 15 | return 16 | } 17 | 18 | if err := api.repository.SaveView(view); err != nil { 19 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 20 | return 21 | } 22 | 23 | c.JSON(http.StatusOK, gin.H{"data": view}) 24 | }) 25 | 26 | r.PUT("/view/:id", func(c *gin.Context) { 27 | view := new(View) 28 | 29 | if err := c.BindJSON(view); err != nil { 30 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 31 | return 32 | } 33 | 34 | err := api.repository.UpdateView(view) 35 | if err != nil { 36 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 37 | return 38 | } 39 | 40 | c.JSON(http.StatusOK, gin.H{ 41 | "data": view, 42 | }) 43 | }) 44 | 45 | r.DELETE("/view/:id", func(c *gin.Context) { 46 | id := c.Param("id") 47 | view, err := api.repository.DeleteView(id) 48 | if err != nil { 49 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 50 | return 51 | } 52 | 53 | c.JSON(http.StatusOK, gin.H{ 54 | "data": view, 55 | }) 56 | }) 57 | 58 | r.GET("/view/:id", func(c *gin.Context) { 59 | id := c.Param("id") 60 | view, err := api.repository.GetViewByID(id) 61 | if err != nil { 62 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 63 | return 64 | } 65 | c.JSON(http.StatusOK, gin.H{ 66 | "data": view, 67 | }) 68 | }) 69 | 70 | r.GET("/views/*name", func(c *gin.Context) { 71 | name := c.Param("name") 72 | if name == "" || name == "/" { 73 | views, err := api.repository.AllViews() 74 | if err != nil { 75 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 76 | return 77 | } 78 | c.JSON(http.StatusOK, gin.H{ 79 | "data": views, 80 | }) 81 | return 82 | } 83 | 84 | views, err := api.repository.FindViewByName(name) 85 | if err != nil { 86 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 87 | return 88 | } 89 | c.JSON(http.StatusOK, gin.H{ 90 | "data": views, 91 | }) 92 | }) 93 | } 94 | -------------------------------------------------------------------------------- /auth.go: -------------------------------------------------------------------------------- 1 | package neocortex 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "io/ioutil" 7 | "log" 8 | "time" 9 | 10 | jwt "github.com/appleboy/gin-jwt/v2" 11 | "github.com/gin-gonic/gin" 12 | ) 13 | 14 | type login struct { 15 | Username string `form:"username" json:"username" binding:"required"` 16 | Password string `form:"password" json:"password" binding:"required"` 17 | } 18 | 19 | type user struct { 20 | Username string 21 | } 22 | 23 | const identityKey = "id" 24 | 25 | func getJWTAuth(engine *Engine, secretKey string) *jwt.GinJWTMiddleware { 26 | authMiddleware, err := jwt.New(&jwt.GinJWTMiddleware{ 27 | Realm: "test zone", 28 | Key: []byte(secretKey), 29 | Timeout: time.Hour, 30 | MaxRefresh: time.Hour, 31 | IdentityKey: identityKey, 32 | PayloadFunc: func(data interface{}) jwt.MapClaims { 33 | if v, ok := data.(*user); ok { 34 | return jwt.MapClaims{ 35 | identityKey: v.Username, 36 | } 37 | } 38 | return jwt.MapClaims{} 39 | }, 40 | IdentityHandler: func(c *gin.Context) interface{} { 41 | claims := jwt.ExtractClaims(c) 42 | return &user{ 43 | Username: claims["id"].(string), 44 | } 45 | }, 46 | Authenticator: func(c *gin.Context) (interface{}, error) { 47 | data, err := ioutil.ReadAll(c.Request.Body) 48 | if err != nil { 49 | return "", errors.New(err.Error()) 50 | } 51 | l := new(login) 52 | if l == nil { 53 | return nil, jwt.ErrFailedAuthentication 54 | } 55 | err = json.Unmarshal(data, l) 56 | if err != nil { 57 | return "", errors.New(err.Error()) 58 | } 59 | 60 | userID := l.Username 61 | password := l.Password 62 | 63 | passwordAdmin, err := engine.getAdmin(userID) 64 | if err != nil || passwordAdmin == "" { 65 | return nil, jwt.ErrFailedAuthentication 66 | } 67 | if password == passwordAdmin { 68 | return &user{ 69 | Username: userID, 70 | }, nil 71 | } 72 | 73 | return nil, jwt.ErrFailedAuthentication 74 | }, 75 | Authorizator: func(data interface{}, c *gin.Context) bool { 76 | return true 77 | }, 78 | Unauthorized: func(c *gin.Context, code int, message string) { 79 | c.JSON(code, gin.H{ 80 | "code": code, 81 | "message": message, 82 | }) 83 | }, 84 | 85 | TokenLookup: "header: Authorization, query: token, cookie: jwt", 86 | TokenHeadName: "Bearer", 87 | 88 | TimeFunc: time.Now, 89 | }) 90 | 91 | if err != nil { 92 | log.Fatal("JWT Error:" + err.Error()) 93 | } 94 | 95 | return authMiddleware 96 | } 97 | -------------------------------------------------------------------------------- /bot.go: -------------------------------------------------------------------------------- 1 | package neocortex 2 | 3 | type Bot struct { 4 | Name string `json:"name"` 5 | Version string `json:"version"` 6 | Author string `json:"author"` 7 | } 8 | -------------------------------------------------------------------------------- /channel.go: -------------------------------------------------------------------------------- 1 | package neocortex 2 | 3 | type CommunicationChannel interface { 4 | RegisterMessageEndpoint(handler MiddleHandler) error 5 | ToHear() error 6 | GetContextFabric() ContextFabric // TODO: Rev 7 | SetContextFabric(fabric ContextFabric) 8 | OnNewContextCreated(callback func(c *Context)) 9 | OnContextIsDone(callback func(c *Context)) 10 | CallContextDone(c *Context) 11 | } 12 | -------------------------------------------------------------------------------- /channels/STTS/channel.go: -------------------------------------------------------------------------------- 1 | package speech 2 | 3 | import ( 4 | "strconv" 5 | 6 | neo "github.com/minskylab/neocortex" 7 | ) 8 | 9 | type Channel struct { 10 | options *ChannelOptions 11 | messageIn neo.MiddleHandler 12 | newContext neo.ContextFabric 13 | contexts map[int64]*neo.Context 14 | newContextCallbacks []*func(c *neo.Context) 15 | doneContextCallbacks []*func(c *neo.Context) 16 | } 17 | 18 | type ChannelOptions struct { 19 | AccessToken string 20 | VerifyToken string 21 | } 22 | 23 | func (stts *Channel) RegisterMessageEndpoint(handler neo.MiddleHandler) error { 24 | stts.messageIn = handler 25 | return nil 26 | } 27 | 28 | func (stts *Channel) ToHear() error { 29 | // TODO 30 | return nil 31 | } 32 | 33 | func (stts *Channel) GetContextFabric() neo.ContextFabric { 34 | return stts.newContext 35 | } 36 | 37 | func (stts *Channel) SetContextFabric(fabric neo.ContextFabric) { 38 | stts.newContext = fabric 39 | } 40 | 41 | func (stts *Channel) OnNewContextCreated(callback func(c *neo.Context)) { 42 | if stts.newContextCallbacks == nil { 43 | stts.newContextCallbacks = []*func(c *neo.Context){} 44 | } 45 | stts.newContextCallbacks = append(stts.newContextCallbacks, &callback) 46 | } 47 | 48 | func (stts *Channel) OnContextIsDone(callback func(c *neo.Context)) { 49 | if stts.doneContextCallbacks == nil { 50 | stts.doneContextCallbacks = []*func(c *neo.Context){} 51 | } 52 | stts.doneContextCallbacks = append(stts.doneContextCallbacks, &callback) 53 | } 54 | 55 | func (stts *Channel) CallContextDone(c *neo.Context) { 56 | id, err := strconv.ParseInt(c.Person.ID, 10, 64) 57 | if err == nil { 58 | delete(stts.contexts, id) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /channels/STTS/client.go: -------------------------------------------------------------------------------- 1 | package speech 2 | -------------------------------------------------------------------------------- /channels/STTS/input.go: -------------------------------------------------------------------------------- 1 | package speech 2 | 3 | import ( 4 | neo "github.com/minskylab/neocortex" 5 | ) 6 | 7 | func (stts *Channel) NewInput(data neo.InputData, i []neo.Intent, e []neo.Entity) *neo.Input { 8 | return &neo.Input{ 9 | Data: data, 10 | Intents: i, 11 | Entities: e, 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /channels/STTS/new.go: -------------------------------------------------------------------------------- 1 | package speech 2 | 3 | -------------------------------------------------------------------------------- /channels/STTS/speech_text.go: -------------------------------------------------------------------------------- 1 | package speech 2 | -------------------------------------------------------------------------------- /channels/STTS/text_speech.go: -------------------------------------------------------------------------------- 1 | package speech 2 | -------------------------------------------------------------------------------- /channels/facebook/channel.go: -------------------------------------------------------------------------------- 1 | package facebook 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "strconv" 8 | 9 | neo "github.com/minskylab/neocortex" 10 | "github.com/minskylab/neocortex/channels/facebook/messenger" 11 | ) 12 | 13 | type Channel struct { 14 | m *messenger.Messenger 15 | messageIn neo.MiddleHandler 16 | newContext neo.ContextFabric 17 | contexts map[int64]*neo.Context 18 | newContextCallbacks []*func(c *neo.Context) 19 | doneContextCallbacks []*func(c *neo.Context) 20 | } 21 | 22 | type ChannelOptions struct { 23 | AccessToken string 24 | VerifyToken string 25 | PageID string 26 | } 27 | 28 | func (fb *Channel) RegisterMessageEndpoint(handler neo.MiddleHandler) error { 29 | fb.messageIn = handler 30 | return nil 31 | } 32 | 33 | func (fb *Channel) ToHear() error { 34 | http.Handle("/fb-channel", fb.m) 35 | fmt.Println("listening on :8080 facebook webhook: /fb-channel") 36 | return http.ListenAndServe(":8080", nil) 37 | } 38 | 39 | func (fb *Channel) GetContextFabric() neo.ContextFabric { 40 | return fb.newContext 41 | } 42 | 43 | func (fb *Channel) SetContextFabric(fabric neo.ContextFabric) { 44 | fb.newContext = fabric 45 | } 46 | 47 | func (fb *Channel) OnNewContextCreated(callback func(c *neo.Context)) { 48 | if fb.newContextCallbacks == nil { 49 | fb.newContextCallbacks = []*func(c *neo.Context){} 50 | } 51 | fb.newContextCallbacks = append(fb.newContextCallbacks, &callback) 52 | } 53 | 54 | func (fb *Channel) OnContextIsDone(callback func(c *neo.Context)) { 55 | if fb.doneContextCallbacks == nil { 56 | fb.doneContextCallbacks = []*func(c *neo.Context){} 57 | } 58 | fb.doneContextCallbacks = append(fb.doneContextCallbacks, &callback) 59 | } 60 | 61 | func (fb *Channel) CallContextDone(c *neo.Context) { 62 | log.Println("fb, CallContextDone: ", c.SessionID) 63 | id, err := strconv.ParseInt(c.Person.ID, 10, 64) 64 | if err == nil { 65 | delete(fb.contexts, id) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /channels/facebook/input.go: -------------------------------------------------------------------------------- 1 | package facebook 2 | 3 | import neo "github.com/minskylab/neocortex" 4 | 5 | func (fb *Channel) NewInput(data neo.InputData, i []neo.Intent, e []neo.Entity) *neo.Input { 6 | return &neo.Input{ 7 | Data: data, 8 | Intents: i, 9 | Entities: e, 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /channels/facebook/input_text.go: -------------------------------------------------------------------------------- 1 | package facebook 2 | 3 | import neo "github.com/minskylab/neocortex" 4 | 5 | func (fb *Channel) NewInputText(text string, i []neo.Intent, e []neo.Entity) *neo.Input { 6 | t := neo.InputData{ 7 | Type: neo.InputText, 8 | Value: text, 9 | Data: []byte(text), 10 | } 11 | return fb.NewInput(t, i, e) 12 | } 13 | -------------------------------------------------------------------------------- /channels/facebook/messenger/facebook.go: -------------------------------------------------------------------------------- 1 | package messenger 2 | 3 | import "fmt" 4 | 5 | // FacebookRequest received from Facebook server on webhook, contains messages, delivery reports and/or postbacks 6 | type FacebookRequest struct { 7 | Entry []struct { 8 | ID int64 `json:"id"` 9 | Messaging []struct { 10 | Recipient struct { 11 | ID int64 `json:"id,string"` 12 | PhoneNumber string `json:"phone_number"` 13 | Name struct { 14 | FirstName string `json:"first_name"` 15 | LastName string `json:"last_name"` 16 | } `json:"name"` 17 | } `json:"recipient"` 18 | Sender struct { 19 | ID int64 `json:"id,string"` 20 | } `json:"sender"` 21 | Timestamp int `json:"timestamp"` 22 | Message *FacebookMessage `json:"message,omitempty"` 23 | Delivery *FacebookDelivery `json:"delivery"` 24 | Postback *FacebookPostback `json:"postback"` 25 | } `json:"messaging"` 26 | Time int `json:"time"` 27 | } `json:"entry"` 28 | Object string `json:"object"` 29 | } 30 | 31 | // FacebookMessage struct for text messaged received from facebook server as part of FacebookRequest struct 32 | type FacebookMessage struct { 33 | Mid string `json:"mid"` 34 | Seq int `json:"seq"` 35 | Text string `json:"text"` 36 | } 37 | 38 | // FacebookDelivery struct for delivery reports received from Facebook server as part of FacebookRequest struct 39 | type FacebookDelivery struct { 40 | Mids []string `json:"mids"` 41 | Seq int `json:"seq"` 42 | Watermark int `json:"watermark"` 43 | } 44 | 45 | // FacebookPostback struct for postbacks received from Facebook server as part of FacebookRequest struct 46 | type FacebookPostback struct { 47 | Payload string `json:"payload"` 48 | } 49 | 50 | // rawFBResponse received from Facebook server after sending the message 51 | // if Error is null we copy this into FacebookResponse object 52 | type rawFBResponse struct { 53 | MessageID string `json:"message_id"` 54 | RecipientID int64 `json:"recipient_id,string"` 55 | Error *FacebookError `json:"error"` 56 | } 57 | 58 | // FacebookResponse received from Facebook server after sending the message 59 | type FacebookResponse struct { 60 | MessageID string `json:"message_id"` 61 | RecipientID int64 `json:"recipient_id,string"` 62 | } 63 | 64 | // FacebookError received form Facebook server if sending messages failed 65 | type FacebookError struct { 66 | Code int `json:"code"` 67 | FbtraceID string `json:"fbtrace_id"` 68 | Message string `json:"message"` 69 | Type string `json:"type"` 70 | } 71 | 72 | // Error returns Go error object constructed from FacebookError data 73 | func (err *FacebookError) Error() error { 74 | return fmt.Errorf("FB Error: Type %s: %s; FB trace ID: %s", err.Type, err.Message, err.FbtraceID) 75 | } 76 | -------------------------------------------------------------------------------- /channels/facebook/messenger/messages.go: -------------------------------------------------------------------------------- 1 | package messenger 2 | 3 | // ButtonType for buttons, it can be ButtonTypeWebURL or ButtonTypePostback 4 | type ButtonType string 5 | 6 | // AttachmentType describes attachment type in GenericMessage 7 | type AttachmentType string 8 | 9 | // TemplateType of template in GenericMessage 10 | type TemplateType string 11 | 12 | // NotificationType for sent messages 13 | type NotificationType string 14 | 15 | // Message interface that represents all type of messages that we can send to Facebook Messenger 16 | type Message interface { 17 | foo() 18 | } 19 | 20 | func (m TextMessage) foo() {} // Message interface 21 | func (m GenericMessage) foo() {} // Message interface 22 | 23 | const ( 24 | // ButtonTypeWebURL is type for web links 25 | ButtonTypeWebURL = ButtonType("web_url") 26 | 27 | //ButtonTypePostback is type for postback buttons that sends data back to webhook 28 | ButtonTypePostback = ButtonType("postback") 29 | 30 | // AttachmentTypeTemplate for template attachments 31 | AttachmentTypeTemplate = AttachmentType("template") 32 | 33 | // TemplateTypeGeneric for generic message templates 34 | TemplateTypeGeneric = TemplateType("generic") 35 | 36 | // NotificationTypeRegular for regular notification type 37 | NotificationTypeRegular = NotificationType("REGULAR") 38 | 39 | // NotificationTypeSilentPush for silent push 40 | NotificationTypeSilentPush = NotificationType("SILENT_PUSH") 41 | 42 | // NotificationTypeNoPush for no push 43 | NotificationTypeNoPush = NotificationType("NO_PUSH") 44 | ) 45 | 46 | // TextMessage struct used for sending text messages to messenger 47 | type TextMessage struct { 48 | Message textMessageContent `json:"message"` 49 | Recipient recipient `json:"recipient"` 50 | NotificationType NotificationType `json:"notification_type,omitempty"` 51 | } 52 | 53 | // GenericMessage struct used for sending structural messages to messenger (messages with images, links, and buttons) 54 | type GenericMessage struct { 55 | Message genericMessageContent `json:"message"` 56 | Recipient recipient `json:"recipient"` 57 | NotificationType NotificationType `json:"notification_type,omitempty"` 58 | } 59 | 60 | type recipient struct { 61 | ID int64 `json:"id,string"` 62 | } 63 | 64 | type textMessageContent struct { 65 | Text string `json:"text,omitempty"` 66 | } 67 | 68 | type genericMessageContent struct { 69 | Attachment *attachment `json:"attachment,omitempty"` 70 | } 71 | 72 | type attachment struct { 73 | Type string `json:"type,omitempty"` 74 | Payload payload `json:"payload,omitempty"` 75 | } 76 | 77 | type payload struct { 78 | Url string `json:"url,omitempty"` 79 | IsReusable bool `json:"is_reusable,omitempty"` 80 | TemplateType string `json:"template_type,omitempty"` 81 | Elements []Element `json:"elements,omitempty"` 82 | } 83 | 84 | // Element in Generic Message template attachment 85 | type Element struct { 86 | Title string `json:"title"` 87 | Subtitle string `json:"subtitle,omitempty"` 88 | ItemURL string `json:"item_url,omitempty"` 89 | ImageURL string `json:"image_url,omitempty"` 90 | Buttons []Button `json:"buttons,omitempty"` 91 | } 92 | 93 | // Button on Generic Message template element 94 | type Button struct { 95 | Type ButtonType `json:"type"` 96 | URL string `json:"url,omitempty"` 97 | Title string `json:"title"` 98 | Payload string `json:"payload,omitempty"` 99 | } 100 | 101 | // NewTextMessage creates new text message for userID 102 | // This function is here for convenient reason, you will 103 | // probably use shorthand version SentTextMessage which sends message immediatly 104 | func (msng Messenger) NewTextMessage(userID int64, text string) TextMessage { 105 | return TextMessage{ 106 | Recipient: recipient{ID: userID}, 107 | Message: textMessageContent{Text: text}, 108 | } 109 | } 110 | 111 | // NewGenericMessage creates new Generic Template message for userID 112 | // Generic template messages are used for structured messages with images, links, buttons and postbacks 113 | func (msng Messenger) NewGenericMessage(userID int64) GenericMessage { 114 | return GenericMessage{ 115 | Recipient: recipient{ID: userID}, 116 | Message: genericMessageContent{ 117 | Attachment: &attachment{ 118 | Type: "template", 119 | Payload: payload{TemplateType: "generic"}, 120 | }, 121 | }, 122 | } 123 | } 124 | 125 | func (msng Messenger) NewImageMessage(userID int64, imageURL string) GenericMessage { 126 | return GenericMessage{ 127 | Recipient: recipient{ID: userID}, 128 | Message: genericMessageContent{ 129 | Attachment: &attachment{ 130 | Type: "image", 131 | Payload: payload{Url: imageURL, IsReusable: true}, 132 | }, 133 | }, 134 | } 135 | } 136 | 137 | // AddNewElement adds element to Generic template message with defined title, subtitle, link url and image url 138 | // Title param is mandatory. If not used set "" for other params and nil for buttons param 139 | // Generic messages can have up to 10 elements which are scolled horizontaly in Facebook messenger 140 | func (m *GenericMessage) AddNewElement(title, subtitle, itemURL, imageURL string, buttons []Button) { 141 | m.AddElement(newElement(title, subtitle, itemURL, imageURL, buttons)) 142 | } 143 | 144 | // AddElement adds element e to Generic Message 145 | // Generic messages can have up to 10 elements which are scolled horizontaly in Facebook messenger 146 | func (m *GenericMessage) AddElement(e Element) { 147 | m.Message.Attachment.Payload.Elements = append(m.Message.Attachment.Payload.Elements, e) 148 | } 149 | 150 | // NewElement creates new element with defined title, subtitle, link url and image url 151 | // Title param is mandatory. If not used set "" for other params and nil for buttons param 152 | // Instead of calling this function you can also initialize Element struct, depends what you prefere 153 | func (msng Messenger) NewElement(title, subtitle, itemURL, imageURL string, buttons []Button) Element { 154 | return newElement(title, subtitle, itemURL, imageURL, buttons) 155 | } 156 | 157 | func newElement(title, subtitle, itemURL, imageURL string, buttons []Button) Element { 158 | return Element{ 159 | Title: title, 160 | Subtitle: subtitle, 161 | ItemURL: itemURL, 162 | ImageURL: imageURL, 163 | Buttons: buttons, 164 | } 165 | } 166 | 167 | // NewWebURLButton creates new web url button 168 | func (msng Messenger) NewWebURLButton(title, URL string) Button { 169 | return Button{ 170 | Type: ButtonTypeWebURL, 171 | Title: title, 172 | URL: URL, 173 | } 174 | } 175 | 176 | // NewPostbackButton creates new postback button that sends payload string back to webhook when pressed 177 | func (msng Messenger) NewPostbackButton(title, payload string) Button { 178 | return Button{ 179 | Type: ButtonTypePostback, 180 | Title: title, 181 | Payload: payload, 182 | } 183 | } 184 | 185 | // AddWebURLButton creates and adds web link URL button to the element 186 | func (e *Element) AddWebURLButton(title, URL string) { 187 | b := Button{ 188 | Type: ButtonTypeWebURL, 189 | Title: title, 190 | URL: URL, 191 | } 192 | e.Buttons = append(e.Buttons, b) 193 | } 194 | 195 | // AddPostbackButton creates and adds button that sends payload string back to webhook when pressed 196 | func (e *Element) AddPostbackButton(title, payload string) { 197 | b := Button{ 198 | Type: ButtonTypePostback, 199 | Title: title, 200 | Payload: payload, 201 | } 202 | e.Buttons = append(e.Buttons, b) 203 | } 204 | -------------------------------------------------------------------------------- /channels/facebook/messenger/messenger.go: -------------------------------------------------------------------------------- 1 | package messenger 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | ) 9 | 10 | const apiURL = "https://graph.facebook.com/v2.6/" 11 | const urlTemplate = "https://graph.facebook.com/%d?fields=first_name,last_name,timezone&access_token=%s" 12 | 13 | // TestURL to mock FB server, used for testing 14 | var TestURL = "" 15 | 16 | type UserInfo struct { 17 | ID int64 18 | Name string 19 | Timezone float64 20 | Locale string 21 | ProfilePic string 22 | } 23 | 24 | // Messenger struct 25 | type Messenger struct { 26 | AccessToken string 27 | VerifyToken string 28 | PageID string 29 | 30 | apiURL string 31 | pageURL string 32 | 33 | // MessageReceived event fires when message from Facebook received 34 | MessageReceived func(msng *Messenger, user UserInfo, m FacebookMessage) 35 | 36 | // DeliveryReceived event fires when delivery report from Facebook received 37 | // Omit (nil) if you don't want to manage this events 38 | DeliveryReceived func(msng *Messenger, user UserInfo, d FacebookDelivery) 39 | 40 | // PostbackReceived event fires when postback received from Facebook server 41 | // Omit (nil) if you don't use postbacks and you don't want to manage this events 42 | PostbackReceived func(msng *Messenger, user UserInfo, p FacebookPostback) 43 | } 44 | 45 | // New creates new messenger instance 46 | func New(accessToken, pageID string) Messenger { 47 | return Messenger{ 48 | AccessToken: accessToken, 49 | PageID: pageID, 50 | } 51 | } 52 | 53 | // SendMessage sends chat message 54 | func (msng *Messenger) SendMessage(m Message) (FacebookResponse, error) { 55 | if msng.apiURL == "" { 56 | if TestURL != "" { 57 | msng.apiURL = TestURL + "me/messages?access_token=" + msng.AccessToken // testing, mock FB URL 58 | } else { 59 | msng.apiURL = apiURL + "me/messages?access_token=" + msng.AccessToken 60 | } 61 | } 62 | 63 | s, _ := json.Marshal(m) 64 | req, err := http.NewRequest("POST", msng.apiURL, bytes.NewBuffer(s)) 65 | req.Header.Set("Content-Type", "application/json") 66 | 67 | client := &http.Client{} 68 | resp, err := client.Do(req) 69 | if err != nil { 70 | return FacebookResponse{}, err 71 | } 72 | 73 | return decodeResponse(resp) 74 | } 75 | 76 | // SendTextMessage sends text messate to receiverID 77 | // it is shorthand instead of crating new text message and then sending it 78 | func (msng Messenger) SendTextMessage(receiverID int64, text string) (FacebookResponse, error) { 79 | m := msng.NewTextMessage(receiverID, text) 80 | return msng.SendMessage(&m) 81 | } 82 | 83 | // ServeHTTP is HTTP handler for Messenger so it could be directly used as http.Handler 84 | func (msng *Messenger) ServeHTTP(w http.ResponseWriter, r *http.Request) { 85 | msng.VerifyWebhook(w, r) // verify webhook if needed 86 | fbRq, _ := DecodeRequest(r) // get FacebookRequest object 87 | 88 | for _, entry := range fbRq.Entry { 89 | for _, msg := range entry.Messaging { 90 | userID := msg.Sender.ID 91 | 92 | url := fmt.Sprintf(urlTemplate, userID, msng.AccessToken) 93 | r, _ := http.Get(url) 94 | 95 | type response struct { 96 | FirstName string `json:"first_name"` 97 | LastName string `json:"last_name"` 98 | Timezone float64 `json:"timezone"` 99 | } 100 | resp := new(response) 101 | 102 | _ = json.NewDecoder(r.Body).Decode(resp) 103 | 104 | name := resp.FirstName + " " + resp.LastName 105 | tz := resp.Timezone 106 | // locale := data["locale"].(string) 107 | // pic := data["profile_pic"].(string) 108 | user := UserInfo{ 109 | ID: userID, 110 | Name: name, 111 | Timezone: tz, 112 | // Locale: locale, 113 | // ProfilePic: pic, 114 | } 115 | 116 | switch { 117 | case msg.Message != nil && msng.MessageReceived != nil: 118 | go msng.MessageReceived(msng, user, *msg.Message) 119 | 120 | case msg.Delivery != nil && msng.DeliveryReceived != nil: 121 | go msng.DeliveryReceived(msng, user, *msg.Delivery) 122 | 123 | case msg.Postback != nil && msng.PostbackReceived != nil: 124 | go msng.PostbackReceived(msng, user, *msg.Postback) 125 | } 126 | } 127 | } 128 | } 129 | 130 | // VerifyWebhook verifies your webhook by checking VerifyToken and sending challange back to Facebook 131 | func (msng Messenger) VerifyWebhook(w http.ResponseWriter, r *http.Request) { 132 | // Facebook sends this query for verifying webhooks 133 | // hub.mode=subscribe&hub.challenge=1085525140&hub.verify_token=moj_token 134 | if r.FormValue("hub.mode") == "subscribe" { 135 | if r.FormValue("hub.verify_token") == msng.VerifyToken { 136 | w.Write([]byte(r.FormValue("hub.challenge"))) 137 | return 138 | } 139 | } 140 | } 141 | 142 | // DecodeRequest decodes http request from FB messagner to FacebookRequest struct 143 | // DecodeRequest will close the Body reader 144 | // Usually you don't have to use DecodeRequest if you setup events for specific types 145 | func DecodeRequest(r *http.Request) (FacebookRequest, error) { 146 | defer r.Body.Close() 147 | var fbRq FacebookRequest 148 | err := json.NewDecoder(r.Body).Decode(&fbRq) 149 | return fbRq, err 150 | } 151 | 152 | // decodeResponse decodes Facebook response after sending message, usually contains MessageID or Error 153 | func decodeResponse(r *http.Response) (FacebookResponse, error) { 154 | defer r.Body.Close() 155 | var fbResp rawFBResponse 156 | err := json.NewDecoder(r.Body).Decode(&fbResp) 157 | if err != nil { 158 | return FacebookResponse{}, err 159 | } 160 | 161 | if fbResp.Error != nil { 162 | return FacebookResponse{}, fbResp.Error.Error() 163 | } 164 | 165 | return FacebookResponse{ 166 | MessageID: fbResp.MessageID, 167 | RecipientID: fbResp.RecipientID, 168 | }, nil 169 | } 170 | -------------------------------------------------------------------------------- /channels/facebook/new.go: -------------------------------------------------------------------------------- 1 | package facebook 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "strconv" 8 | 9 | neo "github.com/minskylab/neocortex" 10 | "github.com/minskylab/neocortex/channels/facebook/messenger" 11 | ) 12 | 13 | func NewChannel(options ChannelOptions, fabric ...neo.ContextFabric) (*Channel, error) { 14 | fb := &Channel{ 15 | contexts: map[int64]*neo.Context{}, 16 | } 17 | 18 | if len(fabric) > 0 { 19 | f := fabric[0] 20 | fb.newContext = f 21 | } 22 | 23 | hook := func(msn *messenger.Messenger, user messenger.UserInfo, m messenger.FacebookMessage) { 24 | uID := strconv.FormatInt(user.ID, 10) 25 | tz := fmt.Sprintf("%d", int(user.Timezone)) 26 | c, contextExist := fb.contexts[user.ID] 27 | 28 | if !contextExist { 29 | c = fb.newContext(context.Background(), neo.PersonInfo{ 30 | ID: uID, 31 | Timezone: tz, 32 | Name: user.Name, 33 | Locale: user.Locale, 34 | Picture: user.ProfilePic, 35 | }) 36 | 37 | for _, call := range fb.newContextCallbacks { 38 | (*call)(c) 39 | } 40 | fb.contexts[user.ID] = c 41 | } 42 | 43 | // This is because facebook channel not support entities or intents as input (from messenger chat) 44 | in := fb.NewInputText(m.Text, nil, nil) 45 | err := fb.messageIn(c, in, func(c *neo.Context, out *neo.Output) error { 46 | fb.contexts[user.ID] = c 47 | err := decodeOutput(user.ID, msn, out) 48 | 49 | if err != nil { 50 | return err 51 | } 52 | return nil 53 | }) 54 | if err != nil { 55 | log.Println(err) 56 | } 57 | } 58 | 59 | postbackHook := func(msn *messenger.Messenger, user messenger.UserInfo, p messenger.FacebookPostback) { 60 | text := p.Payload 61 | 62 | uID := strconv.FormatInt(user.ID, 10) 63 | tz := fmt.Sprintf("%d", int(user.Timezone)) 64 | c, contextExist := fb.contexts[user.ID] 65 | if !contextExist { 66 | c = fb.newContext(context.Background(), neo.PersonInfo{ 67 | ID: uID, 68 | Timezone: tz, 69 | Name: user.Name, 70 | }) 71 | 72 | for _, call := range fb.newContextCallbacks { 73 | (*call)(c) 74 | } 75 | fb.contexts[user.ID] = c 76 | } 77 | // This is because facebook channel not support entities or intents as input (from messenger chat) 78 | in := fb.NewInputText(text, nil, nil) 79 | err := fb.messageIn(c, in, func(c *neo.Context, out *neo.Output) error { 80 | fb.contexts[user.ID] = c 81 | err := decodeOutput(user.ID, msn, out) 82 | 83 | if err != nil { 84 | return err 85 | } 86 | return nil 87 | }) 88 | if err != nil { 89 | log.Println(err) 90 | } 91 | } 92 | 93 | fb.m = &messenger.Messenger{ 94 | AccessToken: options.AccessToken, 95 | VerifyToken: options.VerifyToken, 96 | PageID: options.PageID, 97 | MessageReceived: hook, 98 | PostbackReceived: postbackHook, 99 | } 100 | 101 | return fb, nil 102 | } 103 | -------------------------------------------------------------------------------- /channels/facebook/output.go: -------------------------------------------------------------------------------- 1 | package facebook 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/minskylab/neocortex" 8 | "github.com/minskylab/neocortex/channels/facebook/messenger" 9 | ) 10 | 11 | func decodeOutput(userID int64, msn *messenger.Messenger, out *neocortex.Output) error { 12 | for _, r := range out.Responses { 13 | switch r.Type { 14 | case neocortex.Text: 15 | if err := sendTextResponse(userID, msn, r.Value.(string)); err != nil { 16 | return err 17 | } 18 | case neocortex.Options: 19 | options, isOne := r.Value.(neocortex.OptionsResponse) 20 | optionsArray, isArray := r.Value.([]neocortex.OptionsResponse) 21 | 22 | if isOne && !isArray { 23 | if err := sendOneOptionResponse(userID, msn, options); err != nil { 24 | return err 25 | } 26 | } else if !isOne && isArray { 27 | if err := sendManyOptionsResponse(userID, msn, optionsArray); err != nil { 28 | return err 29 | } 30 | } else { 31 | return errors.New("invalid value, it cannot be parsed as OptionResponse struct") 32 | } 33 | return nil 34 | 35 | case neocortex.Pause: 36 | // * Unsupported by facebook messenger 37 | // * emulated with delay (it's so stupid) 38 | if err := sendPauseResponse(userID, msn, r.Value); err != nil { 39 | return err 40 | } 41 | case neocortex.Image: 42 | if url, ok := r.Value.(string); ok { 43 | return sendImageResponse(userID, msn, url) 44 | } 45 | return errors.New("invalid value, it must be a string") 46 | case neocortex.Suggestion: 47 | // * Unsupported by facebook messenger 48 | case neocortex.Unknown: 49 | // * Unsupported by facebook messenger 50 | default: 51 | // by default neocortex sends a raw stringify of the value 52 | _, err := msn.SendTextMessage(userID, fmt.Sprintf("%v", r.Value)) 53 | if err != nil { 54 | return err 55 | } 56 | } 57 | } 58 | 59 | return nil 60 | } 61 | -------------------------------------------------------------------------------- /channels/facebook/output_image_response.go: -------------------------------------------------------------------------------- 1 | package facebook 2 | 3 | import "github.com/minskylab/neocortex/channels/facebook/messenger" 4 | 5 | func sendImageResponse(userID int64, msn *messenger.Messenger, url string) error { 6 | gm := msn.NewImageMessage(userID, url) 7 | _, err := msn.SendMessage(gm) 8 | return err 9 | } 10 | -------------------------------------------------------------------------------- /channels/facebook/output_option_response.go: -------------------------------------------------------------------------------- 1 | package facebook 2 | 3 | import ( 4 | "github.com/minskylab/neocortex" 5 | "github.com/minskylab/neocortex/channels/facebook/messenger" 6 | ) 7 | 8 | func sendOneOptionResponse(userID int64, msn *messenger.Messenger, options neocortex.OptionsResponse) error { 9 | gm := msn.NewGenericMessage(userID) 10 | buttons := make([]messenger.Button, 0) 11 | for _, o := range options.Options { 12 | if o.IsPostBack { 13 | buttons = append(buttons, msn.NewPostbackButton(o.Text, o.Action)) 14 | } else { 15 | buttons = append(buttons, msn.NewWebURLButton(o.Text, o.Action)) 16 | } 17 | } 18 | gm.AddNewElement(options.Title, options.Description, options.ItemURL, options.Image, buttons) 19 | _, err := msn.SendMessage(gm) 20 | return err 21 | } 22 | 23 | func sendManyOptionsResponse(userID int64, msn *messenger.Messenger, optionsArray []neocortex.OptionsResponse) error { 24 | gm := msn.NewGenericMessage(userID) 25 | for _, options := range optionsArray { 26 | buttons := make([]messenger.Button, 0) 27 | for _, o := range options.Options { 28 | if o.IsPostBack { 29 | buttons = append(buttons, msn.NewPostbackButton(o.Text, o.Action)) 30 | } else { 31 | buttons = append(buttons, msn.NewWebURLButton(o.Text, o.Action)) 32 | } 33 | } 34 | gm.AddNewElement(options.Title, options.Description, options.ItemURL, options.Image, buttons) 35 | } 36 | _, err := msn.SendMessage(gm) 37 | return err 38 | } 39 | -------------------------------------------------------------------------------- /channels/facebook/output_pause_response.go: -------------------------------------------------------------------------------- 1 | package facebook 2 | 3 | import ( 4 | "errors" 5 | "github.com/minskylab/neocortex/channels/facebook/messenger" 6 | "strconv" 7 | "time" 8 | ) 9 | 10 | func sendPauseResponse(userID int64, msn *messenger.Messenger, pause interface{}) error { 11 | switch pause.(type) { 12 | case int: // in milliseconds 13 | delay := time.Duration(pause.(int64)) 14 | time.Sleep(time.Millisecond * delay) 15 | case time.Duration: 16 | delay := pause.(time.Duration) 17 | time.Sleep(delay) 18 | case string: // in milliseconds 19 | delay, err := strconv.Atoi(pause.(string)) 20 | if err != nil { 21 | return err 22 | } 23 | time.Sleep(time.Millisecond * time.Duration(delay)) 24 | default: 25 | return errors.New("invalid value, it cannot be parsed as duration or same") 26 | } 27 | return nil 28 | } 29 | -------------------------------------------------------------------------------- /channels/facebook/output_text_response.go: -------------------------------------------------------------------------------- 1 | package facebook 2 | 3 | import "github.com/minskylab/neocortex/channels/facebook/messenger" 4 | 5 | func sendTextResponse(userID int64, msn *messenger.Messenger, text string) error { 6 | _, err := msn.SendTextMessage(userID, text) 7 | if err != nil { 8 | return err 9 | } 10 | return nil 11 | } 12 | -------------------------------------------------------------------------------- /channels/telegram/channel.go: -------------------------------------------------------------------------------- 1 | package telegram 2 | 3 | import ( 4 | "github.com/minskylab/neocortex" 5 | ) 6 | 7 | type Channel struct {} 8 | 9 | func (c *Channel) RegisterMessageEndpoint(handler neocortex.MiddleHandler) error { 10 | panic("implement me") 11 | } 12 | 13 | func (c *Channel) ToHear() error { 14 | panic("implement me") 15 | } 16 | 17 | func (c *Channel) GetContextFabric() neocortex.ContextFabric { 18 | panic("implement me") 19 | } 20 | 21 | func (c *Channel) SetContextFabric(fabric neocortex.ContextFabric) { 22 | panic("implement me") 23 | } 24 | 25 | func (c *Channel) OnNewContextCreated(callback func(c *neocortex.Context)) { 26 | panic("implement me") 27 | } 28 | 29 | func (c *Channel) OnContextIsDone(callback func(c *neocortex.Context)) { 30 | panic("implement me") 31 | } 32 | 33 | func (c *Channel) CallContextDone(c *neocortex.Context) { 34 | panic("implement me") 35 | } 36 | -------------------------------------------------------------------------------- /channels/terminal/channel.go: -------------------------------------------------------------------------------- 1 | package terminal 2 | 3 | import ( 4 | "bufio" 5 | "strconv" 6 | 7 | neo "github.com/minskylab/neocortex" 8 | ) 9 | 10 | type Channel struct { 11 | reader *bufio.Reader 12 | options *ChannelOptions 13 | messageIn neo.MiddleHandler // req 14 | newContext neo.ContextFabric // req 15 | contexts map[int64]*neo.Context // req 16 | newContextCallbacks []*func(c *neo.Context) 17 | doneContextCallbacks []*func(c *neo.Context) 18 | } 19 | 20 | func (term *Channel) RegisterMessageEndpoint(handler neo.MiddleHandler) error { 21 | term.messageIn = handler 22 | return nil 23 | } 24 | 25 | func (term *Channel) ToHear() error { 26 | return term.renderUserInterface(false) 27 | } 28 | 29 | func (term *Channel) GetContextFabric() neo.ContextFabric { 30 | return term.newContext 31 | } 32 | 33 | func (term *Channel) SetContextFabric(fabric neo.ContextFabric) { 34 | term.newContext = fabric 35 | } 36 | 37 | func (term *Channel) OnNewContextCreated(callback func(c *neo.Context)) { 38 | if term.newContextCallbacks == nil { 39 | term.newContextCallbacks = []*func(c *neo.Context){} 40 | } 41 | term.newContextCallbacks = append(term.newContextCallbacks, &callback) 42 | } 43 | 44 | func (term *Channel) OnContextIsDone(callback func(c *neo.Context)) { 45 | if term.doneContextCallbacks == nil { 46 | term.doneContextCallbacks = []*func(c *neo.Context){} 47 | } 48 | term.doneContextCallbacks = append(term.doneContextCallbacks, &callback) 49 | } 50 | 51 | func (term *Channel) CallContextDone(c *neo.Context) { 52 | id, err := strconv.ParseInt(c.Person.ID, 10, 64) 53 | if err == nil { 54 | delete(term.contexts, id) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /channels/terminal/input.go: -------------------------------------------------------------------------------- 1 | package terminal 2 | 3 | import neo "github.com/minskylab/neocortex" 4 | 5 | func (term *Channel) NewInput(data neo.InputData, i []neo.Intent, e []neo.Entity) *neo.Input { 6 | return &neo.Input{ 7 | Data: data, 8 | Intents: i, 9 | Entities: e, 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /channels/terminal/input_text.go: -------------------------------------------------------------------------------- 1 | package terminal 2 | 3 | import neo "github.com/minskylab/neocortex" 4 | 5 | func (term *Channel) NewInputText(text string, i []neo.Intent, e []neo.Entity) *neo.Input { 6 | t := neo.InputData{ 7 | Type: neo.InputText, 8 | Value: text, 9 | Data: []byte(text), 10 | } 11 | return term.NewInput(t, i, e) 12 | } 13 | -------------------------------------------------------------------------------- /channels/terminal/new.go: -------------------------------------------------------------------------------- 1 | package terminal 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "fmt" 7 | "os" 8 | "strconv" 9 | 10 | neo "github.com/minskylab/neocortex" 11 | ) 12 | 13 | // This channel have particularity only one user, then we have only one user ID for all 14 | const uniqueUserID = 0 15 | 16 | type ChannelOptions struct { 17 | PersonIcon string 18 | PersonName string 19 | BotIcon string 20 | BotName string 21 | SaysSymbol string 22 | } 23 | 24 | func NewChannel(opts *ChannelOptions, fabric ...neo.ContextFabric) *Channel { 25 | if opts == nil { // default 26 | opts = &ChannelOptions{ 27 | PersonIcon: "😀", 28 | PersonName: "User", 29 | BotIcon: "🤖", 30 | BotName: "Bot", 31 | SaysSymbol: " >", 32 | } 33 | } 34 | 35 | t := &Channel{ 36 | reader: bufio.NewReader(os.Stdin), 37 | options: opts, 38 | contexts: map[int64]*neo.Context{}, 39 | } 40 | 41 | if len(fabric) > 0 { 42 | f := fabric[0] 43 | t.newContext = f 44 | } 45 | 46 | return t 47 | } 48 | func (term *Channel) getInput() string { 49 | input, _ := term.reader.ReadString('\n') 50 | input = input[:len(input)-1] 51 | return input 52 | } 53 | 54 | func (term *Channel) renderUserInterface(done bool) error { 55 | c, contextExist := term.contexts[uniqueUserID] 56 | if !contextExist { 57 | c = term.newContext(context.Background(), neo.PersonInfo{ 58 | ID: strconv.Itoa(uniqueUserID), 59 | Name: "Jhon Doe", 60 | }) 61 | 62 | for _, call := range term.newContextCallbacks { 63 | (*call)(c) 64 | } 65 | term.contexts[uniqueUserID] = c 66 | } 67 | if !done { 68 | fmt.Printf("%s %s[sess:%s]%s ", term.options.PersonIcon, term.options.PersonName, c.SessionID, term.options.SaysSymbol) 69 | inStr := term.getInput() 70 | input := term.NewInputText(inStr, nil, nil) 71 | err := term.messageIn(c, input, func(c *neo.Context, out *neo.Output) error { 72 | for _, r := range out.Responses { 73 | if r.Type == neo.Text { 74 | fmt.Printf("%s %s[sess:%s]%s %s\n", 75 | term.options.BotIcon, 76 | term.options.BotName, 77 | c.SessionID, 78 | term.options.SaysSymbol, 79 | r.Value.(string), 80 | ) 81 | } 82 | } 83 | return nil 84 | }) 85 | 86 | if err != nil { 87 | return err 88 | } 89 | 90 | err = term.renderUserInterface(false) 91 | if err != nil { 92 | return err 93 | } 94 | return nil 95 | } else { 96 | return nil 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /cli/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | ) 7 | 8 | func main() { 9 | filepath := flag.String("file", "skill-Basics.json", "") 10 | flag.Parse() 11 | 12 | err := ExamineWatsonSkill(*filepath) 13 | if err != nil { 14 | panic(err) 15 | } 16 | fmt.Println("Skill go files created!") 17 | } 18 | -------------------------------------------------------------------------------- /cli/tools.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "github.com/iancoleman/strcase" 8 | "io/ioutil" 9 | "os/exec" 10 | "strings" 11 | ) 12 | 13 | type Intent struct { 14 | Name string `json:"intent"` 15 | Description string `json:"description"` 16 | } 17 | 18 | type Entity struct { 19 | Name string `json:"entity"` 20 | FuzzyMatch bool `json:"fuzzy_match"` 21 | } 22 | 23 | type Skill struct { 24 | Intents []*Intent `json:"intents"` 25 | Entities []*Entity `json:"entities"` 26 | } 27 | 28 | func ExamineWatsonSkill(skillFile string) error { 29 | data, err := ioutil.ReadFile(skillFile) 30 | if err != nil { 31 | return err 32 | } 33 | skill := new(Skill) 34 | err = json.Unmarshal(data, skill) 35 | if err != nil { 36 | return nil 37 | } 38 | 39 | f := strings.Split(skillFile, ".") 40 | filename := strcase.ToSnake(f[0]) + ".go" 41 | 42 | packageName := "skills" 43 | fileBlank := fmt.Sprintf("package %s\n\n", packageName) 44 | fileBody := bytes.NewBufferString(fileBlank) 45 | 46 | fileBody.WriteString("// Intents constants\n") 47 | fileBody.WriteString("const (\n\t") 48 | for _, i := range skill.Intents { 49 | constName := strcase.ToCamel(i.Name) 50 | line := fmt.Sprintf(`%s = "%s" 51 | `, constName, i.Name) 52 | _, err = fileBody.WriteString(line) 53 | if err != nil { 54 | return err 55 | } 56 | } 57 | 58 | fileBody.WriteString(")\n\n") 59 | 60 | fileBody.WriteString("// Entities constants\n") 61 | fileBody.WriteString("const (\n\t") 62 | for _, e := range skill.Entities { 63 | constName := strcase.ToCamel(e.Name) 64 | line := fmt.Sprintf(`%s = "%s" 65 | `, constName, e.Name) 66 | _, err = fileBody.WriteString(line) 67 | if err != nil { 68 | return err 69 | } 70 | } 71 | 72 | fileBody.WriteString(")\n\n") 73 | 74 | err = ioutil.WriteFile(filename, fileBody.Bytes(), 0644) 75 | if err != nil { 76 | return err 77 | } 78 | 79 | err = exec.Command("go", "fmt").Run() 80 | if err != nil { 81 | return err 82 | } 83 | 84 | return nil 85 | } 86 | -------------------------------------------------------------------------------- /cognitive.go: -------------------------------------------------------------------------------- 1 | package neocortex 2 | 3 | import "context" 4 | 5 | type CognitiveService interface { 6 | CreateNewContext(c *context.Context, info PersonInfo) *Context 7 | OnContextIsDone(callback func(c *Context)) 8 | GetProtoResponse(c *Context, in *Input) (*Output, error) 9 | } 10 | -------------------------------------------------------------------------------- /cognitive/dialogflow/cognitive.go: -------------------------------------------------------------------------------- 1 | package dialogflow 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | ) 7 | 8 | type Cognitive struct { 9 | tokenIdentify string 10 | Url string 11 | } 12 | 13 | type NewCognitiveParams struct { 14 | Url string 15 | AccessToken string 16 | Version string 17 | sessionId string 18 | } 19 | 20 | func NewCognitive(params NewCognitiveParams) (*Cognitive, error) { 21 | if (params.AccessToken == "" || reflect.DeepEqual(params, NewCognitiveParams{})) { 22 | return nil, errors.New("No token found") 23 | } 24 | 25 | client := &Cognitive{ 26 | Url: params.Url, 27 | } 28 | 29 | return client, nil 30 | } 31 | -------------------------------------------------------------------------------- /cognitive/uselessbox/cognitive.go: -------------------------------------------------------------------------------- 1 | package uselessbox 2 | 3 | import ( 4 | "context" 5 | 6 | neo "github.com/minskylab/neocortex" 7 | "github.com/rs/xid" 8 | ) 9 | 10 | type Cognitive struct { 11 | doneContextCallbacks []*func(c *neo.Context) 12 | } 13 | 14 | func NewCognitive() *Cognitive { 15 | return &Cognitive{} 16 | } 17 | 18 | func (useless *Cognitive) CreateNewContext(c *context.Context, info neo.PersonInfo) *neo.Context { 19 | id := xid.New() 20 | return &neo.Context{ 21 | Context: c, 22 | SessionID: id.String(), 23 | Person: info, 24 | Variables: map[string]interface{}{}, 25 | } 26 | } 27 | 28 | func (useless *Cognitive) GetProtoResponse(c *neo.Context, in *neo.Input) (*neo.Output, error) { 29 | if c == nil { 30 | return nil, neo.ErrContextNotExist 31 | } 32 | return useless.NewOutputText(c), nil 33 | } 34 | 35 | func (useless *Cognitive) OnContextIsDone(callback func(c *neo.Context)) { 36 | if useless.doneContextCallbacks == nil { 37 | useless.doneContextCallbacks = []*func(c *neo.Context){} 38 | } 39 | useless.doneContextCallbacks = append(useless.doneContextCallbacks, &callback) 40 | } 41 | -------------------------------------------------------------------------------- /cognitive/uselessbox/input.go: -------------------------------------------------------------------------------- 1 | package uselessbox 2 | 3 | import neo "github.com/minskylab/neocortex" 4 | 5 | func (useless *Cognitive) NewInput(data neo.InputData, i []neo.Intent, e []neo.Entity) *neo.Input { 6 | return &neo.Input{ 7 | Data: data, 8 | Intents: i, 9 | Entities: e, 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /cognitive/uselessbox/input_text.go: -------------------------------------------------------------------------------- 1 | package uselessbox 2 | 3 | import neo "github.com/minskylab/neocortex" 4 | 5 | func (useless *Cognitive) NewInputText(text string, i []neo.Intent, e []neo.Entity) *neo.Input { 6 | t := neo.InputData{ 7 | Type: neo.InputText, 8 | Value: text, 9 | Data: []byte(text), 10 | } 11 | return useless.NewInput(t, i, e) 12 | } 13 | -------------------------------------------------------------------------------- /cognitive/uselessbox/output.go: -------------------------------------------------------------------------------- 1 | package uselessbox 2 | 3 | import neo "github.com/minskylab/neocortex" 4 | 5 | func (useless *Cognitive) NewOutput(c *neo.Context, res []neo.Response, i []neo.Intent, e []neo.Entity) *neo.Output { 6 | return &neo.Output{ 7 | Entities: e, 8 | Intents: i, 9 | Responses: res, 10 | VisitedNodes: nil, // own of Uselessbox 11 | Logs: nil, // own of this Uselessbox 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /cognitive/uselessbox/output_text.go: -------------------------------------------------------------------------------- 1 | package uselessbox 2 | 3 | import neo "github.com/minskylab/neocortex" 4 | 5 | func (useless *Cognitive) NewOutputText(c *neo.Context) *neo.Output { 6 | res := []neo.Response{{ 7 | Type: neo.Text, 8 | Value: "I'm useless, you don't wait more from me", 9 | IsTyping: false, 10 | }} 11 | return useless.NewOutput(c, res, nil, nil) 12 | } 13 | -------------------------------------------------------------------------------- /cognitive/watson/cognitive.go: -------------------------------------------------------------------------------- 1 | package watson 2 | 3 | import ( 4 | "context" 5 | 6 | neo "github.com/minskylab/neocortex" 7 | "github.com/watson-developer-cloud/go-sdk/assistantv2" 8 | ) 9 | 10 | type Cognitive struct { 11 | service *assistantv2.AssistantV2 12 | assistantID string 13 | doneContextCallbacks []*func(c *neo.Context) 14 | } 15 | 16 | type NewCognitiveParams struct { 17 | Url string 18 | Username string 19 | Password string 20 | Version string 21 | AssistantID string 22 | ApiKey string 23 | } 24 | 25 | func NewCognitive(params NewCognitiveParams) (*Cognitive, error) { 26 | assistant, err := assistantv2.NewAssistantV2(&assistantv2.AssistantV2Options{ 27 | Version: params.Version, 28 | Username: params.Username, 29 | Password: params.Password, 30 | URL: params.Url, 31 | IAMApiKey: params.ApiKey, 32 | }) 33 | if err != nil { 34 | return nil, err 35 | } 36 | return &Cognitive{ 37 | service: assistant, 38 | assistantID: params.AssistantID, 39 | // turnsMap: map[string]int{}, 40 | }, nil 41 | } 42 | 43 | func (watson *Cognitive) CreateNewContext(c *context.Context, info neo.PersonInfo) *neo.Context { 44 | r, responseErr := watson.service.CreateSession(watson.service.NewCreateSessionOptions(watson.assistantID)) 45 | if responseErr != nil { 46 | panic(responseErr) 47 | } 48 | sess := watson.service.GetCreateSessionResult(r) 49 | 50 | // watson.turnsMap[*sess.SessionID] = 1 51 | return &neo.Context{ 52 | SessionID: *sess.SessionID, 53 | Person: info, 54 | Context: c, 55 | Variables: map[string]interface{}{}, 56 | } 57 | } 58 | 59 | func (watson *Cognitive) GetProtoResponse(c *neo.Context, in *neo.Input) (*neo.Output, error) { 60 | 61 | var opts *assistantv2.MessageOptions 62 | switch in.Data.Type { 63 | 64 | // Watson only supports one type of input: InputText 65 | case neo.InputText: 66 | _, opts = watson.NewInputText(in.Data.Value, c, in.Intents, in.Entities) 67 | default: 68 | return nil, neo.ErrInvalidInputType 69 | } 70 | 71 | r, err := watson.service.Message(opts) 72 | if err != nil { 73 | for _, call := range watson.doneContextCallbacks { 74 | (*call)(c) 75 | } 76 | return nil, neo.ErrSessionNotExist 77 | } 78 | 79 | if r.StatusCode != 200 { 80 | for _, call := range watson.doneContextCallbacks { 81 | (*call)(c) 82 | } 83 | return nil, neo.ErrInvalidResponseFromCognitiveService 84 | } 85 | 86 | response := watson.service.GetMessageResult(r) 87 | 88 | out := watson.NewOutput(c, response) 89 | 90 | return out, nil 91 | 92 | } 93 | 94 | func (watson *Cognitive) OnContextIsDone(callback func(c *neo.Context)) { 95 | if watson.doneContextCallbacks == nil { 96 | watson.doneContextCallbacks = []*func(c *neo.Context){} 97 | } 98 | watson.doneContextCallbacks = append(watson.doneContextCallbacks, &callback) 99 | } 100 | -------------------------------------------------------------------------------- /cognitive/watson/input.go: -------------------------------------------------------------------------------- 1 | package watson 2 | 3 | import ( 4 | neo "github.com/minskylab/neocortex" 5 | "github.com/watson-developer-cloud/go-sdk/assistantv2" 6 | ) 7 | 8 | func (watson *Cognitive) NewInput(opts *assistantv2.MessageOptions, data neo.InputData) *neo.Input { 9 | entities := make([]neo.Entity, 0) 10 | for _, e := range opts.Input.Entities { 11 | entities = append(entities, getNeocortexEntity(e)) 12 | } 13 | 14 | intents := make([]neo.Intent, 0) 15 | for _, i := range opts.Input.Intents { 16 | intents = append(intents, getNeocortexIntent(i)) 17 | } 18 | 19 | return &neo.Input{ 20 | Data: data, 21 | Intents: intents, 22 | Entities: entities, 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /cognitive/watson/input_text.go: -------------------------------------------------------------------------------- 1 | package watson 2 | 3 | import ( 4 | "github.com/IBM/go-sdk-core/core" 5 | neo "github.com/minskylab/neocortex" 6 | "github.com/watson-developer-cloud/go-sdk/assistantv2" 7 | ) 8 | 9 | func (watson *Cognitive) NewInputText(text string, c *neo.Context, intents []neo.Intent, entities []neo.Entity) (*neo.Input, *assistantv2.MessageOptions) { 10 | ets := make([]assistantv2.RuntimeEntity, 0) 11 | for _, e := range entities { 12 | ets = append(ets, getNativeEntity(&e)) 13 | } 14 | 15 | its := make([]assistantv2.RuntimeIntent, 0) 16 | for _, i := range intents { 17 | its = append(its, getNativeIntent(&i)) 18 | } 19 | 20 | options := &assistantv2.MessageOptions{ 21 | AssistantID: core.StringPtr(watson.assistantID), 22 | SessionID: core.StringPtr(c.SessionID), 23 | Context: &assistantv2.MessageContext{ 24 | Global: &assistantv2.MessageContextGlobal{ 25 | System: &assistantv2.MessageContextGlobalSystem{ 26 | UserID: core.StringPtr(c.Person.ID), 27 | Timezone: core.StringPtr(c.Person.Timezone), 28 | // TurnCount: core.Int64Ptr(int64(watson.turnsMap[c.SessionID])), 29 | }, 30 | }, 31 | Skills: &assistantv2.MessageContextSkills{ 32 | "main skill": map[string]interface{}{ 33 | "user_defined": c.Variables, 34 | }, 35 | }, 36 | }, 37 | Input: &assistantv2.MessageInput{ 38 | MessageType: core.StringPtr("text"), 39 | Text: &text, 40 | Intents: its, 41 | Entities: ets, 42 | Options: &assistantv2.MessageInputOptions{ 43 | 44 | Debug: core.BoolPtr(true), 45 | ReturnContext: core.BoolPtr(true), 46 | }, 47 | }, 48 | } 49 | 50 | data := neo.InputData{ 51 | Type: neo.InputText, 52 | Value: text, 53 | Data: []byte(text), 54 | } 55 | 56 | return watson.NewInput(options, data), options 57 | } 58 | -------------------------------------------------------------------------------- /cognitive/watson/normalizer.go: -------------------------------------------------------------------------------- 1 | package watson 2 | 3 | import ( 4 | neo "github.com/minskylab/neocortex" 5 | "github.com/watson-developer-cloud/go-sdk/assistantv2" 6 | ) 7 | 8 | func getNativeEntity(e *neo.Entity) assistantv2.RuntimeEntity { 9 | return assistantv2.RuntimeEntity{ 10 | Confidence: &e.Confidence, 11 | Entity: &e.Entity, 12 | Location: e.Location, 13 | Value: &e.Value, 14 | Metadata: e.Metadata, 15 | } 16 | } 17 | 18 | func getNativeIntent(i *neo.Intent) assistantv2.RuntimeIntent { 19 | return assistantv2.RuntimeIntent{ 20 | Intent: &i.Intent, 21 | Confidence: &i.Confidence, 22 | } 23 | } 24 | 25 | func getNeocortexIntent(i assistantv2.RuntimeIntent) neo.Intent { 26 | return neo.Intent{ 27 | Intent: *i.Intent, 28 | Confidence: *i.Confidence, 29 | } 30 | } 31 | func getNeocortexEntity(i assistantv2.RuntimeEntity) neo.Entity { 32 | // metadata, ok := i.Metadata.(map[string]interface{}) 33 | // if !ok { 34 | // metadata = map[string]interface{}{} 35 | // } 36 | return neo.Entity{ 37 | Entity: *i.Entity, 38 | // Metadata: metadata, 39 | Confidence: *i.Confidence, 40 | Location: i.Location, 41 | Value: *i.Value, 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /cognitive/watson/output.go: -------------------------------------------------------------------------------- 1 | package watson 2 | 3 | import ( 4 | neo "github.com/minskylab/neocortex" 5 | "github.com/watson-developer-cloud/go-sdk/assistantv2" 6 | ) 7 | 8 | func (watson *Cognitive) NewOutput(c *neo.Context, r *assistantv2.MessageResponse) *neo.Output { 9 | entities := make([]neo.Entity, 0) 10 | for _, e := range r.Output.Entities { 11 | entities = append(entities, getNeocortexEntity(e)) 12 | } 13 | 14 | intents := make([]neo.Intent, 0) 15 | for _, i := range r.Output.Intents { 16 | intents = append(intents, getNeocortexIntent(i)) 17 | } 18 | 19 | logs := make([]*neo.LogMessage, 0) 20 | nodes := make([]*neo.DialogNode, 0) 21 | 22 | if r.Output.Debug != nil { 23 | for _, l := range r.Output.Debug.LogMessages { 24 | logs = append(logs, 25 | &neo.LogMessage{ 26 | Level: neo.LogLevelType(*l.Message), 27 | Message: *l.Level, 28 | }) 29 | } 30 | 31 | for _, n := range r.Output.Debug.NodesVisited { 32 | title := "" 33 | conditions := "" 34 | name := "" 35 | 36 | if n.Title != nil { 37 | title = *n.Title 38 | } 39 | if n.Conditions != nil { 40 | conditions = *n.Conditions 41 | } 42 | 43 | if n.DialogNode != nil { 44 | name = *n.DialogNode 45 | } 46 | 47 | nodes = append(nodes, &neo.DialogNode{ 48 | Title: title, 49 | Conditions: conditions, 50 | Name: name, 51 | }) 52 | } 53 | } 54 | 55 | 56 | if c.Variables == nil { 57 | c.Variables = map[string]interface{}{} 58 | } 59 | 60 | 61 | responses := make([]neo.Response, 0) 62 | for _, gen := range r.Output.Generic { 63 | switch *gen.ResponseType { 64 | case "text": 65 | rText := watson.newTextResponse(gen) 66 | responses = append(responses, rText) 67 | case "option": 68 | rOption := watson.newOptionResponse(gen) 69 | responses = append(responses, rOption) 70 | case "image": 71 | rImage := watson.newImageResponse(gen) 72 | responses = append(responses, rImage) 73 | default: 74 | rUnknown := watson.newUnknownResponse(gen) 75 | responses = append(responses, rUnknown) 76 | } 77 | } 78 | 79 | if r.Context != nil { 80 | if r.Context.Skills != nil { 81 | if main, exist := (*r.Context.Skills)["main skill"]; exist { 82 | if mmain, ok := main.(map[string]interface{}); ok { 83 | if vars, isOk := mmain["user_defined"].(map[string]interface{}); isOk { 84 | for k, v := range vars { 85 | c.Variables[k] = v 86 | } 87 | 88 | } 89 | } 90 | } 91 | } 92 | 93 | } 94 | 95 | return &neo.Output{ 96 | Logs: logs, 97 | VisitedNodes: nodes, 98 | Intents: intents, 99 | Entities: entities, 100 | Responses: responses, 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /cognitive/watson/output_image.go: -------------------------------------------------------------------------------- 1 | package watson 2 | 3 | import ( 4 | neo "github.com/minskylab/neocortex" 5 | "github.com/watson-developer-cloud/go-sdk/assistantv2" 6 | ) 7 | 8 | func (watson *Cognitive) newImageResponse(gen assistantv2.DialogRuntimeResponseGeneric) neo.Response { 9 | typing := false 10 | if gen.Typing != nil { 11 | typing = *gen.Typing 12 | } 13 | src := "" 14 | if gen.Source != nil { 15 | src = *gen.Source 16 | } 17 | return neo.Response{ 18 | Type: neo.Image, 19 | Value: src, 20 | IsTyping: typing, 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /cognitive/watson/output_option.go: -------------------------------------------------------------------------------- 1 | package watson 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/minskylab/neocortex" 7 | "github.com/watson-developer-cloud/go-sdk/assistantv2" 8 | ) 9 | 10 | func (watson *Cognitive) newOptionResponse(gen assistantv2.DialogRuntimeResponseGeneric) neocortex.Response { 11 | typing := false 12 | if gen.Typing != nil { 13 | typing = *gen.Typing 14 | } 15 | 16 | options := make([]*neocortex.Option, 0) 17 | 18 | for _, o := range gen.Options { 19 | postBack := true 20 | if strings.HasPrefix(*o.Value.Input.Text, "http") { 21 | postBack = false 22 | } 23 | 24 | options = append(options, &neocortex.Option{ 25 | Text: *o.Label, 26 | Action: *o.Value.Input.Text, 27 | IsPostBack: postBack, 28 | }) 29 | } 30 | 31 | title := "" 32 | description := "" 33 | 34 | if gen.Title != nil { 35 | title = *gen.Title 36 | } 37 | 38 | if gen.Description != nil { 39 | description = *gen.Description 40 | } 41 | 42 | return neocortex.Response{ 43 | Type: neocortex.Options, 44 | Value: neocortex.OptionsResponse{ 45 | Title: title, 46 | Description: description, 47 | Options: options, 48 | }, 49 | IsTyping: typing, 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /cognitive/watson/output_pause.go: -------------------------------------------------------------------------------- 1 | package watson 2 | -------------------------------------------------------------------------------- /cognitive/watson/output_text.go: -------------------------------------------------------------------------------- 1 | package watson 2 | 3 | import ( 4 | neo "github.com/minskylab/neocortex" 5 | "github.com/watson-developer-cloud/go-sdk/assistantv2" 6 | ) 7 | 8 | func (watson *Cognitive) newTextResponse(gen assistantv2.DialogRuntimeResponseGeneric) neo.Response { 9 | typing := false 10 | if gen.Typing != nil { 11 | typing = *gen.Typing 12 | } 13 | return neo.Response{ 14 | Type: neo.Text, 15 | Value: *gen.Text, 16 | IsTyping: typing, 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /cognitive/watson/output_unknown.go: -------------------------------------------------------------------------------- 1 | package watson 2 | 3 | import ( 4 | neo "github.com/minskylab/neocortex" 5 | "github.com/watson-developer-cloud/go-sdk/assistantv2" 6 | ) 7 | 8 | func (watson *Cognitive) newUnknownResponse(gen assistantv2.DialogRuntimeResponseGeneric) neo.Response { 9 | return neo.Response{ 10 | Type: neo.Unknown, 11 | Value: "unknown or not implemented type: " + *gen.ResponseType, 12 | IsTyping: false, 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /context.go: -------------------------------------------------------------------------------- 1 | package neocortex 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | ) 7 | 8 | // PersonInfo describes the basic info of the person 9 | type PersonInfo struct { 10 | ID string `json:"id"` 11 | Timezone string `json:"timezone"` 12 | Picture string `json:"picture"` 13 | Locale string `json:"locale"` 14 | Name string `json:"name"` 15 | } 16 | 17 | // Context represent the context of a conversation 18 | type Context struct { 19 | Context *context.Context `json:"-" bson:"-"` 20 | SessionID string `json:"session_id" bson:"session_id"` 21 | Person PersonInfo `json:"person" bson:"person"` 22 | Variables map[string]interface{} `json:"variables" bson:"variables"` 23 | } 24 | 25 | func (c *Context) String() string { 26 | s := "\n===== CONTEXT =====\n" 27 | s = s + fmt.Sprintf("session: %s\n", c.SessionID) 28 | s = s + fmt.Sprintf("context: %v\n", c.Context) 29 | s = s + fmt.Sprintf("user name: %s\n", c.Person.Name) 30 | s = s + fmt.Sprintf("total context variables: %d\n", len(c.Variables)) 31 | s = s + "======================\n" 32 | return s 33 | } 34 | -------------------------------------------------------------------------------- /context_utils.go: -------------------------------------------------------------------------------- 1 | package neocortex 2 | 3 | func (c *Context) SetContextVariable(name string, value interface{}) { 4 | if c.Variables == nil { 5 | c.Variables = map[string]interface{}{} 6 | } 7 | 8 | c.Variables[name] = value 9 | } 10 | 11 | func (c *Context) DeleteContextVariable(name string, value interface{}) { 12 | if c.Variables == nil { 13 | c.Variables = map[string]interface{}{} 14 | 15 | } 16 | 17 | delete(c.Variables, name) 18 | 19 | } 20 | 21 | func (c *Context) GetStringContextVariable(name string, _default string) string { 22 | if c.Variables == nil { 23 | c.Variables = map[string]interface{}{} 24 | return _default 25 | } 26 | 27 | value, isOk := c.Variables[name].(string) 28 | if !isOk { 29 | return _default 30 | } 31 | 32 | return value 33 | } 34 | 35 | func (c *Context) GetIntContextVariable(name string, _default int) int { 36 | if c.Variables == nil { 37 | c.Variables = map[string]interface{}{} 38 | return _default 39 | } 40 | 41 | value, isOk := c.Variables[name].(int) 42 | if !isOk { 43 | return _default 44 | } 45 | 46 | return value 47 | } 48 | 49 | func (c *Context) GetFloat64ContextVariable(name string, _default float64) float64 { 50 | if c.Variables == nil { 51 | c.Variables = map[string]interface{}{} 52 | return _default 53 | } 54 | 55 | value, isOk := c.Variables[name].(float64) 56 | if !isOk { 57 | return _default 58 | } 59 | 60 | return value 61 | } 62 | -------------------------------------------------------------------------------- /cortex.go: -------------------------------------------------------------------------------- 1 | package neocortex 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "log" 7 | "time" 8 | ) 9 | 10 | type OutputResponse func(c *Context, output *Output) error 11 | type HandleResolver func(c *Context, in *Input, out *Output, response OutputResponse) error 12 | type MiddleHandler func(c *Context, message *Input, response OutputResponse) error 13 | type ContextFabric func(ctx context.Context, info PersonInfo) *Context 14 | 15 | type Engine struct { 16 | done chan error 17 | cognitive CognitiveService 18 | channels []CommunicationChannel 19 | registeredResolvers map[CommunicationChannel]map[*Matcher]*HandleResolver 20 | generalResolver map[CommunicationChannel]*HandleResolver 21 | registeredInjection map[CommunicationChannel]map[*Matcher]*InInjection 22 | generalInjection map[CommunicationChannel]*InInjection 23 | 24 | Repository Repository 25 | ActiveDialogs map[*Context]*Dialog 26 | api *API 27 | Register map[string]string 28 | 29 | Analytics *Analytics 30 | dialogPerformanceFunc func(*Dialog) float64 31 | 32 | secret string 33 | } 34 | 35 | func (engine *Engine) onNewContextCreated(c *Context) { 36 | log.Println("creating new context: ", c.SessionID) 37 | engine.ActiveDialogs[c] = newDialog() 38 | } 39 | 40 | func (engine *Engine) onContextIsDone(c *Context) { 41 | for _, ch := range engine.channels { 42 | ch.CallContextDone(c) 43 | } 44 | log.Println("closing context: ", c.SessionID) 45 | if dialog, ok := engine.ActiveDialogs[c]; ok { 46 | dialog.EndAt = time.Now() 47 | if engine.Repository != nil { 48 | dialog.Performance = engine.dialogPerformanceFunc(dialog) 49 | err := engine.Repository.SaveDialog(dialog) 50 | if err != nil { 51 | engine.done <- err 52 | } 53 | } 54 | 55 | delete(engine.ActiveDialogs, c) 56 | log.Println("finally deleting: ", c.SessionID) 57 | } 58 | } 59 | 60 | // RegisterAdmin you can register new admin for get info purpose 61 | func (engine *Engine) RegisterAdmin(Username, Password string) error { 62 | if engine.Register != nil { 63 | engine.Register[Username] = Password 64 | } 65 | return nil 66 | } 67 | 68 | // getAdmin you can get the password of the admin 69 | func (engine *Engine) getAdmin(Username string) (string, error) { 70 | if val, ok := engine.Register[Username]; ok { 71 | return val, nil 72 | } 73 | return "", nil 74 | } 75 | 76 | // RemoveAdmin let you remove the admin 77 | func (engine *Engine) RemoveAdmin(Username string) error { 78 | if _, ok := engine.Register[Username]; ok { 79 | delete(engine.Register, Username) 80 | } 81 | 82 | return errors.New("no user found") 83 | } 84 | -------------------------------------------------------------------------------- /cortex_configuration.go: -------------------------------------------------------------------------------- 1 | package neocortex 2 | 3 | type Configuration struct { 4 | } 5 | -------------------------------------------------------------------------------- /cortex_creator.go: -------------------------------------------------------------------------------- 1 | package neocortex 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "time" 7 | 8 | "os" 9 | "os/signal" 10 | 11 | ) 12 | 13 | func newDefaultEngine(cognitive CognitiveService, channels ...CommunicationChannel) *Engine { 14 | engine := &Engine{} 15 | engine.channels = channels 16 | engine.cognitive = cognitive 17 | engine.registeredResolvers = map[CommunicationChannel]map[*Matcher]*HandleResolver{} 18 | engine.generalResolver = map[CommunicationChannel]*HandleResolver{} 19 | engine.registeredInjection = map[CommunicationChannel]map[*Matcher]*InInjection{} 20 | engine.generalInjection = map[CommunicationChannel]*InInjection{} 21 | engine.done = make(chan error, 1) 22 | engine.Register = map[string]string{} 23 | // engine.logger = logrus.StandardLogger() // In the future 24 | engine.ActiveDialogs = map[*Context]*Dialog{} 25 | engine.dialogPerformanceFunc = defaultPerformance 26 | engine.secret = "neocortex2019" 27 | 28 | return engine 29 | } 30 | 31 | // Default ... 32 | func Default(repository Repository, cognitive CognitiveService, channels ...CommunicationChannel) (*Engine, error) { 33 | var err error 34 | engine := newDefaultEngine(cognitive, channels...) 35 | engine.Repository = repository 36 | engine.Analytics, err = newDefaultAnalytics(engine.Repository, defaultPerformance) 37 | if err != nil { 38 | return nil, err 39 | } 40 | engine.api = newCortexAPI(repository, engine.Analytics, "/api", ":4200") 41 | 42 | fabric := func(ctx context.Context, info PersonInfo) *Context { 43 | newContext := cognitive.CreateNewContext(&ctx, info) 44 | return newContext 45 | } 46 | 47 | cognitive.OnContextIsDone(func(c *Context) { 48 | engine.onContextIsDone(c) 49 | }) 50 | 51 | for _, ch := range channels { 52 | engine.registeredResolvers[ch] = map[*Matcher]*HandleResolver{} 53 | ch.SetContextFabric(fabric) 54 | err := ch.RegisterMessageEndpoint(func(c *Context, message *Input, response OutputResponse) error { 55 | return engine.onMessage(ch, c, message, response) 56 | }) 57 | 58 | if err != nil { 59 | return nil, err 60 | } 61 | 62 | ch.OnNewContextCreated(func(c *Context) { 63 | engine.onNewContextCreated(c) 64 | }) 65 | 66 | ch.OnContextIsDone(func(c *Context) { 67 | engine.onContextIsDone(c) 68 | }) 69 | 70 | go func(ch *CommunicationChannel) { 71 | err := (*ch).ToHear() 72 | if err != nil { 73 | engine.done <- err 74 | } 75 | }(&ch) 76 | } 77 | 78 | return engine, nil 79 | } 80 | 81 | func (engine *Engine) Run() error { 82 | signalChan := make(chan os.Signal, 1) 83 | signal.Notify(signalChan, os.Interrupt) 84 | 85 | gc := defaultGarbageCollector(10 * time.Minute) 86 | 87 | go func() { 88 | <-signalChan 89 | log.Println("Closing all dialogs, total: ", len(engine.ActiveDialogs)) 90 | for c := range engine.ActiveDialogs { 91 | engine.onContextIsDone(c) 92 | } 93 | engine.done <- nil 94 | }() 95 | go func() { 96 | if engine.api.repository != nil { 97 | engine.done <- engine.api.Launch(engine) 98 | } 99 | }() 100 | 101 | engine.runGarbageCollector(gc) 102 | 103 | return <-engine.done 104 | } 105 | -------------------------------------------------------------------------------- /cortex_metrics.go: -------------------------------------------------------------------------------- 1 | package neocortex 2 | 3 | // SetPerformanceMetric links a custom performance metric function 4 | func (engine *Engine) SetPerformanceMetric(perf func(*Dialog) float64) { 5 | engine.dialogPerformanceFunc = perf 6 | } 7 | -------------------------------------------------------------------------------- /cortex_onmessage.go: -------------------------------------------------------------------------------- 1 | package neocortex 2 | 3 | import ( 4 | "log" 5 | "strings" 6 | "time" 7 | ) 8 | 9 | func (engine *Engine) onMessage(channel CommunicationChannel, c *Context, in *Input, response OutputResponse) error { 10 | 11 | inMatched := false 12 | for matcher, injector := range engine.registeredInjection[channel] { 13 | if in.Match(nil, matcher) { 14 | in = (*injector)(c, in) 15 | inMatched = true 16 | } 17 | } 18 | 19 | if engine.generalInjection[channel] != nil && !inMatched { 20 | f := *engine.generalInjection[channel] 21 | in = f(c, in) 22 | } 23 | 24 | if dialog, activeDialogExist := engine.ActiveDialogs[c]; activeDialogExist { 25 | dialog.LastActivity = time.Now() 26 | dialog.Contexts = append(dialog.Contexts, &ContextRecord{At: time.Now(), Context: *c}) 27 | dialog.Ins = append(dialog.Ins, &InputRecord{At: time.Now(), Input: *in}) 28 | } 29 | 30 | if in.Data.Type == InputText { 31 | in.Data.Value = strings.ReplaceAll(in.Data.Value, "\n", " ") 32 | } 33 | 34 | out, err := engine.cognitive.GetProtoResponse(c, in) 35 | if err != nil { 36 | if err == ErrSessionNotExist { 37 | // Creating new context 38 | log.Println("calling to engine.cognitive.CreateNewContext") 39 | c1 := engine.cognitive.CreateNewContext(c.Context, c.Person) 40 | c = c1 41 | if engine.generalInjection[channel] != nil && !inMatched { 42 | f := *engine.generalInjection[channel] 43 | in = f(c, in) 44 | } 45 | 46 | engine.ActiveDialogs[c] = newDialog() 47 | 48 | out, err = engine.cognitive.GetProtoResponse(c, in) 49 | if err != nil { 50 | return err 51 | } 52 | 53 | } else { 54 | return err 55 | } 56 | } 57 | 58 | go func(intents []Intent, entities []Entity, nodes []*DialogNode, vars map[string]interface{}) { 59 | var err error 60 | if engine.Repository != nil { 61 | for _, i := range intents { 62 | if err = engine.Repository.RegisterIntent(i.Intent); err != nil { 63 | log.Println(err) 64 | } 65 | } 66 | for _, e := range entities { 67 | if err = engine.Repository.RegisterEntity(e.Entity); err != nil { 68 | log.Println(err) 69 | } 70 | } 71 | for _, n := range nodes { 72 | if err = engine.Repository.RegisterDialogNode(n.Title); err != nil { 73 | log.Println(err) 74 | } 75 | } 76 | for v := range vars { 77 | if err = engine.Repository.RegisterContextVar(v); err != nil { 78 | log.Println(err) 79 | } 80 | } 81 | } 82 | }(out.Intents, out.Entities, out.VisitedNodes, c.Variables) 83 | 84 | resolvers, channelIsRegistered := engine.registeredResolvers[channel] 85 | if !channelIsRegistered { 86 | return ErrChannelIsNotRegistered 87 | } 88 | 89 | exist := false 90 | for m, resolver := range resolvers { 91 | if out.Match(c, m) { 92 | if err = (*resolver)(c, in, out, response); err != nil { 93 | return err 94 | } 95 | 96 | if dialog, activeDialogExist := engine.ActiveDialogs[c]; activeDialogExist { 97 | dialog.LastActivity = time.Now() 98 | dialog.Contexts = append(dialog.Contexts, &ContextRecord{At: time.Now(), Context: *c}) 99 | dialog.Outs = append(dialog.Outs, &OutputRecord{At: time.Now(), Output: *out}) 100 | } 101 | 102 | exist = true 103 | } 104 | } 105 | 106 | if engine.generalResolver[channel] != nil && !exist { 107 | if err = (*engine.generalResolver[channel])(c, in, out, response); err != nil { 108 | return err 109 | } 110 | 111 | if dialog, activeDialogExist := engine.ActiveDialogs[c]; activeDialogExist { 112 | dialog.LastActivity = time.Now() 113 | dialog.Contexts = append(dialog.Contexts, &ContextRecord{At: time.Now(), Context: *c}) 114 | dialog.Outs = append(dialog.Outs, &OutputRecord{At: time.Now(), Output: *out}) 115 | } 116 | } 117 | 118 | return nil 119 | } 120 | -------------------------------------------------------------------------------- /defaults.go: -------------------------------------------------------------------------------- 1 | package neocortex 2 | 3 | var defaultPerformance = func(dialog *Dialog) float64 { 4 | goods := 0 5 | for _, out := range dialog.Outs { 6 | valids := 0 7 | for _, intent := range out.Output.Intents { 8 | if intent.Confidence > 0.1 { 9 | valids++ 10 | } 11 | } 12 | if valids > 0 { 13 | goods++ 14 | } 15 | } 16 | 17 | if totalOuts := float64(len(dialog.Outs)); totalOuts > 0.0 { 18 | return float64(goods) / totalOuts 19 | } 20 | 21 | return 0.0 22 | } 23 | -------------------------------------------------------------------------------- /dialog.go: -------------------------------------------------------------------------------- 1 | package neocortex 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/rs/xid" 8 | ) 9 | 10 | type InputRecord struct { 11 | At time.Time `json:"at"` 12 | Input Input `json:"input"` 13 | } 14 | 15 | type OutputRecord struct { 16 | At time.Time `json:"at"` 17 | Output Output `json:"output"` 18 | } 19 | 20 | type ContextRecord struct { 21 | At time.Time `json:"at"` 22 | Context Context `json:"context"` 23 | } 24 | 25 | type Dialog struct { 26 | ID string `json:"id" bson:"id"` 27 | StartAt time.Time `json:"start_at" bson:"start_at"` 28 | LastActivity time.Time `json:"last_activity" bson:"last_activity"` 29 | EndAt time.Time `json:"end_at" bson:"end_at"` 30 | Ins []*InputRecord `json:"ins" bson:"ins"` 31 | Outs []*OutputRecord `json:"outs" bson:"outs"` 32 | Contexts []*ContextRecord `json:"contexts" bson:"contexts"` 33 | Performance float64 `json:"performance" bson:"performance"` 34 | } 35 | 36 | func newDialog() *Dialog { 37 | return &Dialog{ 38 | ID: xid.New().String(), 39 | LastActivity: time.Now(), 40 | StartAt: time.Now(), 41 | EndAt: time.Time{}, 42 | Ins: []*InputRecord{}, 43 | Outs: []*OutputRecord{}, 44 | Contexts: []*ContextRecord{}, 45 | Performance: 0.0, 46 | } 47 | } 48 | 49 | // TODO: To optimize, please find all at the same time (that has better performance) 50 | func (dialog *Dialog) HasEntity(entity string) bool { 51 | totalIns := len(dialog.Ins) 52 | totalOuts := len(dialog.Outs) 53 | 54 | diff := totalIns - totalOuts 55 | fmt.Println(diff) 56 | if diff == 0 { 57 | for i := 0; i < totalIns; i++ { 58 | for _, ent := range dialog.Ins[i].Input.Entities { 59 | if ent.Value == entity { 60 | return true 61 | } 62 | } 63 | for _, ent := range dialog.Outs[i].Output.Entities { 64 | if ent.Value == entity { 65 | return true 66 | } 67 | } 68 | } 69 | return false 70 | } 71 | 72 | for _, in := range dialog.Ins { 73 | for _, ent := range in.Input.Entities { 74 | if ent.Value == entity { 75 | return true 76 | } 77 | } 78 | } 79 | 80 | for _, out := range dialog.Outs { 81 | for _, ent := range out.Output.Entities { 82 | if ent.Value == entity { 83 | return true 84 | } 85 | } 86 | } 87 | 88 | return false 89 | } 90 | 91 | func (dialog *Dialog) HasIntent(intent string) bool { 92 | totalIns := len(dialog.Ins) 93 | totalOuts := len(dialog.Outs) 94 | 95 | diff := totalIns - totalOuts 96 | 97 | if diff == 0 { 98 | for i := 0; i < totalIns; i++ { 99 | for _, intn := range dialog.Ins[i].Input.Intents { 100 | if intn.Intent == intent { 101 | return true 102 | } 103 | } 104 | for _, intn := range dialog.Outs[i].Output.Intents { 105 | if intn.Intent == intent { 106 | return true 107 | } 108 | } 109 | } 110 | return false 111 | } 112 | 113 | for _, in := range dialog.Ins { 114 | for _, intn := range in.Input.Intents { 115 | if intn.Intent == intent { 116 | return true 117 | } 118 | } 119 | } 120 | 121 | for _, out := range dialog.Outs { 122 | for _, intn := range out.Output.Intents { 123 | if intn.Intent == intent { 124 | return true 125 | } 126 | } 127 | } 128 | 129 | return false 130 | } 131 | 132 | func (dialog *Dialog) HasDialogNode(node string) bool { 133 | for _, out := range dialog.Outs { 134 | for _, visited := range out.Output.VisitedNodes { 135 | if visited.Name == node || visited.Title == node { 136 | return true 137 | } 138 | } 139 | } 140 | 141 | return false 142 | } 143 | 144 | func (dialog *Dialog) HasContextVar(contextVar string) bool { 145 | for _, c := range dialog.Contexts { 146 | if _, ok := c.Context.Variables[contextVar]; ok { 147 | return true 148 | } 149 | } 150 | return false 151 | } 152 | -------------------------------------------------------------------------------- /entity.go: -------------------------------------------------------------------------------- 1 | package neocortex 2 | 3 | // Entity define any kind of object or entity 4 | type Entity struct { 5 | Entity string `json:"entity"` 6 | Location []int64 `json:"location"` 7 | Value string `json:"value"` 8 | Confidence float64 `json:"confidence"` 9 | Metadata map[string]interface{} `json:"metadata"` 10 | } 11 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package neocortex 2 | 3 | import "errors" 4 | 5 | var ErrSessionNotExist = errors.New("session not exist") 6 | 7 | // var ErrSessionExpired = errors.Default("session expired") 8 | var ErrInvalidResponseFromCognitiveService = errors.New("invalid response from cognitive service") 9 | var ErrInvalidInputType = errors.New("invalid or unimplemented input type") 10 | var ErrContextNotExist = errors.New("context is not valid or not exist") 11 | var ErrChannelIsNotRegistered = errors.New("channel not exist on this engine instance") 12 | -------------------------------------------------------------------------------- /examples/basic/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | neo "github.com/minskylab/neocortex" 5 | "github.com/minskylab/neocortex/channels/terminal" 6 | "github.com/minskylab/neocortex/cognitive/uselessbox" 7 | "github.com/minskylab/neocortex/repositories/mongodb" 8 | ) 9 | 10 | // Example of use useless box with terminal channel 11 | func main() { 12 | box := uselessbox.NewCognitive() 13 | term := terminal.NewChannel(nil) 14 | 15 | repo, err := mongodb.New("mongodb+srv://amanda:LZlt2PQrqJW5r5RN@amanda-520ju.mongodb.net/test?retryWrites=true&w=majority") 16 | if err != nil { 17 | panic(err) 18 | } 19 | 20 | engine, err := neo.Default(repo, box, term) 21 | if err != nil { 22 | panic(err) 23 | } 24 | 25 | engine.RegisterAdmin("admin", "admin") 26 | 27 | engine.InjectAll(term, func(c *neo.Context, in *neo.Input) *neo.Input { 28 | c.Variables["user_name"] = "Bregy" 29 | if c.Variables["count"] == nil { 30 | c.Variables["count"] = 0 31 | } 32 | 33 | c.Variables["count"] = c.Variables["count"].(int) + 1 34 | 35 | return in 36 | }) 37 | 38 | engine.ResolveAny(term, func(c *neo.Context, in *neo.Input, out *neo.Output, response neo.OutputResponse) error { 39 | out.AddTextResponse("-----Watermark-----") 40 | 41 | return response(c, out) 42 | }) 43 | 44 | if err = engine.Run(); err != nil { 45 | panic(err) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /examples/facebook/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | neo "github.com/minskylab/neocortex" 5 | "github.com/minskylab/neocortex/channels/facebook" 6 | 7 | "github.com/minskylab/neocortex/cognitive/uselessbox" 8 | ) 9 | 10 | func main() { 11 | box := uselessbox.NewCognitive() 12 | 13 | fb, err := facebook.NewChannel(facebook.ChannelOptions{ 14 | AccessToken: "", 15 | VerifyToken: "", 16 | PageID: "", 17 | }) 18 | 19 | // repo, err := boltdb.New("neocortex.db") 20 | // if err != nil { 21 | // panic(err) 22 | // } 23 | 24 | engine, err := neo.Default(nil, box, fb) 25 | 26 | if err != nil { 27 | panic(err) 28 | } 29 | 30 | engine.ResolveAny(fb, func(c *neo.Context, in *neo.Input, out *neo.Output, response neo.OutputResponse) error { 31 | return response(c, out) 32 | }) 33 | 34 | if err := engine.Run(); err != nil { 35 | panic(err) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /examples/facebook_watson/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | neo "github.com/minskylab/neocortex" 5 | "github.com/minskylab/neocortex/channels/facebook" 6 | "github.com/minskylab/neocortex/cognitive/watson" 7 | ) 8 | 9 | func main() { 10 | watsonAgent, err := watson.NewCognitive(watson.NewCognitiveParams{ 11 | Url: "", 12 | ApiKey: "", 13 | Version: "", 14 | AssistantID: "", 15 | }) 16 | if err != nil { 17 | panic(err) 18 | } 19 | 20 | fb, err := facebook.NewChannel(facebook.ChannelOptions{ 21 | AccessToken: "", 22 | VerifyToken: "", 23 | PageID: "", 24 | }) 25 | if err != nil { 26 | panic(err) 27 | } 28 | 29 | // repo, err := boltdb.New("neocortex.db") 30 | // if err != nil { 31 | // panic(err) 32 | // } 33 | 34 | engine, err := neo.Default(nil, watsonAgent, fb) 35 | 36 | if err != nil { 37 | panic(err) 38 | } 39 | 40 | match := neo.IntentIs("HELLO") 41 | engine.Resolve(fb, match, func(c *neo.Context, in *neo.Input, out *neo.Output, response neo.OutputResponse) error { 42 | out.Fill(map[string]string{ 43 | "Name": c.Person.Name, 44 | }) 45 | return response(c, out) 46 | }) 47 | 48 | engine.ResolveAny(fb, func(c *neo.Context, in *neo.Input, out *neo.Output, response neo.OutputResponse) error { 49 | out.AddTextResponse("[Unhandled]") 50 | return response(c, out) 51 | }) 52 | 53 | err = engine.Run() 54 | if err != nil { 55 | panic(err) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /examples/watson/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | neo "github.com/minskylab/neocortex" 5 | "github.com/minskylab/neocortex/channels/terminal" 6 | "github.com/minskylab/neocortex/cognitive/watson" 7 | ) 8 | 9 | func main() { 10 | watsonAgent, err := watson.NewCognitive(watson.NewCognitiveParams{ 11 | Url: "", 12 | Username: "", 14 | Version: "", 15 | AssistantID: "", 16 | }) 17 | if err != nil { 18 | panic(err) 19 | } 20 | 21 | term := terminal.NewChannel(nil) 22 | 23 | // repo, err := boltdb.New("neocortex.db") 24 | // if err != nil { 25 | // panic(err) 26 | // } 27 | 28 | engine, err := neo.Default(nil, watsonAgent, term) 29 | if err != nil { 30 | panic(err) 31 | } 32 | 33 | if err != nil { 34 | panic(err) 35 | } 36 | 37 | engine.ResolveAny(term, func(c *neo.Context, in *neo.Input, out *neo.Output, response neo.OutputResponse) error { 38 | return response(c, out) 39 | }) 40 | 41 | if err := engine.Run(); err != nil { 42 | panic(err) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /filler.go: -------------------------------------------------------------------------------- 1 | package neocortex 2 | 3 | import ( 4 | "bytes" 5 | "text/template" 6 | ) 7 | 8 | func (out *Output) fill(data interface{}) *Output { 9 | t := template.New("out") 10 | buffer := bytes.NewBufferString("") 11 | newOut := *out 12 | newOut.Responses = []Response{} 13 | for _, r := range out.Responses { 14 | switch r.Type { 15 | case Text: 16 | parsed, _ := t.Parse(r.Value.(string)) 17 | _ = parsed.Execute(buffer, data) 18 | newOut.Responses = append(newOut.Responses, Response{ 19 | Value: buffer.String(), 20 | Type: Text, 21 | IsTyping: false, 22 | }) 23 | } 24 | } 25 | *out = newOut 26 | return out 27 | } 28 | 29 | func (out *Output) Fill(data interface{}) *Output { 30 | return out.fill(data) 31 | } 32 | -------------------------------------------------------------------------------- /garbage_collector.go: -------------------------------------------------------------------------------- 1 | package neocortex 2 | 3 | import "time" 4 | 5 | type garbageCollector struct { 6 | tickTime time.Duration 7 | maxLastResponse time.Duration 8 | } 9 | 10 | func defaultGarbageCollector(maxSessiontime time.Duration) *garbageCollector { 11 | return &garbageCollector{ 12 | tickTime: 1 * time.Second, 13 | maxLastResponse: maxSessiontime, 14 | } 15 | } 16 | 17 | func (engine *Engine) runGarbageCollector(g *garbageCollector) { 18 | ticker := time.NewTicker(g.tickTime) 19 | go func() { 20 | for t := range ticker.C { 21 | for c, diag := range engine.ActiveDialogs { 22 | if t.Sub(diag.LastActivity) > g.maxLastResponse { 23 | engine.onContextIsDone(c) 24 | } 25 | } 26 | } 27 | }() 28 | } 29 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/minskylab/neocortex 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/DataDog/zstd v1.4.4 // indirect 7 | github.com/IBM/go-sdk-core v0.4.1 8 | github.com/Sereal/Sereal v0.0.0-20200210135736-180ff2394e8a // indirect 9 | github.com/appleboy/gin-jwt/v2 v2.6.3 10 | github.com/araddon/dateparse v0.0.0-20190622164848-0fb0a474d195 11 | github.com/asdine/storm v2.1.2+incompatible 12 | github.com/gin-contrib/cors v1.3.1 13 | github.com/gin-gonic/gin v1.5.0 14 | github.com/go-playground/universal-translator v0.17.0 // indirect 15 | github.com/go-stack/stack v1.8.0 // indirect 16 | github.com/golang/protobuf v1.3.4 // indirect 17 | github.com/golang/snappy v0.0.1 // indirect 18 | github.com/google/go-cmp v0.4.0 // indirect 19 | github.com/iancoleman/strcase v0.0.0-20190422225806-e506e3ef7365 20 | github.com/jinzhu/now v1.0.1 21 | github.com/joho/godotenv v1.3.0 // indirect 22 | github.com/json-iterator/go v1.1.9 // indirect 23 | github.com/leodido/go-urn v1.2.0 // indirect 24 | github.com/mattn/go-isatty v0.0.12 // indirect 25 | github.com/onsi/ginkgo v1.12.0 // indirect 26 | github.com/onsi/gomega v1.9.0 // indirect 27 | github.com/rs/xid v1.2.1 28 | github.com/vmihailenco/msgpack v3.3.3+incompatible // indirect 29 | github.com/watson-developer-cloud/go-sdk v0.10.0 30 | github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c // indirect 31 | github.com/xdg/stringprep v1.0.0 // indirect 32 | go.etcd.io/bbolt v1.3.3 // indirect 33 | go.mongodb.org/mongo-driver v1.1.1 34 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e // indirect 35 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 // indirect 36 | google.golang.org/appengine v1.6.5 // indirect 37 | gopkg.in/go-playground/validator.v9 v9.31.0 // indirect 38 | gopkg.in/yaml.v2 v2.2.8 // indirect 39 | ) 40 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/DataDog/zstd v1.4.4 h1:+IawcoXhCBylN7ccwdwf8LOH2jKq7NavGpEPanrlTzE= 2 | github.com/DataDog/zstd v1.4.4/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= 3 | github.com/IBM/go-sdk-core v0.4.1 h1:UWZ5jB7xR44AwDF73G5rCECFERCrb86ns5darN092es= 4 | github.com/IBM/go-sdk-core v0.4.1/go.mod h1:u7wqiIlwK3oRHbanAO4t1kwJUr1EfT2uFySklrUgMmE= 5 | github.com/Sereal/Sereal v0.0.0-20200210135736-180ff2394e8a h1:g/CIca/LIB0zXaOjEogAwY1/wAhux1FE8CdBqNbXkPQ= 6 | github.com/Sereal/Sereal v0.0.0-20200210135736-180ff2394e8a/go.mod h1:D0JMgToj/WdxCgd30Kc1UcA9E+WdZoJqeVOuYW7iTBM= 7 | github.com/appleboy/gin-jwt/v2 v2.6.3 h1:aK4E3DjihWEBUTjEeRnGkA5nUkmwJPL1CPonMa2usRs= 8 | github.com/appleboy/gin-jwt/v2 v2.6.3/go.mod h1:MfPYA4ogzvOcVkRwAxT7quHOtQmVKDpTwxyUrC2DNw0= 9 | github.com/appleboy/gofight/v2 v2.1.2 h1:VOy3jow4vIK8BRQJoC/I9muxyYlJ2yb9ht2hZoS3rf4= 10 | github.com/appleboy/gofight/v2 v2.1.2/go.mod h1:frW+U1QZEdDgixycTj4CygQ48yLTUhplt43+Wczp3rw= 11 | github.com/araddon/dateparse v0.0.0-20190622164848-0fb0a474d195 h1:c4mLfegoDw6OhSJXTd2jUEQgZUQuJWtocudb97Qn9EM= 12 | github.com/araddon/dateparse v0.0.0-20190622164848-0fb0a474d195/go.mod h1:SLqhdZcd+dF3TEVL2RMoob5bBP5R1P1qkox+HtCBgGI= 13 | github.com/asdine/storm v2.1.2+incompatible h1:dczuIkyqwY2LrtXPz8ixMrU/OFgZp71kbKTHGrXYt/Q= 14 | github.com/asdine/storm v2.1.2+incompatible/go.mod h1:RarYDc9hq1UPLImuiXK3BIWPJLdIygvV3PsInK0FbVQ= 15 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 16 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 17 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 18 | github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= 19 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 20 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 21 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 22 | github.com/gin-contrib/cors v1.3.1 h1:doAsuITavI4IOcd0Y19U4B+O0dNWihRyX//nn4sEmgA= 23 | github.com/gin-contrib/cors v1.3.1/go.mod h1:jjEJ4268OPZUcU7k9Pm653S7lXUGcqMADzFA61xsmDk= 24 | github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3 h1:t8FVkw33L+wilf2QiWkw0UV77qRpcH/JHPKGpKa2E8g= 25 | github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= 26 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 27 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 28 | github.com/gin-gonic/gin v1.4.0 h1:3tMoCCfM7ppqsR0ptz/wi1impNpT7/9wQtMZ8lr1mCQ= 29 | github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM= 30 | github.com/gin-gonic/gin v1.5.0 h1:fi+bqFAx/oLK54somfCtEZs9HeH1LHVoEPUgARpTqyc= 31 | github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do= 32 | github.com/go-playground/locales v0.12.1 h1:2FITxuFt/xuCNP1Acdhv62OzaCiviiE4kotfhkmOqEc= 33 | github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= 34 | github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= 35 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 36 | github.com/go-playground/universal-translator v0.16.0 h1:X++omBR/4cE2MNg91AoC3rmGrCjJ8eAeUP/K/EKx4DM= 37 | github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY= 38 | github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= 39 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 40 | github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= 41 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 42 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 43 | github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= 44 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 45 | github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= 46 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 47 | github.com/golang/protobuf v1.3.4 h1:87PNWwrRvUSnqS4dlcBU/ftvOIBep4sYuBLlh6rX2wk= 48 | github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 49 | github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= 50 | github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 51 | github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= 52 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 53 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 54 | github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= 55 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 56 | github.com/iancoleman/strcase v0.0.0-20190422225806-e506e3ef7365 h1:ECW73yc9MY7935nNYXUkK7Dz17YuSUI9yqRqYS8aBww= 57 | github.com/iancoleman/strcase v0.0.0-20190422225806-e506e3ef7365/go.mod h1:SK73tn/9oHe+/Y0h39VT4UCxmurVJkR5NA7kMEAOgSE= 58 | github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M= 59 | github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 60 | github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= 61 | github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= 62 | github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs= 63 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 64 | github.com/json-iterator/go v1.1.7 h1:KfgG9LzI+pYjr4xvmz/5H4FXjokeP+rlHLhv3iH62Fo= 65 | github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 66 | github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= 67 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 68 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 69 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 70 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 71 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 72 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 73 | github.com/leodido/go-urn v1.1.0 h1:Sm1gr51B1kKyfD2BlRcLSiEkffoG96g6TPv6eRoEiB8= 74 | github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= 75 | github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= 76 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 77 | github.com/mattn/go-isatty v0.0.7 h1:UvyT9uN+3r7yLEYSlJsbQGdsaB/a0DlgWP3pql6iwOc= 78 | github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 79 | github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg= 80 | github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= 81 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 82 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 83 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 84 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 85 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 86 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 87 | github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= 88 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 89 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 90 | github.com/onsi/ginkgo v1.12.0 h1:Iw5WCbBcaAAd0fpRb1c9r5YCylv4XDoCSigm1zLevwU= 91 | github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg= 92 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 93 | github.com/onsi/gomega v1.9.0 h1:R1uwffexN6Pr340GtYRIdZmAiN4J+iw6WG4wog1DUXg= 94 | github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= 95 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 96 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 97 | github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc= 98 | github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= 99 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 100 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 101 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 102 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 103 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 104 | github.com/tidwall/gjson v1.3.5 h1:2oW9FBNu8qt9jy5URgrzsVx/T/KSn3qn/smJQ0crlDQ= 105 | github.com/tidwall/gjson v1.3.5/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls= 106 | github.com/tidwall/match v1.0.1 h1:PnKP62LPNxHKTwvHHZZzdOAOCtsJTjo6dZLCwpKm5xc= 107 | github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E= 108 | github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= 109 | github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= 110 | github.com/ugorji/go v1.1.4 h1:j4s+tAvLfL3bZyefP2SEWmhBzmuIlH/eqNuPdFPgngw= 111 | github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= 112 | github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= 113 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= 114 | github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= 115 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= 116 | github.com/vmihailenco/msgpack v3.3.3+incompatible h1:wapg9xDUZDzGCNFlwc5SqI1rvcciqcxEHac4CYj89xI= 117 | github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= 118 | github.com/watson-developer-cloud/go-sdk v0.10.0 h1:qUCMUQwOm/1w6eU4GZ6pU65OdZfjk79b6Vyxiih2xgg= 119 | github.com/watson-developer-cloud/go-sdk v0.10.0/go.mod h1:f2M0+zkTcfD8uf7iBc6VfGQfm1w74shLIAmOA7KQLfU= 120 | github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c h1:u40Z8hqBAAQyv+vATcGgV0YCnDjqSL7/q/JyPhhJSPk= 121 | github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I= 122 | github.com/xdg/stringprep v1.0.0 h1:d9X0esnoa3dFsV0FG35rAT0RIhYFlPq7MiP+DW89La0= 123 | github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y= 124 | go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk= 125 | go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= 126 | go.mongodb.org/mongo-driver v1.1.1 h1:Sq1fR+0c58RME5EoqKdjkiQAmPjmfHlZOoRI6fTUOcs= 127 | go.mongodb.org/mongo-driver v1.1.1/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= 128 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= 129 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 130 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 131 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c h1:uOCk1iQW6Vc18bnC13MfzScl+wdKBmM9Y9kU7Z83/lw= 132 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 133 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65 h1:+rhAzEzT3f4JtomfC371qB+0Ola2caSKcY69NUBZrRQ= 134 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 135 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 136 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= 137 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 138 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 139 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 140 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 141 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 142 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 143 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 144 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 h1:uYVVQ9WP/Ds2ROhcaGPeIdVq0RIXVLwsHlnvJ+cT1So= 145 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 146 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 147 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 148 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 149 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 150 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 151 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 152 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 153 | google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= 154 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 155 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 156 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 157 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 158 | gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= 159 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 160 | gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= 161 | gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= 162 | gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ= 163 | gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= 164 | gopkg.in/go-playground/validator.v9 v9.29.1 h1:SvGtYmN60a5CVKTOzMSyfzWDeZRxRuGvRQyEAKbw1xc= 165 | gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= 166 | gopkg.in/go-playground/validator.v9 v9.31.0 h1:bmXmP2RSNtFES+bn4uYuHT7iJFJv7Vj+an+ZQdDaD1M= 167 | gopkg.in/go-playground/validator.v9 v9.31.0/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= 168 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 169 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 170 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 171 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 172 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 173 | gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 174 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 175 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 176 | -------------------------------------------------------------------------------- /injection.go: -------------------------------------------------------------------------------- 1 | package neocortex 2 | 3 | type InInjection func(c *Context, in *Input) *Input 4 | 5 | func (engine *Engine) InjectAll(channel CommunicationChannel, middle InInjection) { 6 | if engine.generalInjection == nil { 7 | engine.generalInjection = map[CommunicationChannel]*InInjection{} 8 | } 9 | engine.generalInjection[channel] = &middle 10 | } 11 | 12 | func (engine *Engine) Inject(channel CommunicationChannel, matcher *Matcher, middle InInjection) { 13 | if engine.registeredInjection == nil { 14 | engine.registeredInjection = map[CommunicationChannel]map[*Matcher]*InInjection{} 15 | } 16 | 17 | if engine.registeredInjection[channel] == nil { 18 | engine.registeredInjection[channel] = map[*Matcher]*InInjection{} 19 | } 20 | 21 | engine.registeredInjection[channel][matcher] = &middle 22 | } 23 | -------------------------------------------------------------------------------- /input.go: -------------------------------------------------------------------------------- 1 | package neocortex 2 | 3 | type InputType string 4 | 5 | const InputText InputType = "text" 6 | const InputAudio InputType = "audio" 7 | const InputImage InputType = "image" 8 | const InputEmoji InputType = "emoji" 9 | 10 | type InputData struct { 11 | Type InputType `json:"type"` 12 | Value string `json:"value"` 13 | Data []byte `json:"data"` 14 | } 15 | 16 | // Input represent an Input for the cognitive service 17 | type Input struct { 18 | Data InputData `json:"data"` 19 | Entities []Entity `json:"entities"` 20 | Intents []Intent `json:"intents"` 21 | } 22 | -------------------------------------------------------------------------------- /intent.go: -------------------------------------------------------------------------------- 1 | package neocortex 2 | 3 | // Intent define any intent, and intent is like a wish, an intention 4 | type Intent struct { 5 | Intent string `json:"intent"` 6 | Confidence float64 `json:"confidence"` 7 | } 8 | -------------------------------------------------------------------------------- /log.go: -------------------------------------------------------------------------------- 1 | package neocortex 2 | 3 | // LogLevelType is level of the logs 4 | type LogLevelType string 5 | 6 | // Info is a level of log 7 | var Info LogLevelType = "info" 8 | 9 | // Error is a level of log 10 | var Error LogLevelType = "error" 11 | 12 | // Warn is a level of log 13 | var Warn LogLevelType = "warn" 14 | 15 | // LogMessage represents a snapshot of the messages around the dialog 16 | type LogMessage struct { 17 | Level LogLevelType `json:"level"` 18 | Message string `json:"message"` 19 | } 20 | -------------------------------------------------------------------------------- /matcher.go: -------------------------------------------------------------------------------- 1 | package neocortex 2 | 3 | type Match struct { 4 | Is string 5 | Confidence float64 6 | } 7 | 8 | type CMatch struct { 9 | Name string 10 | Value interface{} 11 | } 12 | 13 | type DialogNodeMatch struct { 14 | Title string 15 | Name string 16 | } 17 | 18 | type Matcher struct { 19 | DialogNode DialogNodeMatch 20 | Entity Match 21 | Intent Match 22 | ContextVariable CMatch 23 | AND *Matcher 24 | OR *Matcher 25 | } 26 | 27 | func (out *Output) Match(c *Context, matcher *Matcher) bool { 28 | ok := false 29 | for _, i := range out.Intents { 30 | if i.Intent == matcher.Intent.Is && i.Confidence > matcher.Intent.Confidence { 31 | ok = true 32 | } 33 | } 34 | 35 | for _, e := range out.Entities { 36 | if e.Entity == matcher.Entity.Is && e.Confidence > matcher.Entity.Confidence { 37 | ok = true 38 | } 39 | } 40 | 41 | if c.Variables != nil { 42 | for varName, varValue := range c.Variables { 43 | if matcher.ContextVariable.Name == varName { 44 | if matcher.ContextVariable.Value == varValue { 45 | ok = true 46 | } 47 | } 48 | } 49 | } 50 | 51 | if matcher.DialogNode.Title != "" || matcher.DialogNode.Name != "" { 52 | for _, n := range out.VisitedNodes { 53 | if matcher.DialogNode.Name != "" { 54 | if matcher.DialogNode.Title != "" { 55 | if n.Name == matcher.DialogNode.Name && n.Title == matcher.DialogNode.Title { 56 | ok = true 57 | } 58 | } 59 | 60 | if n.Name == matcher.DialogNode.Name { 61 | ok = true 62 | } 63 | } else if matcher.DialogNode.Title != "" { 64 | if n.Title == matcher.DialogNode.Title { 65 | ok = true 66 | } 67 | } 68 | } 69 | } 70 | 71 | if matcher.AND != nil { 72 | if out.Match(c, matcher.AND) && ok { 73 | ok = true 74 | } else { 75 | ok = false 76 | } 77 | } 78 | 79 | if matcher.OR != nil { 80 | if out.Match(c, matcher.OR) || ok { 81 | ok = true 82 | } else { 83 | ok = false 84 | } 85 | } 86 | 87 | return ok 88 | } 89 | 90 | func (in *Input) Match(c *Context, matcher *Matcher) bool { 91 | ok := false 92 | for _, i := range in.Intents { 93 | if i.Intent == matcher.Intent.Is && i.Confidence > matcher.Intent.Confidence { 94 | ok = true 95 | } 96 | } 97 | 98 | for _, e := range in.Entities { 99 | if e.Entity == matcher.Entity.Is && e.Confidence > matcher.Entity.Confidence { 100 | ok = true 101 | } 102 | } 103 | 104 | if c.Variables != nil { 105 | for varName, varValue := range c.Variables { 106 | if matcher.ContextVariable.Name == varName { 107 | if matcher.ContextVariable.Value == varValue { 108 | ok = true 109 | } 110 | } 111 | } 112 | } 113 | 114 | if matcher.AND != nil { 115 | if in.Match(c, matcher.AND) && ok { 116 | ok = true 117 | } else { 118 | ok = false 119 | } 120 | } 121 | 122 | if matcher.OR != nil { 123 | if in.Match(c, matcher.OR) || ok { 124 | ok = true 125 | } else { 126 | ok = false 127 | } 128 | } 129 | 130 | return ok 131 | 132 | } 133 | -------------------------------------------------------------------------------- /matcher_utils.go: -------------------------------------------------------------------------------- 1 | package neocortex 2 | 3 | func (m *Matcher) And(and *Matcher) *Matcher { 4 | m.AND = and 5 | return m 6 | } 7 | 8 | func (m *Matcher) Or(or *Matcher) *Matcher { 9 | m.OR = or 10 | return m 11 | } 12 | 13 | func IntentIs(intent string, confidence ...float64) *Matcher { 14 | conf := 0.0 15 | if len(confidence) > 0 { 16 | conf = confidence[0] 17 | } 18 | return &Matcher{ 19 | Intent: Match{ 20 | Is: intent, 21 | Confidence: conf, 22 | }, 23 | } 24 | } 25 | 26 | func (m *Matcher) AndIntentIs(intent string, confidence ...float64) *Matcher { 27 | return m.And(IntentIs(intent, confidence...)) 28 | } 29 | 30 | func (m *Matcher) OrIntentIs(intent string, confidence ...float64) *Matcher { 31 | return m.Or(IntentIs(intent, confidence...)) 32 | } 33 | 34 | func IfEntityIs(entity string, confidence ...float64) *Matcher { 35 | conf := 0.0 36 | if len(confidence) > 0 { 37 | conf = confidence[0] 38 | } 39 | return &Matcher{ 40 | Entity: Match{ 41 | Is: entity, 42 | Confidence: conf, 43 | }, 44 | } 45 | } 46 | 47 | func (m *Matcher) AndEntityIs(entity string, confidence ...float64) *Matcher { 48 | return m.And(IfEntityIs(entity, confidence...)) 49 | } 50 | 51 | func (m *Matcher) OrEntityIs(entity string, confidence ...float64) *Matcher { 52 | return m.Or(IfEntityIs(entity, confidence...)) 53 | } 54 | 55 | func IfContextVariableIs(name string, value interface{}) *Matcher { 56 | return &Matcher{ 57 | ContextVariable: CMatch{ 58 | Name: name, 59 | Value: value, 60 | }, 61 | } 62 | } 63 | 64 | func (m *Matcher) AndIfContextVariableIs(name string, value interface{}) *Matcher { 65 | return m.And(IfContextVariableIs(name, value)) 66 | } 67 | 68 | func (m *Matcher) OrIfContextVariableIs(name string, value interface{}) *Matcher { 69 | return m.Or(IfContextVariableIs(name, value)) 70 | } 71 | 72 | func IfDialogNodeTitleIs(title string) *Matcher { 73 | return &Matcher{ 74 | DialogNode: DialogNodeMatch{ 75 | Title: title, 76 | }, 77 | } 78 | } 79 | 80 | func (m *Matcher) AndIfDialogNodeTitleIs(title string) *Matcher { 81 | return m.And(IfDialogNodeTitleIs(title)) 82 | } 83 | 84 | func (m *Matcher) OrIfDialogNodeTitleIs(title string) *Matcher { 85 | return m.Or(IfDialogNodeTitleIs(title)) 86 | } 87 | 88 | func IfDialogNodeNameIs(name string) *Matcher { 89 | return &Matcher{ 90 | DialogNode: DialogNodeMatch{ 91 | Name: name, 92 | }, 93 | } 94 | } 95 | 96 | func (m *Matcher) AndIfDialogNodeNameIs(name string) *Matcher { 97 | return m.And(IfDialogNodeNameIs(name)) 98 | } 99 | 100 | func (m *Matcher) OrIfDialogNodeNameIs(name string) *Matcher { 101 | return m.Or(IfDialogNodeNameIs(name)) 102 | } 103 | 104 | func IfDialogNodeIs(title, name string) *Matcher { 105 | return &Matcher{ 106 | DialogNode: DialogNodeMatch{ 107 | Title: title, 108 | Name: name, 109 | }, 110 | } 111 | } 112 | 113 | func (m *Matcher) AndIfDialogNodeIs(title, name string) *Matcher { 114 | return m.And(IfDialogNodeIs(title, name)) 115 | } 116 | 117 | func (m *Matcher) OrIfDialogNodeIs(title, name string) *Matcher { 118 | return m.Or(IfDialogNodeIs(title, name)) 119 | } 120 | -------------------------------------------------------------------------------- /node.go: -------------------------------------------------------------------------------- 1 | package neocortex 2 | 3 | // DialogNode represents a node into complex tree of any dialog 4 | type DialogNode struct { 5 | Name string `json:"name"` 6 | Title string `json:"title"` 7 | Conditions string `json:"conditions"` 8 | } 9 | -------------------------------------------------------------------------------- /options.go: -------------------------------------------------------------------------------- 1 | package neocortex 2 | 3 | type Option struct { 4 | Text string `json:"text"` 5 | Action string `json:"action"` 6 | IsPostBack bool `json:"is_post_back"` 7 | } 8 | 9 | type OptionsResponse struct { 10 | Title string `json:"title"` 11 | Description string `json:"description"` 12 | ItemURL string `json:"item_url"` 13 | Image string `json:"image"` 14 | Options []*Option `json:"options"` 15 | } 16 | -------------------------------------------------------------------------------- /output.go: -------------------------------------------------------------------------------- 1 | package neocortex 2 | 3 | // ResponseType define the types of generic response 4 | type ResponseType string 5 | 6 | // Text is a kind of generic response 7 | const Text ResponseType = "text" 8 | 9 | // Pause is a kind of generic response 10 | const Pause ResponseType = "pause" 11 | 12 | // Image is a kind of generic response 13 | const Image ResponseType = "image" 14 | 15 | // Options is a kind of generic response 16 | const Options ResponseType = "option" 17 | 18 | // ConnectToAgent is a kind of generic response 19 | // var ConnectToAgent ResponseType = "connect_to_agent" 20 | 21 | const Suggestion ResponseType = "suggestion" 22 | 23 | const Unknown ResponseType = "unknown" 24 | 25 | type Response struct { 26 | IsTyping bool `json:"is_typing"` 27 | Type ResponseType `json:"type"` 28 | Value interface{} `json:"value"` 29 | } 30 | 31 | // Output represents the response of an input from the cognitive service 32 | type Output struct { 33 | Entities []Entity `json:"entities"` 34 | Intents []Intent `json:"intents"` 35 | VisitedNodes []*DialogNode `json:"visited_nodes"` 36 | Logs []*LogMessage `json:"logs"` 37 | Responses []Response `json:"responses"` 38 | } 39 | -------------------------------------------------------------------------------- /output_adders.go: -------------------------------------------------------------------------------- 1 | package neocortex 2 | 3 | import "time" 4 | 5 | func (out *Output) AddTextResponse(resp string) *Output { 6 | if out.Responses == nil { 7 | out.Responses = []Response{} 8 | } 9 | out.Responses = append(out.Responses, Response{ 10 | Type: Text, 11 | Value: resp, 12 | IsTyping: false, 13 | }) 14 | 15 | return out 16 | } 17 | 18 | func (out *Output) AddOptionsResponse(title string, subtitle string, options ...Option) *Output { 19 | if out.Responses == nil { 20 | out.Responses = []Response{} 21 | } 22 | opts := make([]*Option, 0) 23 | for _, o := range options { 24 | opts = append(opts, &Option{ 25 | Text: o.Text, 26 | IsPostBack: o.IsPostBack, 27 | Action: o.Action, 28 | }) 29 | } 30 | out.Responses = append(out.Responses, Response{ 31 | Type: Options, 32 | Value: OptionsResponse{ 33 | Title: title, 34 | Description: subtitle, 35 | Options: opts, 36 | }, 37 | IsTyping: false, 38 | }) 39 | 40 | return out 41 | } 42 | 43 | func (out *Output) AddListOfOptionsResponse(options []OptionsResponse) *Output { 44 | if out.Responses == nil { 45 | out.Responses = []Response{} 46 | } 47 | 48 | out.Responses = append(out.Responses, Response{ 49 | Type: Options, 50 | Value: options, 51 | IsTyping: false, 52 | }) 53 | 54 | return out 55 | } 56 | 57 | func (out *Output) AddImageResponse(url string) *Output { 58 | if out.Responses == nil { 59 | out.Responses = []Response{} 60 | } 61 | 62 | out.Responses = append(out.Responses, Response{ 63 | Type: Image, 64 | Value: url, 65 | IsTyping: false, 66 | }) 67 | 68 | return out 69 | } 70 | 71 | func (out *Output) AddPauseResponse(duration time.Duration) *Output { 72 | if out.Responses == nil { 73 | out.Responses = []Response{} 74 | } 75 | 76 | out.Responses = append(out.Responses, Response{ 77 | Type: Pause, 78 | Value: duration, 79 | IsTyping: false, 80 | }) 81 | 82 | return out 83 | } 84 | -------------------------------------------------------------------------------- /output_utils.go: -------------------------------------------------------------------------------- 1 | package neocortex 2 | 3 | func (out *Output) ClearResponses() *Output { 4 | out.Responses = []Response{} 5 | return out 6 | } 7 | 8 | func (out *Output) Clean() *Output { 9 | out.Responses = []Response{} 10 | return out 11 | } 12 | -------------------------------------------------------------------------------- /pipeline.go: -------------------------------------------------------------------------------- 1 | package neocortex 2 | 3 | type Pipeline struct { 4 | in []CommunicationChannel 5 | } 6 | -------------------------------------------------------------------------------- /proto/neocortex.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package neocortex; 4 | 5 | message Input { 6 | required string type = 1; 7 | required bytes value = 2; 8 | } -------------------------------------------------------------------------------- /repositories/boltdb/operations.go: -------------------------------------------------------------------------------- 1 | package boltdb 2 | 3 | import ( 4 | "github.com/minskylab/neocortex" 5 | "github.com/rs/xid" 6 | ) 7 | 8 | func (repo *Repository) SaveNewDialog(dialog *neocortex.Dialog) (*neocortex.Dialog, error) { 9 | if dialog.ID == "" { 10 | dialog.ID = xid.New().String() 11 | } 12 | err := repo.db.Save(dialog) 13 | if err != nil { 14 | return nil, err 15 | } 16 | return repo.GetDialogByID(dialog.ID) 17 | } 18 | 19 | func (repo *Repository) GetDialogByID(id string) (*neocortex.Dialog, error) { 20 | dialog := new(neocortex.Dialog) 21 | err := repo.db.One("ID", id, dialog) 22 | if err != nil { 23 | return nil, err 24 | } 25 | return dialog, nil 26 | } 27 | 28 | func (repo *Repository) GetAllDialogs() ([]*neocortex.Dialog, error) { 29 | var allDialogs []*neocortex.Dialog 30 | err := repo.db.All(&allDialogs) 31 | if err != nil { 32 | return nil, err 33 | } 34 | return allDialogs, nil 35 | } 36 | 37 | func (repo *Repository) DeleteDialog(id string) (*neocortex.Dialog, error) { 38 | d, err := repo.GetDialogByID(id) 39 | if err != nil { 40 | return nil, err 41 | } 42 | err = repo.db.DeleteStruct(d) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | return d, err 48 | } 49 | 50 | func (repo *Repository) UpdateDialog(dialog *neocortex.Dialog) (*neocortex.Dialog, error) { 51 | err := repo.db.Update(dialog) 52 | if err != nil { 53 | return nil, err 54 | } 55 | 56 | return repo.GetDialogByID(dialog.ID) 57 | } 58 | -------------------------------------------------------------------------------- /repositories/boltdb/repository.go: -------------------------------------------------------------------------------- 1 | package boltdb 2 | 3 | import "github.com/asdine/storm" 4 | 5 | type Repository struct { 6 | filename string 7 | db *storm.DB 8 | } 9 | 10 | func New(path string) (*Repository, error) { 11 | db, err := storm.Open(path) 12 | if err != nil { 13 | return nil, err 14 | } 15 | 16 | return &Repository{ 17 | db: db, 18 | filename: path, 19 | }, nil 20 | } 21 | -------------------------------------------------------------------------------- /repositories/memory/repo.go: -------------------------------------------------------------------------------- 1 | package memory 2 | 3 | import ( 4 | "github.com/minskylab/neocortex" 5 | ) 6 | 7 | type InMemoryRepo struct { 8 | dialogs []*neocortex.Dialog 9 | } 10 | 11 | func (m *InMemoryRepo) SaveDialog(dialog *neocortex.Dialog) error { 12 | m.dialogs = append(m.dialogs, dialog) 13 | return nil 14 | } 15 | -------------------------------------------------------------------------------- /repositories/mongodb/operations.go: -------------------------------------------------------------------------------- 1 | package mongodb 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "fmt" 8 | 9 | "github.com/jinzhu/now" 10 | 11 | "go.mongodb.org/mongo-driver/mongo/options" 12 | 13 | "go.mongodb.org/mongo-driver/bson/primitive" 14 | 15 | "github.com/minskylab/neocortex" 16 | "github.com/rs/xid" 17 | "go.mongodb.org/mongo-driver/bson" 18 | ) 19 | 20 | func (repo *Repository) SaveDialog(dialog *neocortex.Dialog) error { 21 | if dialog.ID == "" { 22 | dialog.ID = xid.New().String() 23 | } 24 | 25 | dialog.LastActivity = time.Now() 26 | 27 | _, err := repo.dialogs.InsertOne(context.Background(), dialog) 28 | if err != nil { 29 | return err 30 | } 31 | 32 | return nil 33 | } 34 | 35 | func (repo *Repository) GetDialogByID(id string) (*neocortex.Dialog, error) { 36 | dialog := new(neocortex.Dialog) 37 | if err := repo.dialogs.FindOne(context.Background(), bson.M{"id": id}).Decode(dialog); err != nil { 38 | return nil, err 39 | } 40 | 41 | return dialog, nil 42 | } 43 | 44 | func (repo *Repository) AllDialogs(frame neocortex.TimeFrame) ([]*neocortex.Dialog, error) { 45 | from := frame.From 46 | to := frame.To 47 | 48 | switch frame.Preset { 49 | case neocortex.DayPreset: 50 | from = now.BeginningOfDay() 51 | to = now.EndOfDay() 52 | case neocortex.WeekPreset: 53 | from = now.BeginningOfWeek() 54 | to = now.EndOfWeek() 55 | case neocortex.MonthPreset: 56 | from = now.BeginningOfMonth() 57 | to = now.EndOfMonth() 58 | case neocortex.YearPreset: 59 | from = now.BeginningOfYear() 60 | to = now.EndOfYear() 61 | // default: 62 | // return nil, errors.New("invalid preset, please choose between: day, week, month or year") 63 | } 64 | 65 | // * Mongo pleaseeee 66 | from = from.Truncate(time.Second) 67 | to = to.Truncate(time.Second) 68 | 69 | filter := bson.M{ 70 | "start_at": bson.M{ 71 | "$gte": primitive.DateTime(from.UnixNano() / 1000000), 72 | "$lte": primitive.DateTime(to.UnixNano() / 1000000), 73 | }, 74 | } 75 | 76 | size := int64(frame.PageSize) 77 | if size <= 0 { 78 | size = 20 79 | } 80 | 81 | skips := int64(size * int64(frame.PageNum-1)) 82 | if skips <= 0 { 83 | skips = 0 84 | } 85 | 86 | opts := options.Find().SetLimit(size).SetSkip(skips) 87 | 88 | cursor, err := repo.dialogs.Find( 89 | context.Background(), 90 | filter, 91 | opts, 92 | ) 93 | 94 | if err != nil { 95 | return nil, err 96 | } 97 | 98 | dialogs := make([]*neocortex.Dialog, 0) 99 | for cursor.Next(context.Background()) { 100 | dialog := new(neocortex.Dialog) 101 | if err := cursor.Decode(dialog); err != nil { 102 | m := bson.M{} 103 | if err := cursor.Decode(&m); err != nil { 104 | continue 105 | } 106 | dialog.ID, _ = m["id"].(string) 107 | 108 | start, _ := m["start_at"].(primitive.DateTime) 109 | end, _ := m["end_at"].(primitive.DateTime) 110 | 111 | // * Mongo pleaseeee 112 | s := time.Unix(int64(start)/1000, int64(start)%1000*1000000) 113 | e := time.Unix(int64(end)/1000, int64(end)%1000*1000000) 114 | 115 | dialog.StartAt = s 116 | dialog.EndAt = e 117 | dialog.Ins = []*neocortex.InputRecord{} 118 | dialog.Outs = []*neocortex.OutputRecord{} 119 | dialog.Contexts = []*neocortex.ContextRecord{} 120 | } 121 | 122 | if dialog.Ins != nil && dialog.Outs != nil { 123 | if len(dialog.Ins) != 0 && len(dialog.Outs) != 0 { 124 | dialogs = append(dialogs, dialog) 125 | } 126 | } 127 | 128 | } 129 | 130 | return dialogs, nil 131 | } 132 | 133 | func (repo *Repository) DeleteDialog(id string) (*neocortex.Dialog, error) { 134 | dialog, err := repo.GetDialogByID(id) 135 | if err != nil { 136 | return nil, err 137 | } 138 | _, err = repo.dialogs.DeleteOne(context.Background(), bson.M{"id": id}) 139 | if err != nil { 140 | return nil, err 141 | } 142 | 143 | return dialog, nil 144 | } 145 | 146 | func checkDialogInView(dialog *neocortex.Dialog, view *neocortex.View) bool { 147 | for _, c := range view.Classes { 148 | switch c.Type { 149 | case neocortex.EntityClass: 150 | if dialog.HasEntity(c.Value) { 151 | return true 152 | } 153 | case neocortex.IntentClass: 154 | if dialog.HasIntent(c.Value) { 155 | return true 156 | } 157 | case neocortex.DialogNodeClass: 158 | if dialog.HasDialogNode(c.Value) { 159 | return true 160 | } 161 | default: 162 | return false 163 | } 164 | } 165 | 166 | return false 167 | } 168 | 169 | func (repo *Repository) DialogsByView(viewID string, frame neocortex.TimeFrame) ([]*neocortex.Dialog, error) { 170 | view, err := repo.GetViewByID(viewID) 171 | if err != nil { 172 | return nil, err 173 | } 174 | 175 | dialogs, err := repo.AllDialogs(frame) 176 | if err != nil { 177 | return nil, err 178 | } 179 | 180 | filteredDialogs := make([]*neocortex.Dialog, 0) 181 | 182 | for _, dialog := range dialogs { 183 | if checkDialogInView(dialog, view) { 184 | filteredDialogs = append(filteredDialogs, dialog) 185 | } 186 | } 187 | 188 | return filteredDialogs, nil 189 | } 190 | 191 | func (repo *Repository) RegisterIntent(intent string) error { 192 | coll := new(collection) 193 | err := repo.collections.FindOne(context.Background(), bson.M{"box": "intents"}).Decode(coll) 194 | if err != nil { 195 | return err 196 | } 197 | 198 | for _, v := range coll.Values { 199 | if v == intent { 200 | return nil 201 | } 202 | } 203 | 204 | coll.Values = append(coll.Values, intent) 205 | 206 | _, err = repo.collections.UpdateOne(context.Background(), bson.M{"box": "intents"}, bson.M{"$set": bson.M{"values": coll.Values}}) 207 | if err != nil { 208 | return err 209 | } 210 | 211 | return nil 212 | } 213 | 214 | func (repo *Repository) RegisterEntity(entity string) error { 215 | coll := new(collection) 216 | err := repo.collections.FindOne(context.Background(), bson.M{"box": "entities"}).Decode(coll) 217 | if err != nil { 218 | return err 219 | } 220 | 221 | for _, v := range coll.Values { 222 | if v == entity { 223 | return nil 224 | } 225 | } 226 | 227 | coll.Values = append(coll.Values, entity) 228 | 229 | _, err = repo.collections.UpdateOne(context.Background(), bson.M{"box": "entities"}, bson.M{"$set": bson.M{"values": coll.Values}}) 230 | if err != nil { 231 | return err 232 | } 233 | 234 | return nil 235 | } 236 | 237 | func (repo *Repository) RegisterDialogNode(name string) error { 238 | coll := new(collection) 239 | err := repo.collections.FindOne(context.Background(), bson.M{"box": "nodes"}).Decode(coll) 240 | if err != nil { 241 | return err 242 | } 243 | 244 | for _, v := range coll.Values { 245 | if v == name { 246 | return nil 247 | } 248 | } 249 | 250 | coll.Values = append(coll.Values, name) 251 | 252 | _, err = repo.collections.UpdateOne(context.Background(), bson.M{"box": "nodes"}, bson.M{"$set": bson.M{"values": coll.Values}}) 253 | if err != nil { 254 | return err 255 | } 256 | 257 | return nil 258 | } 259 | 260 | func (repo *Repository) RegisterContextVar(value string) error { 261 | coll := new(collection) 262 | err := repo.collections.FindOne(context.Background(), bson.M{"box": "context_vars"}).Decode(coll) 263 | if err != nil { 264 | return err 265 | } 266 | 267 | for _, v := range coll.Values { 268 | if v == value { 269 | return nil 270 | } 271 | } 272 | 273 | coll.Values = append(coll.Values, value) 274 | 275 | _, err = repo.collections.UpdateOne(context.Background(), bson.M{"box": "context_vars"}, bson.M{"$set": bson.M{"values": coll.Values}}) 276 | if err != nil { 277 | return err 278 | } 279 | 280 | return nil 281 | } 282 | 283 | func (repo *Repository) Intents() []string { 284 | coll := new(collection) 285 | err := repo.collections.FindOne(context.Background(), bson.M{"box": "intents"}).Decode(coll) 286 | if err != nil { 287 | return nil 288 | } 289 | 290 | return coll.Values 291 | } 292 | 293 | func (repo *Repository) Entities() []string { 294 | coll := new(collection) 295 | err := repo.collections.FindOne(context.Background(), bson.M{"box": "entities"}).Decode(coll) 296 | if err != nil { 297 | return nil 298 | } 299 | 300 | return coll.Values 301 | } 302 | 303 | func (repo *Repository) DialogNodes() []string { 304 | coll := new(collection) 305 | err := repo.collections.FindOne(context.Background(), bson.M{"box": "nodes"}).Decode(coll) 306 | if err != nil { 307 | return nil 308 | } 309 | 310 | return coll.Values 311 | } 312 | 313 | func (repo *Repository) ContextVars() []string { 314 | coll := new(collection) 315 | err := repo.collections.FindOne(context.Background(), bson.M{"box": "context_vars"}).Decode(coll) 316 | if err != nil { 317 | return nil 318 | } 319 | 320 | return coll.Values 321 | } 322 | 323 | func (repo *Repository) SaveView(view *neocortex.View) error { 324 | if view.ID == "" { 325 | view.ID = xid.New().String() 326 | } 327 | 328 | _, err := repo.views.InsertOne(context.Background(), view) 329 | if err != nil { 330 | return err 331 | } 332 | 333 | return nil 334 | } 335 | 336 | func (repo *Repository) GetViewByID(id string) (*neocortex.View, error) { 337 | view := new(neocortex.View) 338 | if err := repo.views.FindOne(context.Background(), bson.M{"id": id}).Decode(view); err != nil { 339 | return nil, err 340 | } 341 | 342 | return view, nil 343 | } 344 | 345 | func (repo *Repository) FindViewByName(name string) ([]*neocortex.View, error) { 346 | 347 | c, err := repo.views.Find(context.Background(), bson.M{"name": name}) 348 | if err != nil { 349 | return nil, err 350 | } 351 | 352 | views := make([]*neocortex.View, 0) 353 | if err != nil { 354 | return nil, err 355 | } 356 | 357 | for c.Next(context.Background()) { 358 | view := new(neocortex.View) 359 | if err := c.Decode(view); err != nil { 360 | return nil, err 361 | } 362 | views = append(views, view) 363 | } 364 | 365 | return views, nil 366 | } 367 | 368 | func (repo *Repository) AllViews() ([]*neocortex.View, error) { 369 | c, err := repo.views.Find(context.Background(), bson.M{}) 370 | if err != nil { 371 | return nil, err 372 | } 373 | 374 | views := make([]*neocortex.View, 0) 375 | if err != nil { 376 | return nil, err 377 | } 378 | 379 | for c.Next(context.Background()) { 380 | view := new(neocortex.View) 381 | if err := c.Decode(view); err != nil { 382 | return nil, err 383 | } 384 | views = append(views, view) 385 | } 386 | 387 | return views, nil 388 | } 389 | 390 | //! MAKE GOOD UPDATE IN UPDATE VIEW 391 | func (repo *Repository) UpdateView(view *neocortex.View) error { 392 | return nil 393 | // _, err := repo.views.UpdateOne(context.Background(), bson.M{"id": view.ID}, bson.M{ 394 | // "name": view.Name, 395 | // "styles": view.Styles, 396 | // "classes": view.Classes, 397 | // "children": view.Children, 398 | // }) 399 | 400 | // return err 401 | } 402 | 403 | func (repo *Repository) DeleteView(id string) (*neocortex.View, error) { 404 | view, err := repo.GetViewByID(id) 405 | if err != nil { 406 | return nil, err 407 | } 408 | 409 | del, err := repo.views.DeleteOne(context.Background(), bson.M{"id": view.ID}) 410 | if err != nil { 411 | return nil, err 412 | } 413 | fmt.Println("del.DeletedCount =", del.DeletedCount) 414 | 415 | return view, nil 416 | 417 | } 418 | 419 | func (repo *Repository) SetActionVar(name string, value string) error { 420 | act := new(action) 421 | err := repo.actions.FindOne(context.Background(), bson.M{"name": "envs"}).Decode(act) 422 | if err != nil { 423 | return err 424 | } 425 | 426 | act.Vars[name] = value 427 | 428 | _, err = repo.actions.UpdateOne(context.Background(), bson.M{"name": "envs"}, bson.M{"$set": bson.M{"vars": act.Vars}}) 429 | return err 430 | } 431 | 432 | func (repo *Repository) GetActionVar(name string) (string, error) { 433 | act := new(action) 434 | err := repo.actions.FindOne(context.Background(), bson.M{"name": "envs"}).Decode(act) 435 | if err != nil { 436 | return "", err 437 | } 438 | 439 | return act.Vars[name], nil 440 | } 441 | 442 | func (repo *Repository) Summary(frame neocortex.TimeFrame) (*neocortex.Summary, error) { 443 | 444 | summary := neocortex.Summary{} 445 | summary.UsersByTimezone = map[string]neocortex.UsersSummary{} 446 | 447 | from := frame.From 448 | to := frame.To 449 | 450 | switch frame.Preset { 451 | case neocortex.DayPreset: 452 | from = now.BeginningOfDay() 453 | to = now.EndOfDay() 454 | case neocortex.WeekPreset: 455 | from = now.BeginningOfWeek() 456 | to = now.EndOfWeek() 457 | case neocortex.MonthPreset: 458 | from = now.BeginningOfMonth() 459 | to = now.EndOfMonth() 460 | case neocortex.YearPreset: 461 | from = now.BeginningOfYear() 462 | to = now.EndOfYear() 463 | // default: 464 | // return nil, errors.New("invalid preset, please choose between: day, week, month or year") 465 | } 466 | 467 | // * Mongo pleaseeee 468 | 469 | filter := bson.M{ 470 | "start_at": bson.M{ 471 | "$gte": primitive.DateTime(from.UnixNano() / 1000000), 472 | "$lte": primitive.DateTime(to.UnixNano() / 1000000), 473 | }, 474 | } 475 | 476 | fmt.Println(" ======== SUMMARY ======== ") 477 | fmt.Printf("From: %s\tTo: %s\n", from, to) 478 | 479 | opts := options.Find().SetLimit(1e8) // 100000000 480 | 481 | fmt.Printf("opts: %v\n", opts.Limit) 482 | 483 | cursor, err := repo.dialogs.Find(context.Background(), filter, opts) 484 | if err != nil { 485 | return nil, err 486 | } 487 | 488 | performanceAccum := 0.0 489 | totalCorrectDialogs := int64(0) 490 | 491 | usersByTimezone := map[string]map[string]int{} 492 | 493 | for cursor.Next(context.Background()) { 494 | dialog := new(neocortex.Dialog) 495 | err = cursor.Decode(dialog) 496 | if err != nil { 497 | continue 498 | } 499 | 500 | if len(dialog.Contexts) > 0 { 501 | c := dialog.Contexts[0] 502 | if usersByTimezone[c.Context.Person.Timezone] == nil { 503 | usersByTimezone[c.Context.Person.Timezone] = map[string]int{} 504 | } 505 | usersByTimezone[c.Context.Person.Timezone][c.Context.Person.Name]++ 506 | } 507 | 508 | if len(dialog.Ins) > 0 { 509 | performanceAccum += dialog.Performance 510 | totalCorrectDialogs++ 511 | } 512 | } 513 | 514 | if totalCorrectDialogs == 0 { 515 | return &neocortex.Summary{ 516 | TotalDialogs: 0, 517 | TotalUsers: 0, 518 | RecurrentUsers: 0, 519 | PerformanceMean: 0.0, 520 | // UsersByTimezone: map[string, 521 | }, nil 522 | } 523 | 524 | totalUsers := int64(0) 525 | totalRecurrent := int64(0) 526 | 527 | for timezone, totalUsersByTimezone := range usersByTimezone { 528 | recurrent := 0 529 | for _, r := range totalUsersByTimezone { 530 | if r > 1 { 531 | recurrent++ 532 | } 533 | } 534 | 535 | summary.UsersByTimezone[timezone] = neocortex.UsersSummary{ 536 | Recurrents: int64(recurrent), 537 | News: int64(len(totalUsersByTimezone) - recurrent), 538 | } 539 | 540 | totalUsers += int64(len(totalUsersByTimezone)) 541 | totalRecurrent += int64(recurrent) 542 | } 543 | 544 | summary.TotalUsers = totalUsers 545 | summary.RecurrentUsers = totalRecurrent 546 | summary.TotalDialogs = totalCorrectDialogs 547 | summary.PerformanceMean = performanceAccum / float64(totalCorrectDialogs) 548 | 549 | return &summary, nil 550 | } 551 | -------------------------------------------------------------------------------- /repositories/mongodb/repository.go: -------------------------------------------------------------------------------- 1 | package mongodb 2 | 3 | import ( 4 | "context" 5 | 6 | "go.mongodb.org/mongo-driver/bson" 7 | "go.mongodb.org/mongo-driver/mongo" 8 | "go.mongodb.org/mongo-driver/mongo/options" 9 | ) 10 | 11 | type Repository struct { 12 | client *mongo.Client 13 | dialogs *mongo.Collection 14 | views *mongo.Collection 15 | actions *mongo.Collection 16 | collections *mongo.Collection 17 | } 18 | 19 | type collection struct { 20 | Box string `json:"box"` 21 | Values []string `json:"values"` 22 | } 23 | 24 | type action struct { 25 | Name string `json:"name"` 26 | Vars map[string]string `json:"vars"` 27 | } 28 | 29 | func New(uri string) (*Repository, error) { 30 | 31 | client, err := mongo.NewClient(options.Client().ApplyURI(uri)) 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | err = client.Connect(context.Background()) 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | dialogs := client.Database("neocortex").Collection("dialogs") 42 | views := client.Database("neocortex").Collection("views") 43 | actions := client.Database("neocortex").Collection("actions") 44 | collections := client.Database("neocortex").Collection("collections") 45 | 46 | // * Creating different 'boxes' for intents, entities, dialog nodes and context variables 47 | 48 | coll := new(collection) 49 | if err := collections.FindOne(context.Background(), bson.M{"box": "intents"}).Decode(coll); err != nil { 50 | _, err := collections.InsertOne(context.Background(), collection{Box: "intents", Values: []string{}}) 51 | if err != nil { 52 | return nil, err 53 | } 54 | } 55 | 56 | if err := collections.FindOne(context.Background(), bson.M{"box": "entities"}).Decode(coll); err != nil { 57 | _, err := collections.InsertOne(context.Background(), collection{Box: "entities", Values: []string{}}) 58 | if err != nil { 59 | return nil, err 60 | } 61 | } 62 | 63 | if err := collections.FindOne(context.Background(), bson.M{"box": "nodes"}).Decode(coll); err != nil { 64 | _, err := collections.InsertOne(context.Background(), collection{Box: "nodes", Values: []string{}}) 65 | if err != nil { 66 | return nil, err 67 | } 68 | } 69 | 70 | if err := collections.FindOne(context.Background(), bson.M{"box": "context_vars"}).Decode(coll); err != nil { 71 | _, err := collections.InsertOne(context.Background(), collection{Box: "context_vars", Values: []string{}}) 72 | if err != nil { 73 | return nil, err 74 | } 75 | } 76 | 77 | act := new(action) 78 | if err := actions.FindOne(context.Background(), bson.M{"name": "envs"}).Decode(act); err != nil { 79 | _, err := actions.InsertOne(context.Background(), action{Name: "envs", Vars: map[string]string{}}) 80 | if err != nil { 81 | return nil, err 82 | } 83 | } 84 | 85 | return &Repository{ 86 | client: client, 87 | dialogs: dialogs, 88 | views: views, 89 | actions: actions, 90 | collections: collections, 91 | }, nil 92 | } 93 | -------------------------------------------------------------------------------- /repository.go: -------------------------------------------------------------------------------- 1 | package neocortex 2 | 3 | import "time" 4 | 5 | type TimeFramePreset string 6 | 7 | const DayPreset TimeFramePreset = "day" 8 | const MonthPreset TimeFramePreset = "month" 9 | const WeekPreset TimeFramePreset = "week" 10 | const YearPreset TimeFramePreset = "year" 11 | 12 | type TimeFrame struct { 13 | From time.Time 14 | To time.Time 15 | Preset TimeFramePreset 16 | PageSize int 17 | PageNum int 18 | } 19 | 20 | type Repository interface { 21 | SaveDialog(dialog *Dialog) error 22 | GetDialogByID(id string) (*Dialog, error) 23 | AllDialogs(frame TimeFrame) ([]*Dialog, error) // to page or not to page? 24 | DeleteDialog(id string) (*Dialog, error) 25 | 26 | // Dialogs are inmutable, cause it doesn't have an updater 27 | DialogsByView(viewID string, frame TimeFrame) ([]*Dialog, error) 28 | Summary(frame TimeFrame) (*Summary, error) 29 | 30 | RegisterIntent(intent string) error 31 | RegisterEntity(entity string) error 32 | RegisterDialogNode(name string) error 33 | RegisterContextVar(value string) error 34 | Intents() []string 35 | Entities() []string 36 | DialogNodes() []string 37 | ContextVars() []string 38 | 39 | SaveView(view *View) error 40 | GetViewByID(id string) (*View, error) 41 | FindViewByName(name string) ([]*View, error) 42 | AllViews() ([]*View, error) 43 | UpdateView(view *View) error 44 | DeleteView(id string) (*View, error) 45 | 46 | SetActionVar(name string, value string) error 47 | GetActionVar(name string) (string, error) 48 | } 49 | -------------------------------------------------------------------------------- /resolvers.go: -------------------------------------------------------------------------------- 1 | package neocortex 2 | 3 | func (engine *Engine) ResolveAny(channel CommunicationChannel, handler HandleResolver) { 4 | if engine.generalResolver == nil { 5 | engine.generalResolver = map[CommunicationChannel]*HandleResolver{} 6 | } 7 | engine.generalResolver[channel] = &handler 8 | } 9 | 10 | func (engine *Engine) Resolve(channel CommunicationChannel, matcher *Matcher, handler HandleResolver) { 11 | if engine.registeredResolvers == nil { 12 | engine.registeredResolvers = map[CommunicationChannel]map[*Matcher]*HandleResolver{} 13 | } 14 | if engine.registeredResolvers[channel] == nil { 15 | engine.registeredResolvers[channel] = map[*Matcher]*HandleResolver{} 16 | } 17 | engine.registeredResolvers[channel][matcher] = &handler 18 | } 19 | 20 | func (engine *Engine) ResolveMany(channels []CommunicationChannel, matcher *Matcher, handler HandleResolver) { 21 | for _, ch := range channels { 22 | engine.Resolve(ch, matcher, handler) 23 | } 24 | } 25 | 26 | func (engine *Engine) ResolveManyAny(channels []CommunicationChannel, handler HandleResolver) { 27 | for _, ch := range channels { 28 | engine.ResolveAny(ch, handler) 29 | } 30 | } 31 | --------------------------------------------------------------------------------