intelligence 0.5.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 (46) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/intelligence.gemspec +47 -0
  4. data/lib/intelligence/adapter/base.rb +7 -0
  5. data/lib/intelligence/adapter/construction_methods.rb +37 -0
  6. data/lib/intelligence/adapter.rb +8 -0
  7. data/lib/intelligence/adapter_error.rb +8 -0
  8. data/lib/intelligence/adapters/anthropic/adapter.rb +59 -0
  9. data/lib/intelligence/adapters/anthropic/chat_methods.rb +358 -0
  10. data/lib/intelligence/adapters/anthropic.rb +2 -0
  11. data/lib/intelligence/adapters/cerebras.rb +35 -0
  12. data/lib/intelligence/adapters/generic/adapter.rb +21 -0
  13. data/lib/intelligence/adapters/generic/chat_methods.rb +331 -0
  14. data/lib/intelligence/adapters/generic.rb +2 -0
  15. data/lib/intelligence/adapters/google/adapter.rb +60 -0
  16. data/lib/intelligence/adapters/google/chat_methods.rb +346 -0
  17. data/lib/intelligence/adapters/google.rb +2 -0
  18. data/lib/intelligence/adapters/groq.rb +51 -0
  19. data/lib/intelligence/adapters/hyperbolic.rb +55 -0
  20. data/lib/intelligence/adapters/legacy/adapter.rb +13 -0
  21. data/lib/intelligence/adapters/legacy/chat_methods.rb +37 -0
  22. data/lib/intelligence/adapters/open_ai/adapter.rb +75 -0
  23. data/lib/intelligence/adapters/open_ai/chat_methods.rb +314 -0
  24. data/lib/intelligence/adapters/open_ai.rb +2 -0
  25. data/lib/intelligence/adapters/samba_nova.rb +64 -0
  26. data/lib/intelligence/adapters/together_ai.rb +46 -0
  27. data/lib/intelligence/chat_error_result.rb +11 -0
  28. data/lib/intelligence/chat_metrics.rb +32 -0
  29. data/lib/intelligence/chat_request.rb +117 -0
  30. data/lib/intelligence/chat_result.rb +32 -0
  31. data/lib/intelligence/chat_result_choice.rb +26 -0
  32. data/lib/intelligence/conversation.rb +48 -0
  33. data/lib/intelligence/error.rb +3 -0
  34. data/lib/intelligence/error_result.rb +24 -0
  35. data/lib/intelligence/invalid_content_error.rb +3 -0
  36. data/lib/intelligence/message.rb +53 -0
  37. data/lib/intelligence/message_content/base.rb +18 -0
  38. data/lib/intelligence/message_content/binary.rb +24 -0
  39. data/lib/intelligence/message_content/text.rb +17 -0
  40. data/lib/intelligence/message_content/tool_call.rb +20 -0
  41. data/lib/intelligence/message_content/tool_result.rb +20 -0
  42. data/lib/intelligence/message_content.rb +16 -0
  43. data/lib/intelligence/unsupported_content_error.rb +3 -0
  44. data/lib/intelligence/version.rb +3 -0
  45. data/lib/intelligence.rb +24 -0
  46. metadata +181 -0
