intelligence 0.7.1 → 0.8.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.
@@ -11,12 +11,64 @@ module Intelligence
11
11
 
12
12
  attr_reader :tool_call_id
13
13
  attr_reader :tool_name
14
- attr_reader :tool_parameters
14
+
15
+ def tool_parameters( options = nil )
16
+ return @tool_parameters if @tool_parameters.nil? || @tool_parameters.is_a?( Hash )
17
+
18
+ options = options || {}
19
+ parse = options.key?( :parse ) ? options[ :parse ] : true
20
+
21
+ return @parsed_tool_parameters if @parsed_tool_parameters && parse
22
+
23
+ tool_parameters = ( options.key?( :repair ) ? options[ :repair ] : true ) ?
24
+ JSON.repair( @tool_parameters ) : @tool_parameters
25
+
26
+ if parse
27
+ @parsed_tool_parameters = tool_parameters =
28
+ JSON.parse(
29
+ tool_parameters,
30
+ parse.is_a?( Hash ) ? parse : { symbolize_names: true }
31
+ )
32
+ end
33
+
34
+ tool_parameters
35
+ end
15
36
 
16
37
  def valid?
17
38
  tool_name && !tool_name.empty?
18
39
  end
19
40
 
41
+ def merge( other_tool_call )
42
+ other_tool_call_id = other_tool_call.tool_call_id
43
+ other_tool_name = other_tool_call.tool_name
44
+ other_tool_parameters = other_tool_call.tool_parameters( repair: false, parse: false )
45
+
46
+ raise ArgumentError,
47
+ "The given tool call parameters are incompative with this tool's parameters." \
48
+ unless @tool_parameters.nil? || other_tool_parameters.nil? ||
49
+ @tool_parameters.is_a?( other_tool_parameters.class )
50
+
51
+ tool_call_id = other_tool_call_id.nil? ?
52
+ @tool_call_id : @tool_call_id || '' + other_tool_call_id
53
+ tool_name = other_tool_name.nil? ?
54
+ @tool_name : @tool_name || '' + other_tool_name
55
+ tool_parameters = @tool_parameters
56
+
57
+ unless other_tool_parameters.nil?
58
+ if other_tool_parameters.is_a?( Hash )
59
+ tool_parameters = ( tool_parameters || {} ).merge( other_tool_parameters )
60
+ else
61
+ tool_parameters = ( tool_parameters || '' ) + other_tool_parameters
62
+ end
63
+ end
64
+
65
+ self.class.new(
66
+ tool_call_id: tool_call_id,
67
+ tool_name: tool_name,
68
+ tool_parameters: tool_parameters
69
+ )
70
+ end
71
+
20
72
  def to_h
