watsbot 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,47 @@
1
+ require "httparty"
2
+ require "celluloid"
3
+
4
+ module Watsbot
5
+ class Message < BaseResource
6
+ include HTTParty
7
+
8
+ def send(uid, message, context={})
9
+ raise "uid should be provided" and return if uid.nil? or uid.empty?
10
+ raise "message should be provided" and return if message.nil? or message.empty?
11
+
12
+ future = Celluloid::Future.new { call(uid, message, context={}) }
13
+ future.value
14
+ end
15
+
16
+ private
17
+
18
+ def call(uid, message, context={})
19
+ state = State.instance
20
+ context = JSON.parse(state.fetch(uid)) rescue nil
21
+ response = call_api(message, context)
22
+ change_state(state, uid, response)
23
+ response
24
+ end
25
+
26
+ def change_state(state, uid, response)
27
+ if response.is_a? Response::Success
28
+ if response.context["system"]["branch_exited_reason"] == 'completed'
29
+ state.delete(uid)
30
+ elsif response.context["system"]["branch_exited_reason"] != 'fallback'
31
+ state.store(uid, response.context.to_json)
32
+ end
33
+ else
34
+ state.delete(uid)
35
+ end
36
+ end
37
+
38
+ def call_api(message, context={})
39
+ body = { input: { text: message }, context: context }
40
+ options = { basic_auth: basic_auth, headers: headers, body: body.to_json }
41
+ response = self.class.post("/workspaces/#{Watsbot.configuration.workspace}/message?version=#{Watsbot.configuration.version}", options)
42
+ logger.info("/log/watsbot.log", "watson api _> #{options.inspect} -- #{response}")
43
+ parser = Watsbot::Response::Parser.new response
44
+ parser.parse
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,5 @@
1
+ module Watsbot
2
+ module Response
3
+ Error = Struct.new(:code, :message) do; end
4
+ end
5
+ end
@@ -0,0 +1,38 @@
1
+ module Watsbot
2
+ module Response
3
+ class Parser
4
+
5
+ def initialize(response)
6
+ @response = response
7
+ @response_body = JSON.parse(response.body)
8
+ end
9
+
10
+ def parse
11
+ if @response.code >= 400
12
+ response = Error.new @response.code
13
+ response.message = parse_error_message
14
+ response
15
+ else
16
+ response = Success.new @response.code
17
+ response.intents = @response_body["intents"]
18
+ response.entities = parse_entities
19
+ response.input = @response_body["input"]
20
+ response.output = @response_body["output"]
21
+ response.context = @response_body["context"]
22
+ response
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def parse_error_message
29
+ @response_body["error"]["error"] rescue ""
30
+ end
31
+
32
+ def parse_entities
33
+ @response_body["entities"]
34
+ end
35
+
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,5 @@
1
+ module Watsbot
2
+ module Response
3
+ Success = Struct.new(:code, :intents, :entities, :input, :output, :context) do; end
4
+ end
5
+ end
@@ -0,0 +1,29 @@
1
+ require "redis"
2
+ require "singleton"
3
+
4
+ module Watsbot
5
+ class State
6
+ include Singleton
7
+
8
+ def initialize
9
+ @redis = Redis.new(:url => Watsbot.configuration.redis_url)
10
+ end
11
+
12
+ def fetch(key)
13
+ @redis.get(key)
14
+ end
15
+
16
+ def store(key, value)
17
+ @redis.set(key, value)
18
+ end
19
+
20
+ def exists?(key)
21
+ @redis.exists(key)
22
+ end
23
+
24
+ def delete(key)
25
+ @redis.del(key)
26
+ end
27
+
28
+ end
29
+ end
@@ -0,0 +1,3 @@
1
+ module Watsbot
2
+ VERSION = "1.0.0"
3
+ end
@@ -0,0 +1,149 @@
1
+ require "spec_helper"
2
+
3
+ module Watsbot
4
+ RSpec.describe Configuration do
5
+
6
+ after :each do
7
+ Watsbot.reset
8
+ end
9
+
10
+ context "username" do
11
+ it "should return default username from environment variable" do
12
+ allow(ENV).to receive(:[]).and_return(nil)
13
+ allow(ENV).to receive(:[]).with("WATSON_USERNAME").and_return("test")
14
+ Configuration.new.username
15
+ expect(Configuration.new.username).to eq("test")
16
+ end
17
+ it "should set username" do
18
+ config = Configuration.new
19
+ config.username = "test"
20
+ expect(config.username).to eq("test")
21
+ end
22
+ it "should set username with argument" do
23
+ config = Configuration.new username: "test"
24
+ expect(config.username).to eq("test")
25
+ end
26
+ it "should set username with block" do
27
+ config = Configuration.new do |c|
28
+ c.username = "username"
29
+ end
30
+ expect(config.username).to eq("username")
31
+ end
32
+ end
33
+
34
+ context "password" do
35
+ it "should return default password from environment variable" do
36
+ allow(ENV).to receive(:[]).and_return(nil)
37
+ allow(ENV).to receive(:[]).with("WATSON_PASSWORD").and_return("test")
38
+ Configuration.new.password
39
+ expect(Configuration.new.password).to eq("test")
40
+ end
41
+ it "should set password" do
42
+ config = Configuration.new
43
+ config.password = "test"
44
+ expect(config.password).to eq("test")
45
+ end
46
+ it "should set password with argument" do
47
+ config = Configuration.new password: "test"
48
+ expect(config.password).to eq("test")
49
+ end
50
+ it "should set password with block" do
51
+ config = Configuration.new do |c|
52
+ c.password = "password"
53
+ end
54
+ expect(config.password).to eq("password")
55
+ end
56
+ end
57
+
58
+ context "workspace" do
59
+ it "should return default workspace from environment variable" do
60
+ allow(ENV).to receive(:[]).and_return(nil)
61
+ allow(ENV).to receive(:[]).with("WATSON_WORKSPACE").and_return("test")
62
+ Configuration.new.workspace
63
+ expect(Configuration.new.workspace).to eq("test")
64
+ end
65
+ it "should set workspace" do
66
+ config = Configuration.new
67
+ config.workspace = "test"
68
+ expect(config.workspace).to eq("test")
69
+ end
70
+ it "should set workspace with argument" do
71
+ config = Configuration.new workspace: "test"
72
+ expect(config.workspace).to eq("test")
73
+ end
74
+ it "should set workspace with block" do
75
+ config = Configuration.new do |c|
76
+ c.workspace = "workspace"
77
+ end
78
+ expect(config.workspace).to eq("workspace")
79
+ end
80
+ end
81
+
82
+ context "version" do
83
+ it "should return default version from environment variable" do
84
+ allow(ENV).to receive(:[]).and_return(nil)
85
+ allow(ENV).to receive(:[]).with("WATSON_WORKSPACE_VERSION").and_return("test")
86
+ Configuration.new.version
87
+ expect(Configuration.new.version).to eq("test")
88
+ end
89
+ it "should set version" do
90
+ config = Configuration.new
91
+ config.version = "test"
92
+ expect(config.version).to eq("test")
93
+ end
94
+ it "should set version with argument" do
95
+ config = Configuration.new version: "test"
96
+ expect(config.version).to eq("test")
97
+ end
98
+ it "should set version with block" do
99
+ config = Configuration.new do |c|
100
+ c.version = "version"
101
+ end
102
+ expect(config.version).to eq("version")
103
+ end
104
+ end
105
+
106
+ context "redis_url" do
107
+ it "should return default redis_url from environment variable" do
108
+ allow(ENV).to receive(:[]).and_return(nil)
109
+ allow(ENV).to receive(:[]).with("REDIS_URL").and_return("test")
110
+ Configuration.new.redis_url
111
+ expect(Configuration.new.redis_url).to eq("test")
112
+ end
113
+ it "should set redis_url" do
114
+ config = Configuration.new
115
+ config.redis_url = "test"
116
+ expect(config.redis_url).to eq("test")
117
+ end
118
+ it "should set redis_url with argument" do
119
+ config = Configuration.new redis_url: "test"
120
+ expect(config.redis_url).to eq("test")
121
+ end
122
+ it "should set redis_url with block" do
123
+ config = Configuration.new do |c|
124
+ c.redis_url = "redis_url"
125
+ end
126
+ expect(config.redis_url).to eq("redis_url")
127
+ end
128
+ end
129
+
130
+ context "base uri" do
131
+ it "should return default base uri" do
132
+ expect(Configuration.new.base_uri.nil?).to be false
133
+ expect(Configuration.new.base_uri.length).to be > 0
134
+ end
135
+ it "should set base uri" do
136
+ config = Configuration.new
137
+ config.base_uri = "http://mycustom.url"
138
+ expect(config.base_uri).to eq("http://mycustom.url")
139
+ end
140
+ it "should set base uri with block" do
141
+ config = Configuration.new do |c|
142
+ c.base_uri = "http://mycustomapi.url"
143
+ end
144
+ expect(config.base_uri).to eq("http://mycustomapi.url")
145
+ end
146
+ end
147
+
148
+ end
149
+ end
@@ -0,0 +1,21 @@
1
+ HTTP/1.1 200 OK
2
+ X-Backside-Transport: OK OK
3
+ Connection: Keep-Alive
4
+ Transfer-Encoding: chunked
5
+ X-Frame-Options: SAMEORIGIN
6
+ Strict-Transport-Security: max-age=31536000;
7
+ ETag: W/"1cd-XPBepDE7HNqNr6wZapZ1+w"
8
+ X-Content-Type-Options: nosniff
9
+ X-Download-Options: noopen
10
+ X-DNS-Prefetch-Control: off
11
+ Vary: Accept-Encoding
12
+ X-XSS-Protection: 1; mode=block
13
+ Date: Tue, 23 May 2017 10:26:14 GMT
14
+ Content-Type: application/json;charset=utf-8
15
+ Server: -
16
+ Set-Cookie: Watson-DPAT=1IUyfHxFvm9UhVYgNXBRScD8lDxY2xWsK3xSpNYmKq5YY0exS%2Fqtz55Sxg9Ys6%2F7v18ofQ9%2BhmrP68PBAMHjkJ4jGENioEoqtqWnU4ts4s0D9%2FTj9dniC7UxKdUsw9eUF32Usd4UVuvSb%2BWSEETs2HoL4GTHZxxWa0jeUXgI2CDSsvPpiSypRfvQz8Jqb9naK1TA2qyWOlNxLRvmFlmhdlBGrIXsp9QhgPeEgikZK9d4MZrdZXw6ht2T%2FJo93BsGgzrUf0ForfXw2pjh6MuAIFNTFzzeTkhqTyCF1GOxFmbDCObOgLTO2tuWTpn7pAiLNUL2vsogOWlf0GEsZSh012hpxPsNj0OCigne%2FxyY5Dj9o%2FY0HXvSa17A71qlIrFoYs0kyC5YCuiWOOOMfP56Jf6Ztnu39SVEKGxHRlSiT2udAJMS4t5jm0kF%2BkP%2BhBOHSOgMJGil5x25ArJtM0YfrunF81mskUM77aYUtHE%2BosCYFIgylZ3mH%2BC6mo3eG2qe76Gj1Uu5LQSSPX1Hm5mcnEsiSISyHfSveIKlb7OaYM9kjRHHsbLjnHfy2CZv7nSBKIFQMCiQk4FrRv3meSqCGz%2BmFeKCo04Cs75%2F6ySShVDA1yhJW8LhPaHhnRtXwm0zbFT4hD4qBTbFmW%2FxXdPJcsE%2B40ltYh%2FQ5H6yIAAYykujtkHCigkTrjiIiyPmAgB9zSx7sl934Lf1XVeSsDNJS83jyqOCG804BCZN8bfhRY4%2BxvjR5zD9Rm%2Fy6Rl9n%2BNOIV9F7aV45YTvHRJ%2FEHPDb5AIl9dwqsW0nfaz43KeZ7K9F2cqgHOP7fJCmUE9Q2e%2FkoUikETdW5q5JxuxntQ6BALK%2BtfC%2FbLIJORiDKm4%2FhKtKlBPoRsdANOkpdAe7%2FjugaJWkmPQ7ayYTOfBlHCyZoYUtbSL9AJFFgaq9utrNqe5Fr0VGnQ4TV%2Fn%2BmxcXYlQSTcDGS%2F8mpm0FLDXtqxuIg%3D%3D; path=/conversation/api; secure; HttpOnly
17
+ X-Client-IP: 220.101.98.130
18
+ X-Global-Transaction-ID: 1468324772
19
+ X-DP-Watson-Tran-ID: gateway-dp02-1468324772
20
+
21
+ {"intents":[{"intent":"Greeting","confidence":1}],"entities":[{"entity":"Greeting","location":[0,2],"value":"Hi","confidence":1}],"input":{"text":"hi"},"output":{"log_messages":[],"text":["Hi, how are you?"],"nodes_visited":["greeting"]},"context":{"conversation_id":"d17270f6-671e-473b-b981-65761a9d5128","system":{"dialog_stack":[{"dialog_node":"greeting"}],"dialog_turn_counter":1,"dialog_request_counter":1,"_node_output_map":{"node_2_1494386292425":[0]}}}}
@@ -0,0 +1,21 @@
1
+ HTTP/1.1 400 Bad Request
2
+ X-Backside-Transport: FAIL FAIL
3
+ Connection: Keep-Alive
4
+ Transfer-Encoding: chunked
5
+ X-Frame-Options: SAMEORIGIN
6
+ Strict-Transport-Security: max-age=31536000;
7
+ ETag: W/"37-GiTifP4vb9PqzZCy57Cz7A"
8
+ X-Content-Type-Options: nosniff
9
+ X-Download-Options: noopen
10
+ X-DNS-Prefetch-Control: off
11
+ Vary: Accept-Encoding
12
+ X-XSS-Protection: 1; mode=block
13
+ Date: Tue, 23 May 2017 10:59:42 GMT
14
+ Content-Type: application/json;charset=utf-8
15
+ Server: -
16
+ Set-Cookie: Watson-DPAT=AqqzbJRf25aUFnKhzKDjg%2Bmqhdx%2BKMTTg8I9RMzub6jsDkn6kC2ObdYFLwjUBhXglnrH47KPEBaC7dF5G62Gsh9kklkjKX4GLW5j%2BztLAh3dr4tv6jt9qN97W4URreaF99zP0fLHH%2FDvyYxTAw0VSvGpNc5%2B0kC%2FOukrM7EUDXdsgN%2FQdglfeTficysYFhaq%2FXLs6rphgcD5m7x99klh1Bh4aFjaSueecLB%2FEV8WUhb281%2FZIRFKgVE%2F2IB3V8PV3D1oXtTIjJHeumVRc7JGIHcXy%2Ba0DI%2BFAr8zI0mSKz7LqB%2Fq8OWikXssLZW3Noojlq9FX1hNYIMxOup583aeptDJhpSe%2BIV2rCrqAYafVy8s2%2BcLovB3WdHPFmE35cAmm%2BokC1PU4CMXoslsDk5sN97CfwVXaFJdKg1cviKFcw6hwX0aHTauNof8kbXC2aGStG%2F0e8nkXFs2bFAhANJ8u88x57i5ciS8fnry5ZwZZiFxs4qTbOKADuoV3r96ClrwAtodeLdMYJCClMFENb4IMqnMmhgOo8sjHCoDqJH8LHJEVCxxSlgJrb4wcCtVAEJDeVxejl2v5dokRakBWvdG%2BwOvVcgR41LzQ6n8Xj%2BtQ2HfUsfzaHT33KsoXHYFHTcC8elzq5R0z8G6WrGZMAaAtxwwsaGGaKbVKyruGRP0AyYI2QwW9%2FWYUzzXHoTt6r9cQBbru72gyIVDQwGanMSLt6h3QDwXDsTb4I5JVB0E9T3sR1fUt1SEivUBZqipCvgctp%2BbVAKbnZNr4OjDOxL0VbS2WiCvr6p3RGi39KXfH500xUpWJXtcPNi8xLtNjKEVnoancJYZmC20p6WYqnEJAHva4BgkxrDZOsAsxS4XvVSzcyYtxOnBceCsOvMVtt2xYhF68OrShMp9VAJkd75qYoJTHGJQ5BW%2FysMY4djh5xdrtsI4EvvwzvHHLQezcOp2P0d57h%2BqhUY%3D; path=/conversation/api; secure; HttpOnly
17
+ X-Client-IP: 220.101.98.130
18
+ X-Global-Transaction-ID: 1437918292
19
+ X-DP-Watson-Tran-ID: gateway-dp01-1437918292
20
+
21
+ {"error":{"error":"Missing 'version' query parameter"}}
@@ -0,0 +1,20 @@
1
+
2
+ def spec_dir
3
+ File.join Watsbot.root, "spec"
4
+ end
5
+
6
+ def fixture_dir
7
+ File.join spec_dir, "fixtures"
8
+ end
9
+
10
+ def watson_uri(path)
11
+ Watsbot::BASE_URI + path
12
+ end
13
+
14
+ def watson_ruri(path)
15
+ %r{#{watson_uri(path)}}
16
+ end
17
+
18
+ def fixture_path(file)
19
+ File.new File.join(fixture_dir, file)
20
+ end
@@ -0,0 +1,65 @@
1
+ require "spec_helper"
2
+
3
+ module Watsbot
4
+ RSpec.describe Message do
5
+
6
+ let(:valid_config) {
7
+ Watsbot::Configuration.new(
8
+ username: ENV["WATSON_USERNAME"],
9
+ password: ENV["WATSON_PASSWORD"],
10
+ workspace: ENV["WATSON_WORKSPACE"],
11
+ version: ENV["WATSON_WORKSPACE_VERSION"],
12
+ redis_url: ENV["REDIS_URL"]
13
+ )
14
+ }
15
+
16
+ describe "#send" do
17
+ context "invalid" do
18
+ it "raises error - no uid" do
19
+ message = Watsbot::Message.new(valid_config)
20
+ expect { message.send(nil, "Hi", {}) }.to raise_error("uid should be provided")
21
+ end
22
+ it "raises error - no message" do
23
+ message = Watsbot::Message.new(valid_config)
24
+ uid = SecureRandom.uuid
25
+ expect { message.send(uid, nil, {}) }.to raise_error("message should be provided")
26
+ end
27
+ it "raises error - blank uid" do
28
+ message = Watsbot::Message.new(valid_config)
29
+ expect { message.send('', "Hi", {}) }.to raise_error("uid should be provided")
30
+ end
31
+ it "raises error - blank message" do
32
+ message = Watsbot::Message.new(valid_config)
33
+ uid = SecureRandom.uuid
34
+ expect { message.send(uid, '', {}) }.to raise_error("message should be provided")
35
+ end
36
+ it "responds with error state - no version" do
37
+ stub_request(:post, watson_uri("/workspaces/#{Watsbot.configuration.workspace}/message?version=")).to_return(fixture_path("message/error.txt"))
38
+ message = Watsbot::Message.new(valid_config)
39
+ uid = SecureRandom.uuid
40
+ Watsbot.configuration.version = nil
41
+ response = message.send(uid, "Hi", {})
42
+ expect(response.class).to eq(Response::Error)
43
+ Watsbot.configuration.version = ENV['WATSON_WORKSPACE_VERSION']
44
+ end
45
+ end
46
+ context "valid" do
47
+ it "responds with success state" do
48
+ stub_request(:post, watson_uri("/workspaces/#{Watsbot.configuration.workspace}/message?version=#{Watsbot.configuration.version}")).to_return(fixture_path("message/created.txt"))
49
+ message = Watsbot::Message.new(valid_config)
50
+ uid = SecureRandom.uuid
51
+ response = message.send(uid, "Hi", {})
52
+ expect(response.class).to eq(Response::Success)
53
+ end
54
+ it "stores the context" do
55
+ stub_request(:post, watson_uri("/workspaces/#{Watsbot.configuration.workspace}/message?version=#{Watsbot.configuration.version}")).to_return(fixture_path("message/created.txt"))
56
+ message = Watsbot::Message.new(valid_config)
57
+ uid = SecureRandom.uuid
58
+ message.send(uid, "Hi", {})
59
+ expect(State.instance.fetch(uid)).not_to eq(nil)
60
+ end
61
+ end
62
+ end
63
+
64
+ end
65
+ end
@@ -0,0 +1,21 @@
1
+ require "bundler/setup"
2
+ require "watsbot"
3
+ require "pry"
4
+ require "simplecov"
5
+ require "webmock/rspec"
6
+ require "byebug"
7
+ require "helpers"
8
+ require "securerandom"
9
+ require "dotenv"
10
+
11
+ Dotenv.load
12
+ WebMock.enable!
13
+ SimpleCov.start
14
+
15
+ RSpec.configure do |config|
16
+ config.example_status_persistence_file_path = ".rspec_status"
17
+ config.disable_monkey_patching!
18
+ config.expect_with :rspec do |c|
19
+ c.syntax = :expect
20
+ end
21
+ end
@@ -0,0 +1,67 @@
1
+ require "spec_helper"
2
+
3
+ module Watsbot
4
+ RSpec.describe State do
5
+
6
+ describe "#fetch" do
7
+ it "returns nil" do
8
+ state = State.instance
9
+ uid = SecureRandom.uuid
10
+ expect(state.fetch(uid)).to be(nil)
11
+ end
12
+ it "returns data" do
13
+ state = State.instance
14
+ uid = SecureRandom.uuid
15
+ data = { data: {} }
16
+ state.store(uid, data.to_json)
17
+ expect(state.fetch(uid)).to eq(data.to_json)
18
+ end
19
+ end
20
+
21
+ describe "#store" do
22
+ it "returns data" do
23
+ state = State.instance
24
+ uid = SecureRandom.uuid
25
+ expect(state.fetch(uid)).to eq(nil)
26
+ end
27
+ it "returns data" do
28
+ state = State.instance
29
+ uid = SecureRandom.uuid
30
+ data = { data: {} }
31
+ state.store(uid, data.to_json)
32
+ expect(state.fetch(uid)).to eq(data.to_json)
33
+ end
34
+ end
35
+
36
+ describe "#exists?" do
37
+ it "returns data" do
38
+ state = State.instance
39
+ uid = SecureRandom.uuid
40
+ expect(state.exists?(uid)).to eq(false)
41
+ end
42
+ it "returns data" do
43
+ state = State.instance
44
+ uid = SecureRandom.uuid
45
+ data = { data: {} }
46
+ state.store(uid, data.to_json)
47
+ expect(state.exists?(uid)).to eq(true)
48
+ end
49
+ end
50
+
51
+ describe "#delete" do
52
+ it "returns data" do
53
+ state = State.instance
54
+ uid = SecureRandom.uuid
55
+ expect(state.delete(uid)).to eq(0)
56
+ end
57
+ it "returns data" do
58
+ state = State.instance
59
+ uid = SecureRandom.uuid
60
+ data = { data: {} }
61
+ state.store(uid, data.to_json)
62
+ expect(state.delete(uid)).to eq(1)
63
+ end
64
+ end
65
+
66
+ end
67
+ end