ibm_watson 0.1.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.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +258 -0
  3. data/bin/console +14 -0
  4. data/bin/setup +8 -0
  5. data/lib/ibm_watson.rb +16 -0
  6. data/lib/ibm_watson/assistant_v1.rb +1997 -0
  7. data/lib/ibm_watson/detailed_response.rb +21 -0
  8. data/lib/ibm_watson/discovery_v1.rb +2039 -0
  9. data/lib/ibm_watson/iam_token_manager.rb +166 -0
  10. data/lib/ibm_watson/language_translator_v3.rb +411 -0
  11. data/lib/ibm_watson/natural_language_classifier_v1.rb +309 -0
  12. data/lib/ibm_watson/natural_language_understanding_v1.rb +297 -0
  13. data/lib/ibm_watson/personality_insights_v3.rb +260 -0
  14. data/lib/ibm_watson/speech_to_text_v1.rb +2153 -0
  15. data/lib/ibm_watson/text_to_speech_v1.rb +716 -0
  16. data/lib/ibm_watson/tone_analyzer_v3.rb +287 -0
  17. data/lib/ibm_watson/version.rb +3 -0
  18. data/lib/ibm_watson/visual_recognition_v3.rb +579 -0
  19. data/lib/ibm_watson/watson_api_exception.rb +41 -0
  20. data/lib/ibm_watson/watson_service.rb +180 -0
  21. data/lib/ibm_watson/websocket/recognize_callback.rb +32 -0
  22. data/lib/ibm_watson/websocket/speech_to_text_websocket_listener.rb +162 -0
  23. data/rakefile +45 -0
  24. data/test/integration/test_assistant_v1.rb +645 -0
  25. data/test/integration/test_discovery_v1.rb +200 -0
  26. data/test/integration/test_iam_assistant_v1.rb +707 -0
  27. data/test/integration/test_language_translator_v3.rb +81 -0
  28. data/test/integration/test_natural_language_classifier_v1.rb +69 -0
  29. data/test/integration/test_natural_language_understanding_v1.rb +98 -0
  30. data/test/integration/test_personality_insights_v3.rb +95 -0
  31. data/test/integration/test_speech_to_text_v1.rb +187 -0
  32. data/test/integration/test_text_to_speech_v1.rb +81 -0
  33. data/test/integration/test_tone_analyzer_v3.rb +72 -0
  34. data/test/integration/test_visual_recognition_v3.rb +64 -0
  35. data/test/test_helper.rb +22 -0
  36. data/test/unit/test_assistant_v1.rb +1598 -0
  37. data/test/unit/test_discovery_v1.rb +1144 -0
  38. data/test/unit/test_iam_token_manager.rb +165 -0
  39. data/test/unit/test_language_translator_v3.rb +461 -0
  40. data/test/unit/test_natural_language_classifier_v1.rb +187 -0
  41. data/test/unit/test_natural_language_understanding_v1.rb +132 -0
  42. data/test/unit/test_personality_insights_v3.rb +172 -0
  43. data/test/unit/test_speech_to_text_v1.rb +755 -0
  44. data/test/unit/test_text_to_speech_v1.rb +336 -0
  45. data/test/unit/test_tone_analyzer_v3.rb +200 -0
  46. data/test/unit/test_vcap_using_personality_insights.rb +150 -0
  47. data/test/unit/test_visual_recognition_v3.rb +345 -0
  48. metadata +302 -0
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require("json")
4
+ # Custom exception class for errors returned from Watson APIs
5
+ class WatsonApiException < StandardError
6
+ attr_reader :code, :error, :info, :transaction_id, :global_transaction_id
7
+ # :param HTTP::Response response: The response object from the Watson API
8
+ def initialize(code: nil, error: nil, info: nil, transaction_id: nil, global_transaction_id: nil, response: nil)
9
+ if code.nil? || error.nil?
10
+ @code = response.code
11
+ @error = response.reason
12
+ unless response.body.empty?
13
+ body_hash = JSON.parse(response.body.to_s)
14
+ @code = body_hash["code"] || body_hash["error_code"]
15
+ @error = body_hash["error"] || body_hash["error_message"]
16
+ %w[code error_code error error_message].each { |k| body_hash.delete(k) }
17
+ @info = body_hash
18
+ end
19
+ @transaction_id = transaction_id
20
+ @global_transaction_id = global_transaction_id
21
+ @transaction_id = response.headers["X-DP-Watson-Tran-ID"] if response.headers.include?("X-DP-Watson-Tran-ID")
22
+ @global_transaction_id = response.headers["X-Global-Transaction-ID"] if response.headers.include?("X-Global-Transaction-ID")
23
+ else
24
+ # :nocov:
25
+ @code = code
26
+ @error = error
27
+ @info = info
28
+ @transaction_id = transaction_id
29
+ @global_transaction_id = global_transaction_id
30
+ # :nocov:
31
+ end
32
+ end
33
+
34
+ def to_s
35
+ msg = "Error: #{@error}, Code: #{@code}"
36
+ msg += ", Information: #{@info}" unless @info.nil?
37
+ msg += ", X-dp-watson-tran-id: #{@transaction_id}" unless @transaction_id.nil?
38
+ msg += ", X-global-transaction-id: #{@global_transaction_id}" unless @global_transaction_id.nil?
39
+ msg
40
+ end
41
+ end
@@ -0,0 +1,180 @@
1
+ # frozen_string_literal: true
2
+
3
+ require("http")
4
+ require("rbconfig")
5
+ require("stringio")
6
+ require("json")
7
+ require_relative("./detailed_response.rb")
8
+ require_relative("./watson_api_exception.rb")
9
+ require_relative("./iam_token_manager.rb")
10
+ require_relative("./version.rb")
11
+ # require("httplog")
12
+ # HttpLog.configure do |config|
13
+ # config.log_connect = true
14
+ # config.log_request = true
15
+ # config.log_headers = true
16
+ # config.log_data = true
17
+ # config.log_status = true
18
+ # config.log_response = true
19
+ # end
20
+
21
+ # Class for interacting with the Watson API
22
+ class WatsonService
23
+ attr_accessor :password, :url, :username
24
+ attr_reader :conn
25
+ def initialize(vars)
26
+ defaults = {
27
+ vcap_services_name: nil,
28
+ username: nil,
29
+ password: nil,
30
+ use_vcap_services: true,
31
+ api_key: nil,
32
+ x_watson_learning_opt_out: false,
33
+ iam_api_key: nil,
34
+ iam_access_token: nil,
35
+ iam_url: nil
36
+ }
37
+ vars = defaults.merge(vars)
38
+ @url = vars[:url]
39
+ @username = nil
40
+ @password = nil
41
+ @token_manager = nil
42
+ @temp_headers = nil
43
+
44
+ user_agent_string = "watson-apis-ruby-sdk-" + IBMWatson::VERSION
45
+ user_agent_string += " #{RbConfig::CONFIG["host"]}"
46
+ user_agent_string += " #{RbConfig::CONFIG["RUBY_BASE_NAME"]}-#{RbConfig::CONFIG["RUBY_PROGRAM_VERSION"]}"
47
+
48
+ headers = {
49
+ "User-Agent" => user_agent_string
50
+ }
51
+ headers["x-watson-learning-opt-out"] = true if vars[:x_watson_learning_opt_out]
52
+ if vars[:use_vcap_services]
53
+ @vcap_service_credentials = load_from_vcap_services(service_name: vars[:vcap_services_name])
54
+ if !@vcap_service_credentials.nil? && @vcap_service_credentials.instance_of?(Hash)
55
+ @url = @vcap_service_credentials["url"]
56
+ @username = @vcap_service_credentials["username"] if @vcap_service_credentials.key?("username")
57
+ @password = @vcap_service_credentials["password"] if @vcap_service_credentials.key?("password")
58
+ @iam_api_key = @vcap_service_credentials["iam_api_key"] if @vcap_service_credentials.key?("iam_api_key")
59
+ @iam_access_token = @vcap_service_credentials["iam_access_token"] if @vcap_service_credentials.key?("iam_access_token")
60
+ @iam_url = @vcap_service_credentials["iam_url"] if @vcap_service_credentials.key?("iam_url")
61
+ end
62
+ end
63
+
64
+ if !vars[:iam_access_token].nil? || !vars[:iam_api_key].nil?
65
+ _token_manager(iam_api_key: vars[:iam_api_key], iam_access_token: vars[:iam_access_token], iam_url: vars[:iam_url])
66
+ elsif !vars[:username].nil? && !vars[:password].nil?
67
+ if vars[:username] == "apikey"
68
+ _iam_api_key(iam_api_key: vars[:password])
69
+ else
70
+ @username = vars[:username]
71
+ @password = vars[:password]
72
+ end
73
+ end
74
+
75
+ @conn = HTTP::Client.new(
76
+ headers: headers
77
+ ).timeout(
78
+ :per_operation,
79
+ read: 60,
80
+ write: 60,
81
+ connect: 60
82
+ )
83
+ end
84
+
85
+ def load_from_vcap_services(service_name:)
86
+ vcap_services = ENV["VCAP_SERVICES"]
87
+ unless vcap_services.nil?
88
+ services = JSON.parse(vcap_services)
89
+ return services[service_name][0]["credentials"] if services.key?(service_name)
90
+ end
91
+ nil
92
+ end
93
+
94
+ def add_default_headers(headers: {})
95
+ raise TypeError unless headers.instance_of?(Hash)
96
+ headers.each_pair { |k, v| @conn.default_options.headers.add(k, v) }
97
+ end
98
+
99
+ def _token_manager(iam_api_key: nil, iam_access_token: nil, iam_url: nil)
100
+ @iam_api_key = iam_api_key
101
+ @iam_access_token = iam_access_token
102
+ @iam_url = iam_url
103
+ @token_manager = IAMTokenManager.new(iam_api_key: iam_api_key, iam_access_token: iam_access_token, iam_url: iam_url)
104
+ end
105
+
106
+ def _iam_access_token(iam_access_token:)
107
+ @token_manager._access_token(iam_access_token: iam_access_token) unless @token_manager.nil?
108
+ @token_manager = IAMTokenManager.new(iam_access_token: iam_access_token) if @token_manager.nil?
109
+ @iam_access_token = iam_access_token
110
+ end
111
+
112
+ def _iam_api_key(iam_api_key:)
113
+ @token_manager._iam_api_key(iam_api_key: iam_api_key) unless @token_manager.nil?
114
+ @token_manager = IAMTokenManager.new(iam_api_key: iam_api_key) if @token_manager.nil?
115
+ @iam_api_key = iam_api_key
116
+ end
117
+
118
+ # @return [DetailedResponse]
119
+ def request(args)
120
+ defaults = { method: nil, url: nil, accept_json: false, headers: nil, params: nil, json: {}, data: nil }
121
+ args = defaults.merge(args)
122
+ args[:data].delete_if { |_k, v| v.nil? } if args[:data].instance_of?(Hash)
123
+ args[:json] = args[:data].merge(args[:json]) if args[:data].respond_to?(:merge)
124
+ args[:json] = args[:data] if args[:json].empty? || (args[:data].instance_of?(String) && !args[:data].empty?)
125
+ args[:json].delete_if { |_k, v| v.nil? } if args[:json].instance_of?(Hash)
126
+ args[:headers]["Accept"] = "application/json" if args[:accept_json]
127
+ args[:headers]["Content-Type"] = "application/json" unless args[:headers].key?("Content-Type")
128
+ args[:json] = args[:json].to_json if args[:json].instance_of?(Hash)
129
+ args[:headers].delete_if { |_k, v| v.nil? } if args[:headers].instance_of?(Hash)
130
+ args[:params].delete_if { |_k, v| v.nil? } if args[:params].instance_of?(Hash)
131
+ args[:form].delete_if { |_k, v| v.nil? } if args.key?(:form)
132
+ args.delete_if { |_, v| v.nil? }
133
+ args[:headers].delete("Content-Type") if args.key?(:form) || args[:json].nil?
134
+
135
+ if @username == "apikey"
136
+ _iam_api_key(iam_api_key: @password)
137
+ @username = nil
138
+ end
139
+
140
+ conn = @conn
141
+ if !@token_manager.nil?
142
+ access_token = @token_manager._token
143
+ args[:headers]["Authorization"] = "Bearer #{access_token}"
144
+ elsif !@username.nil? && !@password.nil?
145
+ conn = @conn.basic_auth(user: @username, pass: @password)
146
+ end
147
+
148
+ args[:headers] = args[:headers].merge(@temp_headers) unless @temp_headers.nil?
149
+ @temp_headers = nil unless @temp_headers.nil?
150
+
151
+ if args.key?(:form)
152
+ response = conn.follow.request(
153
+ args[:method],
154
+ HTTP::URI.parse(@url + args[:url]),
155
+ headers: conn.default_options.headers.merge(HTTP::Headers.coerce(args[:headers])),
156
+ params: args[:params],
157
+ form: args[:form]
158
+ )
159
+ else
160
+ response = conn.follow.request(
161
+ args[:method],
162
+ HTTP::URI.parse(@url + args[:url]),
163
+ headers: conn.default_options.headers.merge(HTTP::Headers.coerce(args[:headers])),
164
+ body: args[:json],
165
+ params: args[:params]
166
+ )
167
+ end
168
+ return DetailedResponse.new(response: response) if (200..299).cover?(response.code)
169
+ raise WatsonApiException.new(response: response)
170
+ end
171
+
172
+ # @note Chainable
173
+ # @param headers [Hash] Custom headers to be sent with the request
174
+ # @return [self]
175
+ def headers(headers)
176
+ raise TypeError("Expected Hash type, received #{headers.class}") unless headers.instance_of?(Hash)
177
+ @temp_headers = headers
178
+ self
179
+ end
180
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IBMWatson
4
+ # Abstract class for Recognize Callbacks
5
+ class RecognizeCallback
6
+ def initialize(*); end
7
+
8
+ # Called when an interim result is received
9
+ def on_transcription(transcript:); end
10
+
11
+ # Called when a WebSocket connection is made
12
+ def on_connected; end
13
+
14
+ # Called when there is an error in the WebSocket connection
15
+ def on_error(error:); end
16
+
17
+ # Called when there is an inactivity timeout
18
+ def on_inactivity_timeout(error:); end
19
+
20
+ # Called when the service is listening for audio
21
+ def on_listening; end
22
+
23
+ # Called after the service returns the final result for the transcription
24
+ def on_transcription_complete; end
25
+
26
+ # Called when the service returns the final hypothesis
27
+ def on_hypothesis(hypothesis:); end
28
+
29
+ # Called when the service returns results. The data is returned unparsed
30
+ def on_data(data:); end
31
+ end
32
+ end
@@ -0,0 +1,162 @@
1
+ # frozen_string_literal: true
2
+
3
+ require("eventmachine")
4
+ require("faye/websocket")
5
+ require("json")
6
+
7
+ ONE_KB = 1024
8
+ TIMEOUT_PREFIX = "No speech detected for".freeze
9
+ CLOSE_SIGNAL = 1000
10
+ TEN_MILLISECONDS = 0.01
11
+
12
+ # Class for interacting with the WebSocket API
13
+ class WebSocketClient
14
+ def initialize(audio: nil, chunk_data:, options:, recognize_callback:, url:, headers:)
15
+ @audio = audio
16
+ @options = options
17
+ @callback = recognize_callback
18
+ @bytes_sent = 0
19
+ @headers = headers
20
+ @is_listening = false
21
+ @url = url
22
+ @timer = nil
23
+ @chunk_data = chunk_data
24
+ @mic_running = false
25
+ @data_size = audio.nil? ? 0 : @audio.size
26
+ @queue = Queue.new
27
+ end
28
+
29
+ def start
30
+ on_open = lambda do |event|
31
+ on_connect(event)
32
+ @client.send(build_start_message(options: @options))
33
+ @mic_running = true if @chunk_data
34
+ send_audio(data: @audio)
35
+ end
36
+
37
+ on_message = lambda do |event|
38
+ json_object = JSON.parse(event.data)
39
+ if json_object.key?("error")
40
+ error = json_object["error"]
41
+ if error.start_with?(TIMEOUT_PREFIX)
42
+ @callback.on_inactivity_timeout(error: error)
43
+ else
44
+ @callback.on_error(error: error)
45
+ end
46
+ elsif json_object.key?("state")
47
+ if !@is_listening
48
+ @is_listening = true
49
+ else
50
+ @client.send(build_close_message)
51
+ @callback.on_transcription_complete
52
+ @client.close(CLOSE_SIGNAL)
53
+ end
54
+ elsif json_object.key?("results") || json_object.key?("speaker_labels")
55
+ hypothesis = ""
56
+ unless json_object["results"].nil?
57
+ hypothesis = json_object.dig("results", 0, "alternatives", 0, "transcript")
58
+ b_final = json_object.dig("results", 0, "final")
59
+ transcripts = extract_transcripts(alternatives: json_object.dig("results", 0, "alternatives"))
60
+
61
+ @callback.on_hypothesis(hypothesis: hypothesis) if b_final
62
+
63
+ @callback.on_transcription(transcript: transcripts)
64
+ @callback.on_data(data: json_object)
65
+ end
66
+ end
67
+ end
68
+
69
+ on_close = lambda do |_event|
70
+ @client = nil
71
+ EM.stop_event_loop
72
+ end
73
+
74
+ on_error = lambda do |event|
75
+ p event.message
76
+ end
77
+
78
+ EM.reactor_thread.join unless EM.reactor_thread.nil?
79
+ EM.run do
80
+ @client = Faye::WebSocket::Client.new(@url, nil, headers: @headers)
81
+ @client.onclose = on_close
82
+ @client.onerror = on_error
83
+ @client.onmessage = on_message
84
+ @client.onopen = on_open
85
+ @client.add_listener(Faye::WebSocket::API::Event.create("open"))
86
+ @client.add_listener(Faye::WebSocket::API::Event.create("message"))
87
+ @client.add_listener(Faye::WebSocket::API::Event.create("close"))
88
+ @client.add_listener(Faye::WebSocket::API::Event.create("error"))
89
+ end
90
+ end
91
+
92
+ def add_audio_chunk(chunk:)
93
+ @data_size += chunk.size
94
+ @queue << chunk
95
+ end
96
+
97
+ def stop_audio
98
+ @mic_running = false
99
+ end
100
+
101
+ private
102
+
103
+ def on_connect(_response)
104
+ @callback.on_connected
105
+ end
106
+
107
+ def build_start_message(options:)
108
+ options["action"] = "start"
109
+ options.to_json
110
+ end
111
+
112
+ def build_close_message
113
+ { "action" => "close" }.to_json
114
+ end
115
+
116
+ def send_audio(data:)
117
+ if @chunk_data
118
+ if @mic_running
119
+ @queue.empty? ? send_chunk(chunk: nil, final: false) : send_chunk(chunk: @queue.pop(true), final: false)
120
+ elsif @queue.length == 1
121
+ send_chunk(chunk: @queue.pop(true), final: true)
122
+ @queue.close
123
+ @timer.cancel if @timer.respond_to?(:cancel)
124
+ return
125
+ else
126
+ send_chunk(chunk: @queue.pop(true), final: false) unless @queue.empty?
127
+ end
128
+ else
129
+ if @bytes_sent + ONE_KB >= @data_size
130
+ if @data_size > @bytes_sent
131
+ send_chunk(chunk: data.read(ONE_KB), final: true)
132
+ @timer.cancel if @timer.respond_to?(:cancel)
133
+ return
134
+ end
135
+ @timer.cancel if @timer.respond_to?(:cancel)
136
+ end
137
+ send_chunk(chunk: data.read(ONE_KB), final: false)
138
+ end
139
+ @timer = EventMachine::Timer.new(TEN_MILLISECONDS) { send_audio(data: data) }
140
+ end
141
+
142
+ def extract_transcripts(alternatives:)
143
+ transcripts = []
144
+ unless alternatives.nil?
145
+ alternatives.each do |alternative|
146
+ transcript = {}
147
+ transcript["confidence"] = alternative["confidence"] if alternative.key?("confidence")
148
+ transcript["transcript"] = alternative["transcript"]
149
+ transcripts << transcript
150
+ end
151
+ end
152
+ transcripts
153
+ end
154
+
155
+ def send_chunk(chunk:, final: false)
156
+ return if chunk.nil?
157
+ @bytes_sent += chunk.size
158
+ @client.send(chunk.bytes)
159
+ @client.send({ "action" => "stop" }.to_json) if final
160
+ @timer.cancel if @timer.respond_to?(:cancel) && final
161
+ end
162
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dotenv/tasks"
4
+ require "rake/testtask"
5
+ require "rubocop/rake_task"
6
+
7
+ task default: %w[def]
8
+
9
+ RuboCop::RakeTask.new
10
+
11
+ namespace :test do
12
+ Rake::TestTask.new do |t|
13
+ t.name = "unit"
14
+ t.description = "Run unit tests"
15
+ t.libs << "test"
16
+ t.test_files = FileList["test/unit/*.rb"]
17
+ t.verbose = true
18
+ t.warning = true
19
+ end
20
+
21
+ Rake::TestTask.new do |t|
22
+ t.name = "integration"
23
+ t.description = "Run integration tests (put credentials in a .env file)"
24
+ t.libs << "test"
25
+ t.test_files = FileList["test/integration/*.rb"]
26
+ t.verbose = true
27
+ t.warning = true
28
+ t.deps = [:dotenv]
29
+ end
30
+ end
31
+
32
+ desc "Run unit & integration tests"
33
+ task :test do
34
+ Rake::Task["test:unit"].invoke
35
+ Rake::Task["test:integration"].invoke
36
+ end
37
+
38
+ desc "Run tests and generate a code coverage report"
39
+ task :coverage do
40
+ ENV["COVERAGE"] = "true"
41
+ Rake::Task["test"].execute
42
+ end
43
+
44
+ task def: %i[rubocop coverage] do
45
+ end