@@ -0,0 +1,346 @@
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
+ def chat_request_uri( options )
10
+
11
+ options = options.nil? || options.empty? ? {} : self.class.configure( options )
12
+
13
+ key = options[ :key ] || self.key
14
+
15
+ gc = options[ :generationConfig ] || {}
16
+ model = gc[ :model ] || self.model
17
+ stream = gc.key?( :stream ) ? gc[ :stream ] : self.stream
18
+
19
+ raise ArgumentError.new( "A Google API key is required to build a Google chat request." ) \
20
+ if self.key.nil?
21
+ raise ArgumentError.new( "A Google model is required to build a Google chat request." ) \
22
+ if model.nil?
23
+
24
+ uri = URI( GENERATIVE_LANGUAGE_URI )
25
+ path = File.join( uri.path, model )
26
+ path += stream ? ':streamGenerateContent' : ':generateContent'
27
+ uri.path = path
28
+ query = { key: self.key }
29
+ query[ :alt ] = 'sse' if stream
30
+ uri.query = URI.encode_www_form( query )
31
+
32
+ uri.to_s
33
+
34
+ end
35
+
36
+ def chat_request_headers( options = {} )
37
+ { 'Content-Type' => 'application/json' }
38
+ end
39
+
40
+ def chat_request_body( conversation, options = {} )
41
+
42
+ result = {}
43
+ result[ :generationConfig ] = self.chat_options
44
+
45
+ options = options.nil? || options.empty? ? {} : self.class.configure( options )
46
+ result = result.merge( options )
47
+
48
+ # discard properties not part of the google endpoint schema
49
+ result[ :generationConfig ].delete( :model )
50
+ result[ :generationConfig ].delete( :stream )
51
+
52
+ # construct the system prompt in the form of the google schema
53
+ system_instructions = translate_system_message( conversation[ :system_message ] )
54
+ result[ :systemInstruction ] = system_instructions if system_instructions
55
+
56
+ result[ :contents ] = []
57
+ conversation[ :messages ]&.each do | message |
58
+
59
+ result_message = { role: message[ :role ] == :user ? 'user' : 'model' }
60
+ result_message_parts = []
61
+
62
+ message[ :contents ]&.each do | content |
63
+ case content[ :type ]
64
+ when :text
65
+ result_message_parts << { text: content[ :text ] }
66
+ when :binary
67
+ content_type = content[ :content_type ]
68
+ bytes = content[ :bytes ]
69
+ if content_type && bytes
70
+ unless MIME::Types[ content_type ].empty?
71
+ # TODO: verify the specific google supported MIME types
72
+ result_message_parts << {
73
+ inline_data: {
74
+ mime_type: content_type,
75
+ data: Base64.strict_encode64( bytes )
76
+ }
77
+ }
78
+ else
79
+ raise UnsupportedContentError.new(
80
+ :google,
81
+ 'only support recognized mime types'
82
+ )
83
+ end
84
+ else
85
+ raise UnsupportedContentError.new(
86
+ :google,
87
+ 'requires binary content to include content type and ( packed ) bytes'
88
+ )
89
+ end
90
+ end
91
+ end
92
+
93
+ result_message[ :parts ] = result_message_parts
94
+ result[ :contents ] << result_message
95
+
96
+ end
97
+
98
+ JSON.generate( result )
99
+
100
+ end
101
+
102
+ def chat_result_attributes( response )
103
+
104
+ return nil unless response.success?
105
+
106
+ response_json = JSON.parse( response.body, symbolize_names: true ) rescue nil
107
+ return nil \
108
+ if response_json.nil? || response_json[ :candidates ].nil?
109
+
110
+ result = {}
111
+ result[ :choices ] = []
112
+
113
+ response_json[ :candidates ]&.each do | response_choice |
114
+
115
+ end_reason = translate_finish_reason( response_choice[ :finishReason ] )
116
+
117
+ role = nil
118
+ contents = []
119
+
120
+ response_content = response_choice[ :content ]
121
+ if response_content
122
+ role = ( response_content[ :role ] == 'model' ) ? 'assistant' : 'user'
123
+
124
+ contents = []
125
+ response_content[ :parts ]&.each do | response_content_part |
126
+ if response_content_part.key?( :text )
127
+ contents.push( {
128
+ type: 'text', text: response_content_part[ :text ]
129
+ } )
130
+ end
131
+ end
132
+ end
133
+
134
+ result_message = nil
135
+ if role
136
+ result_message = { role: role }
137
+ result_message[ :contents ] = contents
138
+ end
139
+
140
+ result[ :choices ].push( { end_reason: end_reason, message: result_message } )
141
+
142
+ end
143
+
144
+ metrics_json = response_json[ :usageMetadata ]
145
+ unless metrics_json.nil?
146
+
147
+ metrics = {}
148
+ metrics[ :input_tokens ] = metrics_json[ :promptTokenCount ]
149
+ metrics[ :output_tokens ] = metrics_json[ :candidatesTokenCount ]
150
+ metrics = metrics.compact
151
+
152
+ result[ :metrics ] = metrics unless metrics.empty?
153
+
154
+ end
155
+
156
+ result
157
+
158
+ end
159
+
160
+ def chat_result_error_attributes( response )
161
+
162
+ error_type, error_description = translate_error_response_status( response.status )
163
+ result = { error_type: error_type.to_s, error_description: error_description }
164
+
165
+ response_body = JSON.parse( response.body, symbolize_names: true ) rescue nil
166
+ if response_body && response_body[ :error ]
167
+ error_details_reason = response_body[ :error ][ :details ]&.first&.[]( :reason )
168
+ # a special case for authentication
169
+ error_type = :authentication_error if error_details_reason == 'API_KEY_INVALID'
170
+ result = {
171
+ error_type: error_type.to_s,
172
+ error: error_details_reason || response_body[ :error ][ :status ] || error_type,
173
+ error_description: response_body[ :error ][ :message ]
174
+ }
175
+ end
176
+ result
177
+
178
+ end
179
+
180
+ def stream_result_chunk_attributes( context, chunk )
181
+ #---------------------------------------------------
182
+
183
+ context ||= {}
184
+ buffer = context[ :buffer ] || ''
185
+ metrics = context[ :metrics ] || {
186
+ input_tokens: 0,
187
+ output_tokens: 0
188
+ }
189
+ choices = context[ :choices ] || Array.new( 1 , { message: {} } )
190
+
191
+ choices.each do | choice |
192
+ choice[ :message ][ :contents ] = choice[ :message ][ :contents ]&.map do | content |
193
+ case content[ :type ]
194
+ when :text
195
+ content[ :text ] = ''
196
+ else
197
+ content.clear
198
+ end
199
+ content
200
+ end
201
+ end
202
+
203
+ buffer += chunk
204
+ while ( eol_index = buffer.index( "\n" ) )
205
+
206
+ line = buffer.slice!( 0..eol_index )
207
+ line = line.strip
208
+ next if line.empty? || !line.start_with?( 'data:' )
209
+ line = line[ 6..-1 ]
210
+
211
+ data = JSON.parse( line, symbolize_names: true )
212
+ if data.is_a?( Hash )
213
+
214
+ data[ :candidates ]&.each do | data_candidate |
215
+
216
+ data_candidate_index = data_candidate[ :index ] || 0
217
+ data_candidate_content = data_candidate[ :content ]
218
+ data_candidate_finish_reason = data_candidate[ :finishReason ]
219
+ choices.fill( { message: { role: 'assistant' } }, choices.size, data_candidate_index + 1 ) \
220
+ if choices.size <= data_candidate_index
221
+ contents = choices[ data_candidate_index ][ :message ][ :contents ] || []
222
+ last_content = contents&.last
223
+
224
+ if data_candidate_content&.include?( :parts )
225
+ data_candidate_content_parts = data_candidate_content[ :parts ]
226
+ data_candidate_content_parts&.each do | data_candidate_content_part |
227
+ if data_candidate_content_part.key?( :text )
228
+ if last_content.nil? || last_content[ :type ] != :text
229
+ contents.push( { type: :text, text: data_candidate_content_part[ :text ] } )
230
+ else
231
+ last_content[ :text ] =
232
+ ( last_content[ :text ] || '' ) + data_candidate_content_part[ :text ]
233
+ end
234
+ end
235
+ end
236
+ end
237
+ choices[ data_candidate_index ][ :message ][ :contents ] = contents
238
+ choices[ data_candidate_index ][ :end_reason ] =
239
+ translate_finish_reason( data_candidate_finish_reason )
240
+ end
241
+
242
+ if usage = data[ :usageMetadata ]
243
+ metrics[ :input_tokens ] = usage[ :promptTokenCount ]
244
+ metrics[ :output_tokens ] = usage[ :candidatesTokenCount ]
245
+ end
246
+
247
+ end
248
+
249
+ end
250
+
251
+ context[ :buffer ] = buffer
252
+ context[ :metrics ] = metrics
253
+ context[ :choices ] = choices
254
+
255
+ [ context, choices.empty? ? nil : { choices: choices.dup } ]
256
+
257
+ end
258
+
259
+ def stream_result_attributes( context )
260
+ #--------------------------------------
261
+
262
+ choices = context[ :choices ]
263
+ metrics = context[ :metrics ]
264
+
265
+ choices = choices.map do | choice |
266
+ { end_reason: choice[ :end_reason ] }
267
+ end
268
+
269
+ { choices: choices, metrics: context[ :metrics ] }
270
+
271
+ end
272
+
273
+ alias_method :stream_result_error_attributes, :chat_result_error_attributes
274
+
275
+ private; def translate_system_message( system_message )
276
+ # -----------------------------------------------------
277
+
278
+ return nil if system_message.nil?
279
+
280
+ text = ''
281
+ system_message[ :contents ].each do | content |
282
+ text += content[ :text ] if content[ :type ] == :text
283
+ end
284
+
285
+ return nil if text.empty?
286
+
287
+ {
288
+ role: 'user',
289
+ parts: [
290
+ { text: text }
291
+ ]
292
+ }
293
+
294
+ end
295
+
296
+ private; def translate_finish_reason( finish_reason )
297
+ # ---------------------------------------------------
298
+ case finish_reason
299
+ when 'STOP'
300
+ :ended
301
+ when 'MAX_TOKENS'
302
+ :token_limit_exceeded
303
+ when 'SAFETY', 'RECITATION', 'BLOCKLIST', 'PROHIBITED_CONTENT', 'SPII'
304
+ :filtered
305
+ else
306
+ nil
307
+ end
308
+ end
309
+
310
+ private; def translate_error_response_status( status )
311
+ case status
312
+ when 400
313
+ [ :invalid_request_error,
314
+ "There was an issue with the format or content of your request." ]
315
+ when 403
316
+ [ :permission_error,
317
+ "Your API key does not have permission to use the specified resource." ]
318
+ when 404
319
+ [ :not_found_error,
320
+ "The requested resource was not found." ]
321
+ when 413
322
+ [ :request_too_large,
323
+ "Request exceeds the maximum allowed number of bytes." ]
324
+ when 422
325
+ [ :invalid_request_error,
326
+ "There was an issue with the format or content of your request." ]
327
+ when 429
328
+ [ :rate_limit_error,
329
+ "Your account has hit a rate limit." ]
330
+ when 500, 502, 503
331
+ [ :api_error,
332
+ "An unexpected error has occurred internal to the providers systems." ]
333
+ when 529
334
+ [ :overloaded_error,
335
+ "The providers server is temporarily overloaded." ]
336
+ else
337
+ [ :unknown_error, "
338
+ An unknown error occurred." ]
339
+ end
340
+ end
341
+
342
+ end
343
+
344
+ end
345
+
346
+ end
@@ -0,0 +1,2 @@
1
+ require_relative '../adapter'
2
+ require_relative 'google/adapter'
@@ -0,0 +1,51 @@
1
+ require_relative 'legacy/adapter'
2
+
3
+ module Intelligence
4
+ module Groq
5
+
6
+ class Adapter < Legacy::Adapter
7
+
8
+ chat_request_uri 'https://api.groq.com/openai/v1/chat/completions'
9
+
10
+ configuration do
11
+ parameter :key, String, required: true
12
+ group :chat_options do
13
+ parameter :frequency_penalty, Float
14
+ parameter :logit_bias
15
+ parameter :logprobs, [ TrueClass, FalseClass ]
16
+ parameter :max_tokens, Integer
17
+ parameter :model, String
18
+ parameter :n, Integer
19
+ # the parallel_tool_calls parameter is only allowed when 'tools' are specified
20
+ parameter :parallel_tool_calls, [ TrueClass, FalseClass ]
21
+ parameter :presence_penalty, Float
22
+ group :response_format do
23
+ # 'text' and 'json_object' are the only supported types; you must also instruct
24
+ # the model to output json
25
+ parameter :type, String
26
+ end
27
+ parameter :seed, Integer
28
+ parameter :stop, String, array: true
29
+ parameter :stream, [ TrueClass, FalseClass ]
30
+ group :stream_options do
31
+ parameter :include_usage, [ TrueClass, FalseClass ]
32
+ end
33
+ parameter :temperature, Float
34
+ group :tool_choice do
35
+ # one of 'auto', 'none' or 'function'
36
+ parameter :type, String
37
+ # the function group is required if you specify a type of 'function'
38
+ group :function do
39
+ parameter :name, String
40
+ end
41
+ end
42
+ parameter :top_logprobs, Integer
43
+ parameter :top_p, Float
44
+ parameter :user, String
45
+ end
46
+ end
47
+
48
+ end
49
+
50
+ end
51
+ end
@@ -0,0 +1,55 @@
1
+ require_relative 'generic/adapter'
2
+
3
+ module Intelligence
4
+ module Hyperbolic
5
+
6
+ class Adapter < Generic::Adapter
7
+
8
+ chat_request_uri "https://api.hyperbolic.xyz/v1/chat/completions"
9
+
10
+ configuration do
11
+ parameter :key, String, required: true
12
+ group :chat_options do
13
+ parameter :model, String
14
+ parameter :temperature, Float
15
+ parameter :top_p, Float
16
+ parameter :n, Integer
17
+ parameter :max_tokens, Integer
18
+ parameter :stop, String, array: true
19
+ parameter :stream, [ TrueClass, FalseClass ]
20
+ parameter :frequency_penalty, Float
21
+ parameter :presence_penalty, Float
22
+ parameter :user, String
23
+ end
24
+ end
25
+
26
+ def chat_result_error_attributes( response )
27
+
28
+ error_type, error_description = translate_error_response_status( response.status )
29
+ result = {
30
+ error_type: error_type.to_s,
31
+ error_description: error_description
32
+ }
33
+ parsed_body = JSON.parse( response.body, symbolize_names: true ) rescue nil
34
+ if parsed_body && parsed_body.respond_to?( :include? )
35
+ if parsed_body.include?( :error )
36
+ result = {
37
+ error_type: error_type.to_s,
38
+ error: parsed_body[ :error ][ :code ] || error_type.to_s,
39
+ error_description: parsed_body[ :error ][ :message ] || error_description
40
+ }
41
+ elsif parsed_body.include?( :detail )
42
+ result[ :error_description ] = parsed_body[ :detail ]
43
+ elsif parsed_body[ :object ] == 'error'
44
+ result[ :error_description ] = parsed_body[ :message ]
45
+ end
46
+ end
47
+
48
+ result
49
+
50
+ end
51
+
52
+ end
53
+
54
+ end
55
+ end
@@ -0,0 +1,13 @@
1
+ require_relative '../generic/adapter'
2
+ require_relative 'chat_methods'
3
+
4
+ module Intelligence
5
+ module Legacy
6
+
7
+ class Adapter < Generic::Adapter
8
+ include ChatMethods
9
+ end
10
+
11
+ end
12
+ end
13
+
@@ -0,0 +1,37 @@
1
+ module Intelligence
2
+ module Legacy
3
+ module ChatMethods
4
+
5
+ def chat_request_body( conversation, options = {} )
6
+ result = self.chat_options.merge( options ).compact
7
+ result[ :messages ] = []
8
+
9
+ system_message = system_message_to_s( conversation[ :system_message ] )
10
+ result[ :messages ] << { role: 'system', content: system_message } if system_message
11
+
12
+ conversation[ :messages ]&.each do | message |
13
+ result[ :messages ] << chat_request_message_attributes( message )
14
+ end
15
+
16
+ JSON.generate( result )
17
+ end
18
+
19
+ def chat_request_message_attributes( message )
20
+ result_message = { role: message[ :role ] }
21
+ result_message_content = ""
22
+
23
+ message[ :contents ]&.each do | content |
24
+ case content[ :type ]
25
+ when :text
26
+ result_message_content += content[ :text ]
27
+ end
28
+ end
29
+
30
+ result_message[ :content ] = result_message_content
31
+ result_message
32
+ end
33
+
34
+ end
35
+ end
36
+ end
37
+
@@ -0,0 +1,75 @@
1
+ require_relative 'chat_methods'
2
+
3
+ module Intelligence
4
+ module OpenAi
5
+ class Adapter < Adapter::Base
6
+
7
+ configuration do
8
+
9
+ # normalized properties for all endpoints
10
+ parameter :key, String, required: true
11
+
12
+ # openai properties for all endpoints
13
+ parameter :organization
14
+ parameter :project
15
+
16
+ # properties for generative text endpoints
17
+ group :chat_options do
18
+
19
+ # normalized properties for openai generative text endpoint
20
+ parameter :model, String, required: true
21
+ parameter :n, Integer
22
+ parameter :max_tokens, Integer, as: :max_completion_tokens
23
+ parameter :temperature, Float
24
+ parameter :top_p, Float
25
+ parameter :seed, Integer
26
+ parameter :stop, String, array: true
27
+ parameter :stream, [ TrueClass, FalseClass ]
28
+
29
+ parameter :frequency_penalty, Float
30
+ parameter :presence_penalty, Float
31
+
32
+ # openai variant of normalized properties for openai generative text endpoints
33
+ parameter :max_completion_tokens, Integer
34
+
35
+ # openai properties for openai generative text endpoint
36
+ parameter :logit_bias
37
+ parameter :logprobs, [ TrueClass, FalseClass ]
38
+ parameter :parallel_tool_calls, [ TrueClass, FalseClass ]
39
+ group :response_format do
40
+ # 'text' and 'json_schema' are the only supported types
41
+ parameter :type, String
42
+ parameter :json_schema
43
+ end
44
+ parameter :service_tier, String
45
+ group :stream_options do
46
+ parameter :include_usage, [ TrueClass, FalseClass ]
47
+ end
48
+ parameter :tool_choice
49
+ # the parallel_tool_calls parameter is only allowed when 'tools' are specified
50
+ parameter :top_logprobs, Integer
51
+ parameter :user
52
+
53
+ end
54
+
55
+ end
56
+
57
+ attr_reader :key
58
+ attr_reader :organization
59
+ attr_reader :project
60
+ attr_reader :chat_options
61
+
62
+
63
+ def initialize( attributes = nil, &block )
64
+ configuration = self.class.configure( attributes, &block )
65
+ @key = configuration[ :key ]
66
+ @organization = configuration[ :organization ]
67
+ @project = configuration[ :project ]
68
+ @chat_options = configuration[ :chat_options ] || {}
69
+ end
70
+
71
+ include ChatMethods
72
+
73
+ end
74
+ end
75
+ end