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,331 @@
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 = {} )
24
+ result = {}
25
+
26
+ key = options[ :key ] || self.key
27
+
28
+ raise ArgumentError.new( "An API key is required to build a chat request." ) \
29
+ if key.nil?
30
+
31
+ result[ 'Content-Type' ] = 'application/json'
32
+ result[ 'Authorization' ] = "Bearer #{key}"
33
+
34
+ result
35
+ end
36
+
37
+ def chat_request_body( conversation, options = {} )
38
+ result = self.chat_options.merge( options ).compact
39
+ result[ :messages ] = []
40
+
41
+ system_message = system_message_to_s( conversation[ :system_message ] )
42
+ result[ :messages ] << { role: 'system', content: system_message } if system_message
43
+
44
+ conversation[ :messages ]&.each do | message |
45
+ result[ :messages ] << chat_request_message_attributes( message )
46
+ end
47
+
48
+ JSON.generate( result )
49
+ end
50
+
51
+ def chat_request_message_attributes( message )
52
+ result_message = { role: message[ :role ] }
53
+ result_message_content = []
54
+ message[ :contents ]&.each do | content |
55
+ case content[ :type ]
56
+ when :text
57
+ result_message_content << { type: 'text', text: content[ :text ] }
58
+ when :binary
59
+ content_type = content[ :content_type ]
60
+ bytes = content[ :bytes ]
61
+ if content_type && bytes
62
+ mime_type = MIME::Types[ content_type ].first
63
+ if mime_type&.media_type == 'image'
64
+ result_message_content << {
65
+ type: 'image_url',
66
+ image_url: {
67
+ url: "data:#{content_type};base64,#{Base64.strict_encode64( bytes )}".freeze
68
+ }
69
+ }
70
+ else
71
+ raise UnsupportedContentError.new(
72
+ :generic,
73
+ 'only support content of type image/*'
74
+ )
75
+ end
76
+ else
77
+ raise UnsupportedContentError.new(
78
+ :generic,
79
+ 'requires binary content to include content type and ( packed ) bytes'
80
+ )
81
+ end
82
+ end
83
+ end
84
+ result_message[ :content ] = result_message_content
85
+ result_message
86
+ end
87
+
88
+ def chat_result_attributes( response )
89
+
90
+ return nil unless response.success?
91
+ response_json = JSON.parse( response.body, symbolize_names: true ) rescue nil
92
+ return nil if response_json.nil? || response_json[ :choices ].nil?
93
+
94
+ result = {}
95
+ result[ :choices ] = []
96
+
97
+ ( response_json[ :choices ] || [] ).each do | json_choice |
98
+ json_message = json_choice[ :message ]
99
+ result[ :choices ].push(
100
+ {
101
+ end_reason: translate_end_result( json_choice[ :finish_reason ] ),
102
+ message: {
103
+ role: json_message[ :role ],
104
+ contents: [ { type: 'text', text: json_message[ :content ] } ]
105
+ }
106
+ }
107
+ )
108
+ end
109
+
110
+ metrics_json = response_json[ :usage ]
111
+ unless metrics_json.nil?
112
+
113
+ metrics = {}
114
+ metrics[ :input_tokens ] = metrics_json[ :prompt_tokens ]
115
+ metrics[ :output_tokens ] = metrics_json[ :completion_tokens ]
116
+ metrics = metrics.compact
117
+
118
+ result[ :metrics ] = metrics unless metrics.empty?
119
+
120
+ end
121
+
122
+ result
123
+
124
+ end
125
+
126
+ def chat_result_error_attributes( response )
127
+
128
+ error_type, error_description = translate_error_response_status( response.status )
129
+ result = {
130
+ error_type: error_type.to_s,
131
+ error_description: error_description
132
+ }
133
+
134
+ parsed_body = JSON.parse( response.body, symbolize_names: true ) rescue nil
135
+ if parsed_body && parsed_body.respond_to?( :include? ) && parsed_body.include?( :error )
136
+ result = {
137
+ error_type: error_type.to_s,
138
+ error: parsed_body[ :error ][ :code ] || error_type.to_s,
139
+ error_description: parsed_body[ :error ][ :message ] || error_description
140
+ }
141
+ end
142
+
143
+ result
144
+
145
+ end
146
+
147
+ def stream_result_chunk_attributes( context, chunk )
148
+
149
+ context ||= {}
150
+ buffer = context[ :buffer ] || ''
151
+ metrics = context[ :metrics ] || {
152
+ input_tokens: 0,
153
+ output_tokens: 0
154
+ }
155
+ choices = context[ :choices ] || Array.new( 1 , { message: {} } )
156
+
157
+ choices.each do | choice |
158
+ choice[ :message ][ :contents ] = choice[ :message ][ :contents ]&.map do | content |
159
+ case content[ :type ]
160
+ when :text
161
+ content[ :text ] = ''
162
+ when :tool_call
163
+ content[ :tool_parameters ] = ''
164
+ else
165
+ content.clear
166
+ end
167
+ content
168
+ end
169
+ end
170
+
171
+ buffer += chunk
172
+ while ( eol_index = buffer.index( "\n" ) )
173
+
174
+ line = buffer.slice!( 0..eol_index )
175
+ line = line.strip
176
+ next if line.empty? || !line.start_with?( 'data:' )
177
+ line = line[ 6..-1 ]
178
+
179
+ next if line.end_with?( '[DONE]' )
180
+ data = JSON.parse( line )
181
+
182
+ if data.is_a?( Hash )
183
+
184
+ data[ 'choices' ]&.each do | data_choice |
185
+
186
+ data_choice_index = data_choice[ 'index' ]
187
+ data_choice_delta = data_choice[ 'delta' ]
188
+ data_choice_finish_reason = data_choice[ 'finish_reason' ]
189
+
190
+ choices.fill( { message: {} }, choices.size, data_choice_index + 1 ) \
191
+ if choices.size <= data_choice_index
192
+ contents = choices[ data_choice_index ][ :message ][ :contents ] || []
193
+ last_content = contents&.last
194
+
195
+ if data_choice_delta.include?( 'content' )
196
+ data_choice_content = data_choice_delta[ 'content' ] || ''
197
+ if last_content.nil? || last_content[ :type ] == :tool_call
198
+ contents.push( { type: :text, text: data_choice_content } )
199
+ elsif last_content[ :type ].nil?
200
+ last_content[ :type ] = :text
201
+ last_content[ :text ] = data_choice_content
202
+ elsif last_content[ :type ] == :text
203
+ last_content[ :text ] = ( last_content[ :text ] || '' ) + data_choice_content
204
+ end
205
+ elsif data_choice_delta.include?( 'function_call' )
206
+ data_choice_tool_call = data_choice_delta[ 'function_call' ]
207
+ data_choice_tool_name = data_choice_tool_call[ 'name' ]
208
+ data_choice_tool_parameters = data_choice_tool_call[ 'arguments' ]
209
+ if last_content.nil? || last_content[ :type ] == :text
210
+ contents.push( {
211
+ type: :tool_call,
212
+ tool_name: data_choice_tool_name,
213
+ tool_parameters: data_choice_tool_parameters
214
+ } )
215
+ elsif last_content[ :type ].nil?
216
+ last_content[ :type ] = :tool_call
217
+ last_content[ :tool_name ] = data_choice_tool_name \
218
+ if data_choice_tool_name.present?
219
+ last_content[ :tool_parameters ] = tool_parameters
220
+ elsif last_content[ :type ] == :tool_call
221
+ last_content[ :tool_parameters ] =
222
+ ( last_content[ :tool_parameters ] || '' ) + data_choice_tool_parameters
223
+ end
224
+ end
225
+ choices[ data_choice_index ][ :message ][ :contents ] = contents
226
+ choices[ data_choice_index ][ :end_reason ] =
227
+ translate_end_result( data_choice_finish_reason )
228
+ end
229
+
230
+ if usage = data[ 'usage' ]
231
+ # note: A number of providers will resend the input tokens as part of their usage
232
+ # payload.
233
+ metrics[ :input_tokens ] = usage[ 'prompt_tokens' ] \
234
+ if usage.include?( 'prompt_tokens' )
235
+ metrics[ :output_tokens ] += usage[ 'completion_tokens' ] \
236
+ if usage.include?( 'completion_tokens' )
237
+ end
238
+
239
+ end
240
+
241
+ end
242
+
243
+ context[ :buffer ] = buffer
244
+ context[ :metrics ] = metrics
245
+ context[ :choices ] = choices
246
+
247
+ [ context, choices.empty? ? nil : { choices: choices.dup } ]
248
+
249
+ end
250
+
251
+ def stream_result_attributes( context )
252
+
253
+ choices = context[ :choices ]
254
+ metrics = context[ :metrics ]
255
+
256
+ choices = choices.map do | choice |
257
+ { end_reason: choice[ :end_reason ] }
258
+ end
259
+
260
+ { choices: choices, metrics: context[ :metrics ] }
261
+
262
+ end
263
+
264
+ alias_method :stream_result_error_attributes, :chat_result_error_attributes
265
+
266
+ def translate_end_result( end_result )
267
+ case end_result
268
+ when 'stop'
269
+ :ended
270
+ when 'length'
271
+ :token_limit_exceeded
272
+ when 'function_call'
273
+ :tool_called
274
+ when 'content_filter'
275
+ :filtered
276
+ else
277
+ nil
278
+ end
279
+ end
280
+
281
+ def translate_error_response_status( status )
282
+ case status
283
+ when 400
284
+ [ :invalid_request_error,
285
+ "There was an issue with the format or content of your request." ]
286
+ when 401
287
+ [ :authentication_error,
288
+ "There's an issue with your API key." ]
289
+ when 403
290
+ [ :permission_error,
291
+ "Your API key does not have permission to use the specified resource." ]
292
+ when 404
293
+ [ :not_found_error,
294
+ "The requested resource was not found." ]
295
+ when 413
296
+ [ :request_too_large,
297
+ "Request exceeds the maximum allowed number of bytes." ]
298
+ when 422
299
+ [ :invalid_request_error,
300
+ "There was an issue with the format or content of your request." ]
301
+ when 429
302
+ [ :rate_limit_error,
303
+ "Your account has hit a rate limit." ]
304
+ when 500, 502, 503
305
+ [ :api_error,
306
+ "An unexpected error has occurred internal to the providers systems." ]
307
+ when 529
308
+ [ :overloaded_error,
309
+ "The providers server is temporarily overloaded." ]
310
+ else
311
+ [ :unknown_error, "
312
+ An unknown error occurred." ]
313
+ end
314
+ end
315
+
316
+ private; def system_message_to_s( system_message )
317
+
318
+ return nil if system_message.nil?
319
+
320
+ result = ''
321
+ system_message[ :contents ].each do | content |
322
+ result += content[ :text ] if content[ :type ] == :text
323
+ end
324
+
325
+ result.empty? ? nil : result
326
+
327
+ end
328
+
329
+ end
330
+ end
331
+ end
@@ -0,0 +1,2 @@
1
+ require_relative '../adapter'
2
+ require_relative 'generic/adapter'
@@ -0,0 +1,60 @@
1
+ require_relative 'chat_methods'
2
+
3
+ module Intelligence
4
+ module Google
5
+ class Adapter < Adapter::Base
6
+
7
+ configuration do
8
+
9
+ # normalized properties for all endpoints
10
+ parameter :key, String, required: true
11
+
12
+ group :chat_options, as: :generationConfig do
13
+
14
+ # normalized properties for google generative text endpoint
15
+ parameter :model, String, required: true
16
+ parameter :max_tokens, Integer, as: :maxOutputTokens
17
+ parameter :n, Integer, as: :candidateCount
18
+ parameter :temperature, Float
19
+ parameter :top_k, Integer, as: :topK
20
+ parameter :top_p, Float, as: :topP
21
+ parameter :seed, Integer
22
+ parameter :stop, String, array: true, as: :stopSequences
23
+ parameter :stream, [ TrueClass, FalseClass ]
24
+
25
+ parameter :frequency_penalty, Float, as: :frequencyPenalty
26
+ parameter :presence_penalty, Float, as: :presencePenalty
27
+
28
+ # google variant of normalized properties for google generative text endpoints
29
+ parameter :candidate_count, Integer, as: :candidateCount
30
+ parameter :max_output_tokens, Integer, as: :maxOutputTokens
31
+ parameter :stop_sequences, String, array: true, as: :stopSequences
32
+
33
+ # google specific properties for google generative text endpoints
34
+ parameter :response_mime_type, String, as: :responseMimeType
35
+ parameter :response_schema, as: :responseSchema
36
+
37
+ end
38
+
39
+ end
40
+
41
+ attr_reader :key
42
+ attr_reader :model
43
+ attr_reader :stream
44
+ attr_reader :chat_options
45
+
46
+ def initialize( attributes = nil, &block )
47
+ configuration = self.class.configure( attributes, &block ).to_h
48
+ @key = configuration.delete( :key )
49
+ @model = configuration[ :generationConfig ]&.delete( :model )
50
+ @stream = configuration[ :generationConfig ]&.delete( :stream ) || false
51
+ @chat_options = configuration[ :generationConfig ] || {}
52
+ end
53
+
54
+ include ChatMethods
55
+
56
+ end
57
+
58
+ end
59
+
60
+ end