intelligence 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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