watsbot 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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