intelligence 0.6.0 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +576 -0
  3. data/intelligence.gemspec +2 -1
  4. data/lib/intelligence/adapter/base.rb +13 -6
  5. data/lib/intelligence/adapter/class_methods.rb +15 -0
  6. data/lib/intelligence/adapter/module_methods.rb +41 -0
  7. data/lib/intelligence/adapter.rb +2 -2
  8. data/lib/intelligence/adapters/anthropic/adapter.rb +21 -19
  9. data/lib/intelligence/adapters/anthropic/chat_request_methods.rb +189 -0
  10. data/lib/intelligence/adapters/anthropic/{chat_methods.rb → chat_response_methods.rb} +13 -137
  11. data/lib/intelligence/adapters/cerebras.rb +19 -19
  12. data/lib/intelligence/adapters/generic/adapter.rb +4 -2
  13. data/lib/intelligence/adapters/generic/chat_request_methods.rb +221 -0
  14. data/lib/intelligence/adapters/generic/chat_response_methods.rb +234 -0
  15. data/lib/intelligence/adapters/generic.rb +1 -1
  16. data/lib/intelligence/adapters/google/adapter.rb +33 -22
  17. data/lib/intelligence/adapters/google/chat_request_methods.rb +234 -0
  18. data/lib/intelligence/adapters/google/chat_response_methods.rb +236 -0
  19. data/lib/intelligence/adapters/groq.rb +29 -49
  20. data/lib/intelligence/adapters/hyperbolic.rb +13 -39
  21. data/lib/intelligence/adapters/mistral.rb +21 -42
  22. data/lib/intelligence/adapters/open_ai/adapter.rb +39 -32
  23. data/lib/intelligence/adapters/open_ai/chat_request_methods.rb +186 -0
  24. data/lib/intelligence/adapters/open_ai/chat_response_methods.rb +239 -0
  25. data/lib/intelligence/adapters/open_ai.rb +1 -1
  26. data/lib/intelligence/adapters/open_router.rb +18 -18
  27. data/lib/intelligence/adapters/samba_nova.rb +16 -18
  28. data/lib/intelligence/adapters/together_ai.rb +25 -23
  29. data/lib/intelligence/conversation.rb +11 -10
  30. data/lib/intelligence/message.rb +45 -29
  31. data/lib/intelligence/message_content/base.rb +2 -9
  32. data/lib/intelligence/message_content/binary.rb +3 -3
  33. data/lib/intelligence/message_content/file.rb +3 -3
  34. data/lib/intelligence/message_content/text.rb +10 -2
  35. data/lib/intelligence/message_content/tool_call.rb +61 -5
  36. data/lib/intelligence/message_content/tool_result.rb +11 -6
  37. data/lib/intelligence/tool.rb +139 -0
  38. data/lib/intelligence/version.rb +1 -1
  39. data/lib/intelligence.rb +3 -1
  40. metadata +31 -13
  41. data/lib/intelligence/adapter/class_methods/construction.rb +0 -17
  42. data/lib/intelligence/adapter/module_methods/construction.rb +0 -43
  43. data/lib/intelligence/adapters/generic/chat_methods.rb +0 -355
  44. data/lib/intelligence/adapters/google/chat_methods.rb +0 -393
  45. data/lib/intelligence/adapters/legacy/adapter.rb +0 -11
  46. data/lib/intelligence/adapters/legacy/chat_methods.rb +0 -54
  47. data/lib/intelligence/adapters/open_ai/chat_methods.rb +0 -345
