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,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