21
73
  {
22
74
  type: :tool_call,
@@ -1,3 +1,3 @@
1
1
  module Intelligence
2
- VERSION = "0.7.1"
2
+ VERSION = "0.8.0"
3
3
  end
data/lib/intelligence.rb CHANGED
@@ -4,6 +4,7 @@ require 'base64'
4
4
  require 'faraday'
5
5
  require 'dynamic_schema'
6
6
  require 'mime-types'
7
+ require 'json/repair'
7
8
 
8
9
  require 'intelligence/version'
9
10
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: intelligence
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.1
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kristoph Cichocki-Romanov
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-10-27 00:00:00.000000000 Z
11
+ date: 2024-10-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '3.6'
55
+ - !ruby/object:Gem::Dependency
56
+ name: json-repair
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '0.2'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '0.2'
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: rspec
57
71
  requirement: !ruby/object:Gem::Requirement
@@ -123,15 +137,14 @@ files:
123
137
  - lib/intelligence/adapters/cerebras.rb
124
138
  - lib/intelligence/adapters/generic.rb
125
139
  - lib/intelligence/adapters/generic/adapter.rb
126
- - lib/intelligence/adapters/generic/chat_methods.rb
140
+ - lib/intelligence/adapters/generic/chat_request_methods.rb
141
+ - lib/intelligence/adapters/generic/chat_response_methods.rb
127
142
  - lib/intelligence/adapters/google.rb
128
143
  - lib/intelligence/adapters/google/adapter.rb
129
144
  - lib/intelligence/adapters/google/chat_request_methods.rb
130
145
  - lib/intelligence/adapters/google/chat_response_methods.rb
131
146
  - lib/intelligence/adapters/groq.rb
132
147
  - lib/intelligence/adapters/hyperbolic.rb
133
- - lib/intelligence/adapters/legacy/adapter.rb
134
- - lib/intelligence/adapters/legacy/chat_methods.rb
135
148
  - lib/intelligence/adapters/mistral.rb
136
149
  - lib/intelligence/adapters/open_ai.rb
137
150
  - lib/intelligence/adapters/open_ai/adapter.rb
@@ -1,362 +0,0 @@
1
- module Intelligence
2
- module Generic
3
- module ChatMethods
4
-
5
- module ClassMethods
6
- def chat_request_uri( uri = nil )
7
- if uri
8
- @chat_request_uri = uri
9
- else
10
- @chat_request_uri
11
- end
12
- end
13
- end
14
-
15
- def self.included( base )
16
- base.extend( ClassMethods )
17
- end
18
-
19
- def chat_request_uri( options )
20
- self.class.chat_request_uri
21
- end
22
-
23
- def chat_request_headers( options = nil )
24
- options = @options.merge( build_options( options ) )
25
- result = {}
26
-
27
- key = options[ :key ]
28
-
29
- raise ArgumentError.new( "An API key is required to build a chat request." ) \
30
- if key.nil?
31
-
32
- result[ 'Content-Type' ] = 'application/json'
33
- result[ 'Authorization' ] = "Bearer #{key}"
34
-
35
- result
36
- end
37
-
38
- def chat_request_body( conversation, options = nil )
39
- options = @options.merge( build_options( options ) )
40
-
41
- result = options[ :chat_options ]
42
- result[ :messages ] = []
43
-
44
- system_message = system_message_to_s( conversation[ :system_message ] )
45
- result[ :messages ] << { role: 'system', content: system_message } if system_message
46
-
47
- conversation[ :messages ]&.each do | message |
48
- result[ :messages ] << chat_request_message_attributes( message )
49
- end
50
-
51
- JSON.generate( result )
52
- end
53
-
54
- def chat_request_message_attributes( message )
55
- result_message = { role: message[ :role ] }
56
- result_message_content = []
57
- message[ :contents ]&.each do | content |
58
- case content[ :type ]
59
- when :text
60
- result_message_content << { type: 'text', text: content[ :text ] }
61
- when :binary
62
- content_type = content[ :content_type ]
63
- bytes = content[ :bytes ]
64
- if content_type && bytes
65
- mime_type = MIME::Types[ content_type ].first
66
- if mime_type&.media_type == 'image'
67
- result_message_content << {
68
- type: 'image_url',
69
- image_url: {
70
- url: "data:#{content_type};base64,#{Base64.strict_encode64( bytes )}".freeze
71
- }
72
- }
73
- else
74
- raise UnsupportedContentError.new(
75
- :generic,
76
- 'only support content of type image/*'
77
- )
78
- end
79
- else
80
- raise UnsupportedContentError.new(
81
- :generic,
82
- 'requires binary content to include content type and ( packed ) bytes'
83
- )
84
- end
85
- when :file
86
- content_type = content[ :content_type ]
87
- uri = content[ :uri ]
88
- if content_type && uri
89
- mime_type = MIME::Types[ content_type ].first
90
- if mime_type&.media_type == 'image'
91
- result_message_content << {
92
- type: 'image_url',
93
- image_url: { url: uri }
94
- }
95
- else
96
- raise UnsupportedContentError.new(
97
- :generic,
98
- 'only support content of type image/*'
99
- )
100
- end
101
- else
102
- raise UnsupportedContentError.new(
103
- :generic,
104
- 'requires binary content to include content type and ( packed ) bytes'
105
- )
106
- end
107
- end
108
- end
109
- result_message[ :content ] = result_message_content
110
- result_message
111
- end
112
-
113
- def chat_result_attributes( response )
114
-
115
- return nil unless response.success?
116
- response_json = JSON.parse( response.body, symbolize_names: true ) rescue nil
117
- return nil if response_json.nil? || response_json[ :choices ].nil?
118
-
119
- result = {}
120
- result[ :choices ] = []
121
-
122
- ( response_json[ :choices ] || [] ).each do | json_choice |
123
- json_message = json_choice[ :message ]
124
- result[ :choices ].push(
125
- {
126
- end_reason: translate_end_result( json_choice[ :finish_reason ] ),
127
- message: {
128
- role: json_message[ :role ],
129
- contents: [ { type: 'text', text: json_message[ :content ] } ]
130
- }
131
- }
132
- )
133
- end
134
-
135
- metrics_json = response_json[ :usage ]
136
- unless metrics_json.nil?
137
-
138
- metrics = {}
139
- metrics[ :input_tokens ] = metrics_json[ :prompt_tokens ]
140
- metrics[ :output_tokens ] = metrics_json[ :completion_tokens ]
141
- metrics = metrics.compact
142
-
143
- result[ :metrics ] = metrics unless metrics.empty?
144
-
145
- end
146
-
147
- result
148
-
149
- end
150
-
151
- def chat_result_error_attributes( response )
152
-
153
- error_type, error_description = translate_error_response_status( response.status )
154
- result = {
155
- error_type: error_type.to_s,
156
- error_description: error_description
157
- }
158
-
159
- parsed_body = JSON.parse( response.body, symbolize_names: true ) rescue nil
160
- if parsed_body && parsed_body.respond_to?( :include? ) && parsed_body.include?( :error )
161
- result = {
162
- error_type: error_type.to_s,
163
- error: parsed_body[ :error ][ :code ] || error_type.to_s,
164
- error_description: parsed_body[ :error ][ :message ] || error_description
165
- }
166
- end
167
-
168
- result
169
-
170
- end
171
-
172
- def stream_result_chunk_attributes( context, chunk )
173
-
174
- context ||= {}
175
- buffer = context[ :buffer ] || ''
176
- metrics = context[ :metrics ] || {
177
- input_tokens: 0,
178
- output_tokens: 0
179
- }
180
- choices = context[ :choices ] || Array.new( 1 , { message: {} } )
181
-
182
- choices.each do | choice |
183
- choice[ :message ][ :contents ] = choice[ :message ][ :contents ]&.map do | content |
184
- case content[ :type ]
185
- when :text
186
- content[ :text ] = ''
187
- when :tool_call
188
- content[ :tool_parameters ] = ''
189
- else
190
- content.clear
191
- end
192
- content
193
- end
194
- end
195
-
196
- buffer += chunk
197
- while ( eol_index = buffer.index( "\n" ) )
198
-
199
- line = buffer.slice!( 0..eol_index )
200
- line = line.strip
201
- next if line.empty? || !line.start_with?( 'data:' )
202
- line = line[ 6..-1 ]
203
-
204
- next if line.end_with?( '[DONE]' )
205
- data = JSON.parse( line )
206
- if data.is_a?( Hash )
207
-
208
- data[ 'choices' ]&.each do | data_choice |
209
-
210
- data_choice_index = data_choice[ 'index' ]
211
- data_choice_delta = data_choice[ 'delta' ]
212
- data_choice_finish_reason = data_choice[ 'finish_reason' ]
213
-
214
- choices.fill( { message: {} }, choices.size, data_choice_index + 1 ) \
215
- if choices.size <= data_choice_index
216
- contents = choices[ data_choice_index ][ :message ][ :contents ] || []
217
- last_content = contents&.last
218
-
219
- if data_choice_delta.include?( 'content' )
220
- data_choice_content = data_choice_delta[ 'content' ] || ''
221
- if last_content.nil? || last_content[ :type ] == :tool_call
222
- contents.push( { type: :text, text: data_choice_content } )
223
- elsif last_content[ :type ] == :text || last_content[ :type ].nil?
224
- last_content[ :type ] = :text
225
- last_content[ :text ] = ( last_content[ :text ] || '' ) + data_choice_content
226
- end
227
- elsif data_choice_delta.include?( 'function_call' )
228
- data_choice_tool_call = data_choice_delta[ 'function_call' ]
229
- data_choice_tool_name = data_choice_tool_call[ 'name' ]
230
- data_choice_tool_parameters = data_choice_tool_call[ 'arguments' ]
231
- if last_content.nil? || last_content[ :type ] == :text
232
- contents.push( {
233
- type: :tool_call,
234
- tool_name: data_choice_tool_name,
235
- tool_parameters: data_choice_tool_parameters
236
- } )
237
- elsif last_content[ :type ].nil?
238
- last_content[ :type ] = :tool_call
239
- last_content[ :tool_name ] = data_choice_tool_name \
240
- if data_choice_tool_name.present?
241
- last_content[ :tool_parameters ] = tool_parameters
242
- elsif last_content[ :type ] == :tool_call
243
- last_content[ :tool_parameters ] =
244
- ( last_content[ :tool_parameters ] || '' ) + data_choice_tool_parameters
245
- end
246
- end
247
- choices[ data_choice_index ][ :message ][ :contents ] = contents
248
- choices[ data_choice_index ][ :end_reason ] ||=
249
- translate_end_result( data_choice_finish_reason )
250
- end
251
-
252
- if usage = data[ 'usage' ]
253
- # note: A number of providers will resend the input tokens as part of their usage
254
- # payload.
255
- metrics[ :input_tokens ] = usage[ 'prompt_tokens' ] \
256
- if usage.include?( 'prompt_tokens' )
257
- metrics[ :output_tokens ] += usage[ 'completion_tokens' ] \
258
- if usage.include?( 'completion_tokens' )
259
- end
260
-
261
- end
262
-
263
- end
264
-
265
- context[ :buffer ] = buffer
266
- context[ :metrics ] = metrics
267
- context[ :choices ] = choices
268
-
269
- [ context, choices.empty? ? nil : { choices: choices.dup } ]
270
-
271
- end
272
-
273
- def stream_result_attributes( context )
274
-
275
- choices = context[ :choices ]
276
- metrics = context[ :metrics ]
277
-
278
- choices = choices.map do | choice |
279
- { end_reason: choice[ :end_reason ] }
280
- end
281
-
282
- { choices: choices, metrics: context[ :metrics ] }
283
-
284
- end
285
-
286
- alias_method :stream_result_error_attributes, :chat_result_error_attributes
287
-
288
- def translate_end_result( end_result )
289
- case end_result
290
- when 'stop'
291
- :ended
292
- when 'length'
293
- :token_limit_exceeded
294
- when 'function_call'
295
- :tool_called
296
- when 'content_filter'
297
- :filtered
298
- else
299
- nil
300
- end
301
- end
302
-
303
- def translate_error_response_status( status )
304
- case status
305
- when 400
306
- [ :invalid_request_error,
307
- "There was an issue with the format or content of your request." ]
308
- when 401
309
- [ :authentication_error,
310
- "There's an issue with your API key." ]
311
- when 403
312
- [ :permission_error,
313
- "Your API key does not have permission to use the specified resource." ]
314
- when 404
315
- [ :not_found_error,
316
- "The requested resource was not found." ]
317
- when 413
318
- [ :request_too_large,
319
- "Request exceeds the maximum allowed number of bytes." ]
320
- when 422
321
- [ :invalid_request_error,
322
- "There was an issue with the format or content of your request." ]
323
- when 429
324
- [ :rate_limit_error,
325
- "Your account has hit a rate limit." ]
326
- when 500, 502, 503
327
- [ :api_error,
328
- "An unexpected error has occurred internal to the providers systems." ]
329
- when 529
330
- [ :overloaded_error,
331
- "The providers server is temporarily overloaded." ]
332
- else
333
- [ :unknown_error, "
334
- An unknown error occurred." ]
335
- end
336
- end
337
-
338
- private
339
-
340
- def to_options( options, &block )
341
- return {} unless options&.any?
342
- @options_builder ||= DynamicSchema::Builder.new.define( &self.class.schema )
343
- @options_builder.build( options, &block )
344
- end
345
-
346
-
347
- def system_message_to_s( system_message )
348
-
349
- return nil if system_message.nil?
350
-
351
- result = ''
352
- system_message[ :contents ].each do | content |
353
- result += content[ :text ] if content[ :type ] == :text
354
- end
355
-
356
- result.empty? ? nil : result
357
-
358
- end
359
-
360
- end
361
- end
362
- end
@@ -1,11 +0,0 @@
1
- require_relative '../generic/adapter'
2
- require_relative 'chat_methods'
3
-
4
- module Intelligence
5
- module Legacy
6
- class Adapter < Generic::Adapter
7
- include ChatMethods
8
- end
9
- end
10
- end
11
-
@@ -1,53 +0,0 @@
1
- module Intelligence
2
- module Legacy
3
- module ChatMethods
4
-
5
- def chat_request_body( conversation, options = {} )
6
- options = @options.merge( to_options( options ) )
7
-
8
- result = options[ :chat_options ]&.compact || {}
9
- result[ :messages ] = []
10
-
11
- system_message = system_message_to_s( conversation[ :system_message ] )
12
- result[ :messages ] << { role: 'system', content: system_message } if system_message
13
-
14
- # detect if the conversation has any non-text content; this handles the sittuation
15
- # where non-vision models only support the legacy message schema while the vision
16
- # models only support the modern message schema
17
- has_non_text_content = conversation[ :messages ]&.find do | message |
18
- message[ :contents ]&.find do | content |
19
- content[ :type ] != nil && content[ :type ] != :text
20
- end
21
- end
22
-
23
- if has_non_text_content
24
- conversation[ :messages ]&.each do | message |
25
- result[ :messages ] << chat_request_message_attributes( message )
26
- end
27
- else
28
- conversation[ :messages ]&.each do | message |
29
- result[ :messages ] << chat_request_legacy_message_attributes( message )
30
- end
31
- end
32
- JSON.generate( result )
33
- end
34
-
35
- def chat_request_legacy_message_attributes( message )
36
- result_message = { role: message[ :role ] }
37
- result_message_content = ""
38
-
39
- message[ :contents ]&.each do | content |
40
- case content[ :type ]
41
- when :text
42
- result_message_content += content[ :text ]
43
- end
44
- end
45
-
46
- result_message[ :content ] = result_message_content
47
- result_message
48
- end
49
-
50
- end
51
- end
52
- end
53
-