@@ -1,393 +0,0 @@
1
- require 'uri'
2
-
3
- module Intelligence
4
- module Google
5
- module ChatMethods
6
-
7
- GENERATIVE_LANGUAGE_URI = "https://generativelanguage.googleapis.com/v1beta/models/"
8
-
9
- SUPPORTED_BINARY_MEDIA_TYPES = %w[ text ]
10
-
11
- SUPPORTED_BINARY_CONTENT_TYPES = %w[
12
- image/png image/jpeg image/webp image/heic image/heif
13
- audio/aac audio/flac audio/mp3 audio/m4a audio/mpeg audio/mpga audio/mp4 audio/opus
14
- audio/pcm audio/wav audio/webm
15
- application/pdf
16
- ]
17
-
18
- SUPPORTED_FILE_MEDIA_TYPES = %w[ text ]
19
-
20
- SUPPORTED_CONTENT_TYPES = %w[
21
- image/png image/jpeg image/webp image/heic image/heif
22
- video/x-flv video/quicktime video/mpeg video/mpegps video/mpg video/mp4 video/webm
23
- video/wmv video/3gpp
24
- audio/aac audio/flac audio/mp3 audio/m4a audio/mpeg audio/mpga audio/mp4 audio/opus
25
- audio/pcm audio/wav audio/webm
26
- application/pdf
27
- ]
28
-
29
- def chat_request_uri( options )
30
- options = options ? self.class.configure( options ) : {}
31
- options = @options.merge( options )
32
-
33
- key = options[ :key ]
34
- gc = options[ :generationConfig ] || {}
35
- model = gc[ :model ]
36
- stream = gc.key?( :stream ) ? gc[ :stream ] : false
37
-
38
- raise ArgumentError.new( "A Google API key is required to build a Google chat request." ) \
39
- if key.nil?
40
- raise ArgumentError.new( "A Google model is required to build a Google chat request." ) \
41
- if model.nil?
42
-
43
- uri = URI( GENERATIVE_LANGUAGE_URI )
44
- path = File.join( uri.path, model )
45
- path += stream ? ':streamGenerateContent' : ':generateContent'
46
- uri.path = path
47
- query = { key: key }
48
- query[ :alt ] = 'sse' if stream
49
- uri.query = URI.encode_www_form( query )
50
-
51
- uri.to_s
52
- end
53
-
54
- def chat_request_headers( options = {} )
55
- { 'Content-Type' => 'application/json' }
56
- end
57
-
58
- def chat_request_body( conversation, options = {} )
59
- options = options ? self.class.configure( options ) : {}
60
- options = @options.merge( options )
61
-
62
- gc = options[ :generationConfig ]
63
- # discard properties not part of the google generationConfig schema
64
- gc.delete( :model )
65
- gc.delete( :stream )
66
-
67
- result = {}
68
- result[ :generationConfig ] = gc
69
-
70
- # construct the system prompt in the form of the google schema
71
- system_instructions = translate_system_message( conversation[ :system_message ] )
72
- result[ :systemInstruction ] = system_instructions if system_instructions
73
-
74
- result[ :contents ] = []
75
- conversation[ :messages ]&.each do | message |
76
-
77
- result_message = { role: message[ :role ] == :user ? 'user' : 'model' }
78
- result_message_parts = []
79
-
80
- message[ :contents ]&.each do | content |
81
- case content[ :type ]
82
- when :text
83
- result_message_parts << { text: content[ :text ] }
84
- when :binary
85
- content_type = content[ :content_type ]
86
- bytes = content[ :bytes ]
87
- if content_type && bytes
88
- mime_type = MIME::Types[ content_type ].first
89
- if SUPPORTED_BINARY_MEDIA_TYPES.include?( mime_type&.media_type ) ||
90
- SUPPORTED_BINARY_CONTENT_TYPES.include?( content_type )
91
- result_message_parts << {
92
- inline_data: {
93
- mime_type: content_type,
94
- data: Base64.strict_encode64( bytes )
95
- }
96
- }
97
- else
98
- raise UnsupportedContentError.new(
99
- :google,
100
- "does not support #{content_type} content type"
101
- )
102
- end
103
- else
104
- raise UnsupportedContentError.new(
105
- :google,
106
- 'requires binary content to include content type and ( packed ) bytes'
107
- )
108
- end
109
- when :file
110
- content_type = content[ :content_type ]
111
- uri = content[ :uri ]
112
- if content_type && uri
113
- mime_type = MIME::Types[ content_type ].first
114
- if SUPPORTED_FILE_MEDIA_TYPES.include?( mime_type&.media_type ) ||
115
- SUPPORTED_FILE_CONTENT_TYPES.include?( content_type )
116
- result_message_parts << {
117
- file_data: {
118
- mime_type: content_type,
119
- file_uri: uri
120
- }
121
- }
122
- else
123
- raise UnsupportedContentError.new(
124
- :google,
125
- "does not support #{content_type} content type"
126
- )
127
- end
128
- else
129
- raise UnsupportedContentError.new(
130
- :google,
131
- 'requires file content to include content type and uri'
132
- )
133
- end
134
-
135
- else
136
- raise InvalidContentError.new( :google )
137
- end
138
- end
139
-
140
- result_message[ :parts ] = result_message_parts
141
- result[ :contents ] << result_message
142
-
143
- end
144
-
145
- JSON.generate( result )
146
-
147
- end
148
-
149
- def chat_result_attributes( response )
150
-
151
- return nil unless response.success?
152
-
153
- response_json = JSON.parse( response.body, symbolize_names: true ) rescue nil
154
- return nil \
155
- if response_json.nil? || response_json[ :candidates ].nil?
156
-
157
- result = {}
158
- result[ :choices ] = []
159
-
160
- response_json[ :candidates ]&.each do | response_choice |
161
-
162
- end_reason = translate_finish_reason( response_choice[ :finishReason ] )
163
-
164
- role = nil
165
- contents = []
166
-
167
- response_content = response_choice[ :content ]
168
- if response_content
169
- role = ( response_content[ :role ] == 'model' ) ? 'assistant' : 'user'
170
-
171
- contents = []
172
- response_content[ :parts ]&.each do | response_content_part |
173
- if response_content_part.key?( :text )
174
- contents.push( {
175
- type: 'text', text: response_content_part[ :text ]
176
- } )
177
- end
178
- end
179
- end
180
-
181
- result_message = nil
182
- if role
183
- result_message = { role: role }
184
- result_message[ :contents ] = contents
185
- end
186
-
187
- result[ :choices ].push( { end_reason: end_reason, message: result_message } )
188
-
189
- end
190
-
191
- metrics_json = response_json[ :usageMetadata ]
192
- unless metrics_json.nil?
193
-
194
- metrics = {}
195
- metrics[ :input_tokens ] = metrics_json[ :promptTokenCount ]
196
- metrics[ :output_tokens ] = metrics_json[ :candidatesTokenCount ]
197
- metrics = metrics.compact
198
-
199
- result[ :metrics ] = metrics unless metrics.empty?
200
-
201
- end
202
-
203
- result
204
-
205
- end
206
-
207
- def chat_result_error_attributes( response )
208
-
209
- error_type, error_description = translate_error_response_status( response.status )
210
- result = { error_type: error_type.to_s, error_description: error_description }
211
-
212
- response_body = JSON.parse( response.body, symbolize_names: true ) rescue nil
213
- if response_body && response_body[ :error ]
214
- error_details_reason = response_body[ :error ][ :details ]&.first&.[]( :reason )
215
- # a special case for authentication
216
- error_type = :authentication_error if error_details_reason == 'API_KEY_INVALID'
217
- result = {
218
- error_type: error_type.to_s,
219
- error: error_details_reason || response_body[ :error ][ :status ] || error_type,
220
- error_description: response_body[ :error ][ :message ]
221
- }
222
- end
223
- result
224
-
225
- end
226
-
227
- def stream_result_chunk_attributes( context, chunk )
228
- #---------------------------------------------------
229
-
230
- context ||= {}
231
- buffer = context[ :buffer ] || ''
232
- metrics = context[ :metrics ] || {
233
- input_tokens: 0,
234
- output_tokens: 0
235
- }
236
- choices = context[ :choices ] || Array.new( 1 , { message: {} } )
237
-
238
- choices.each do | choice |
239
- choice[ :message ][ :contents ] = choice[ :message ][ :contents ]&.map do | content |
240
- case content[ :type ]
241
- when :text
242
- content[ :text ] = ''
243
- else
244
- content.clear
245
- end
246
- content
247
- end
248
- end
249
-
250
- buffer += chunk
251
- while ( eol_index = buffer.index( "\n" ) )
252
-
253
- line = buffer.slice!( 0..eol_index )
254
- line = line.strip
255
- next if line.empty? || !line.start_with?( 'data:' )
256
- line = line[ 6..-1 ]
257
-
258
- data = JSON.parse( line, symbolize_names: true )
259
- if data.is_a?( Hash )
260
-
261
- data[ :candidates ]&.each do | data_candidate |
262
-
263
- data_candidate_index = data_candidate[ :index ] || 0
264
- data_candidate_content = data_candidate[ :content ]
265
- data_candidate_finish_reason = data_candidate[ :finishReason ]
266
- choices.fill( { message: { role: 'assistant' } }, choices.size, data_candidate_index + 1 ) \
267
- if choices.size <= data_candidate_index
268
- contents = choices[ data_candidate_index ][ :message ][ :contents ] || []
269
- last_content = contents&.last
270
-
271
- if data_candidate_content&.include?( :parts )
272
- data_candidate_content_parts = data_candidate_content[ :parts ]
273
- data_candidate_content_parts&.each do | data_candidate_content_part |
274
- if data_candidate_content_part.key?( :text )
275
- if last_content.nil? || last_content[ :type ] != :text
276
- contents.push( { type: :text, text: data_candidate_content_part[ :text ] } )
277
- else
278
- last_content[ :text ] =
279
- ( last_content[ :text ] || '' ) + data_candidate_content_part[ :text ]
280
- end
281
- end
282
- end
283
- end
284
- choices[ data_candidate_index ][ :message ][ :contents ] = contents
285
- choices[ data_candidate_index ][ :end_reason ] =
286
- translate_finish_reason( data_candidate_finish_reason )
287
- end
288
-
289
- if usage = data[ :usageMetadata ]
290
- metrics[ :input_tokens ] = usage[ :promptTokenCount ]
291
- metrics[ :output_tokens ] = usage[ :candidatesTokenCount ]
292
- end
293
-
294
- end
295
-
296
- end
297
-
298
- context[ :buffer ] = buffer
299
- context[ :metrics ] = metrics
300
- context[ :choices ] = choices
301
-
302
- [ context, choices.empty? ? nil : { choices: choices.dup } ]
303
-
304
- end
305
-
306
- def stream_result_attributes( context )
307
- #--------------------------------------
308
-
309
- choices = context[ :choices ]
310
- metrics = context[ :metrics ]
311
-
312
- choices = choices.map do | choice |
313
- { end_reason: choice[ :end_reason ] }
314
- end
315
-
316
- { choices: choices, metrics: context[ :metrics ] }
317
-
318
- end
319
-
320
- alias_method :stream_result_error_attributes, :chat_result_error_attributes
321
-
322
- private; def translate_system_message( system_message )
323
- # -----------------------------------------------------
324
-
325
- return nil if system_message.nil?
326
-
327
- text = ''
328
- system_message[ :contents ].each do | content |
329
- text += content[ :text ] if content[ :type ] == :text
330
- end
331
-
332
- return nil if text.empty?
333
-
334
- {
335
- role: 'user',
336
- parts: [
337
- { text: text }
338
- ]
339
- }
340
-
341
- end
342
-
343
- private; def translate_finish_reason( finish_reason )
344
- # ---------------------------------------------------
345
- case finish_reason
346
- when 'STOP'
347
- :ended
348
- when 'MAX_TOKENS'
349
- :token_limit_exceeded
350
- when 'SAFETY', 'RECITATION', 'BLOCKLIST', 'PROHIBITED_CONTENT', 'SPII'
351
- :filtered
352
- else
353
- nil
354
- end
355
- end
356
-
357
- private; def translate_error_response_status( status )
358
- case status
359
- when 400
360
- [ :invalid_request_error,
361
- "There was an issue with the format or content of your request." ]
362
- when 403
363
- [ :permission_error,
364
- "Your API key does not have permission to use the specified resource." ]
365
- when 404
366
- [ :not_found_error,
367
- "The requested resource was not found." ]
368
- when 413
369
- [ :request_too_large,
370
- "Request exceeds the maximum allowed number of bytes." ]
371
- when 422
372
- [ :invalid_request_error,
373
- "There was an issue with the format or content of your request." ]
374
- when 429
375
- [ :rate_limit_error,
376
- "Your account has hit a rate limit." ]
377
- when 500, 502, 503
378
- [ :api_error,
379
- "An unexpected error has occurred internal to the providers systems." ]
380
- when 529
381
- [ :overloaded_error,
382
- "The providers server is temporarily overloaded." ]
383
- else
384
- [ :unknown_error, "
385
- An unknown error occurred." ]
386
- end
387
- end
388
-
389
- end
390
-
391
- end
392
-
393
- 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,54 +0,0 @@
1
- module Intelligence
2
- module Legacy
3
- module ChatMethods
4
-
5
- def chat_request_body( conversation, options = {} )
6
- options = options ? self.class.configure( options ) : {}
7
- options = @options.merge( options )
8
-
9
- result = options[ :chat_options ]&.compact || {}
10
- result[ :messages ] = []
11
-
12
- system_message = system_message_to_s( conversation[ :system_message ] )
13
- result[ :messages ] << { role: 'system', content: system_message } if system_message
14
-
15
- # detect if the conversation has any non-text content; this handles the sittuation
16
- # where non-vision models only support the legacy message schema while the vision
17
- # models only support the modern message schema
18
- has_non_text_content = conversation[ :messages ]&.find do | message |
19
- message[ :contents ]&.find do | content |
20
- content[ :type ] != nil && content[ :type ] != :text
21
- end
22
- end
23
-
24
- if has_non_text_content
25
- conversation[ :messages ]&.each do | message |
26
- result[ :messages ] << chat_request_message_attributes( message )
27
- end
28
- else
29
- conversation[ :messages ]&.each do | message |
30
- result[ :messages ] << chat_request_legacy_message_attributes( message )
31
- end
32
- end
33
- JSON.generate( result )
34
- end
35
-
36
- def chat_request_legacy_message_attributes( message )
37
- result_message = { role: message[ :role ] }
38
- result_message_content = ""
39
-
40
- message[ :contents ]&.each do | content |
41
- case content[ :type ]
42
- when :text
43
- result_message_content += content[ :text ]
44
- end
45
- end
46
-
47
- result_message[ :content ] = result_message_content
48
- result_message
49
- end
50
-
51
- end
52
- end
53
- end
54
-