intelligence 0.6.0 → 0.7.1

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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +555 -0
  3. data/intelligence.gemspec +1 -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} +8 -125
  11. data/lib/intelligence/adapters/cerebras.rb +17 -17
  12. data/lib/intelligence/adapters/generic/chat_methods.rb +12 -5
  13. data/lib/intelligence/adapters/generic.rb +1 -1
  14. data/lib/intelligence/adapters/google/adapter.rb +33 -22
  15. data/lib/intelligence/adapters/google/chat_request_methods.rb +233 -0
  16. data/lib/intelligence/adapters/google/chat_response_methods.rb +236 -0
  17. data/lib/intelligence/adapters/groq.rb +27 -28
  18. data/lib/intelligence/adapters/hyperbolic.rb +13 -13
  19. data/lib/intelligence/adapters/legacy/chat_methods.rb +1 -2
  20. data/lib/intelligence/adapters/mistral.rb +18 -18
  21. data/lib/intelligence/adapters/open_ai/adapter.rb +39 -32
  22. data/lib/intelligence/adapters/open_ai/chat_request_methods.rb +186 -0
  23. data/lib/intelligence/adapters/open_ai/{chat_methods.rb → chat_response_methods.rb} +60 -162
  24. data/lib/intelligence/adapters/open_ai.rb +1 -1
  25. data/lib/intelligence/adapters/open_router.rb +18 -18
  26. data/lib/intelligence/adapters/samba_nova.rb +13 -13
  27. data/lib/intelligence/adapters/together_ai.rb +21 -19
  28. data/lib/intelligence/conversation.rb +11 -10
  29. data/lib/intelligence/message.rb +44 -28
  30. data/lib/intelligence/message_content/base.rb +2 -9
  31. data/lib/intelligence/message_content/binary.rb +3 -3
  32. data/lib/intelligence/message_content/file.rb +3 -3
  33. data/lib/intelligence/message_content/text.rb +2 -2
  34. data/lib/intelligence/message_content/tool_call.rb +8 -4
  35. data/lib/intelligence/message_content/tool_result.rb +11 -6
  36. data/lib/intelligence/tool.rb +139 -0
  37. data/lib/intelligence/version.rb +1 -1
  38. data/lib/intelligence.rb +2 -1
  39. metadata +15 -10
  40. data/lib/intelligence/adapter/class_methods/construction.rb +0 -17
  41. data/lib/intelligence/adapter/module_methods/construction.rb +0 -43
  42. data/lib/intelligence/adapters/google/chat_methods.rb +0 -393
@@ -0,0 +1,233 @@
1
+ require 'uri'
2
+
3
+ module Intelligence
4
+ module Google
5
+ module ChatRequestMethods
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.merge( build_options( options ) )
31
+
32
+ key = options[ :key ]
33
+ gc = options[ :generationConfig ] || {}
34
+ model = gc[ :model ]
35
+ stream = gc.key?( :stream ) ? gc[ :stream ] : false
36
+
37
+ raise ArgumentError.new( "A Google API key is required to build a Google chat request." ) \
38
+ if key.nil?
39
+ raise ArgumentError.new( "A Google model is required to build a Google chat request." ) \
40
+ if model.nil?
41
+
42
+ uri = URI( GENERATIVE_LANGUAGE_URI )
43
+ path = File.join( uri.path, model )
44
+ path += stream ? ':streamGenerateContent' : ':generateContent'
45
+ uri.path = path
46
+ query = { key: key }
47
+ query[ :alt ] = 'sse' if stream
48
+ uri.query = URI.encode_www_form( query )
49
+
50
+ uri.to_s
51
+ end
52
+
53
+ def chat_request_headers( options = {} )
54
+ { 'Content-Type' => 'application/json' }
55
+ end
56
+
57
+ def chat_request_body( conversation, options = {} )
58
+ options = @options.merge( build_options( options ) )
59
+
60
+ gc = options[ :generationConfig ]
61
+ # discard properties not part of the google generationConfig schema
62
+ gc.delete( :model )
63
+ gc.delete( :stream )
64
+
65
+ # googlify tool configuration
66
+ if tool_config = gc.delete( :tool_config )
67
+ mode = tool_config[ :function_calling_config ]&.[]( :mode )
68
+ tool_config[ :function_calling_config ][ :mode ] = mode.to_s.upcase if mode
69
+ end
70
+
71
+ result = {}
72
+ result[ :generationConfig ] = gc
73
+ result[ :tool_config ] = tool_config if tool_config
74
+
75
+ # construct the system prompt in the form of the google schema
76
+ system_instructions = to_google_system_message( conversation[ :system_message ] )
77
+ result[ :systemInstruction ] = system_instructions if system_instructions
78
+
79
+ result[ :contents ] = []
80
+ conversation[ :messages ]&.each do | message |
81
+
82
+ result_message = { role: message[ :role ] == :user ? 'user' : 'model' }
83
+ result_message_parts = []
84
+
85
+ message[ :contents ]&.each do | content |
86
+ case content[ :type ]
87
+ when :text
88
+ result_message_parts << { text: content[ :text ] }
89
+ when :binary
90
+ content_type = content[ :content_type ]
91
+ bytes = content[ :bytes ]
92
+ if content_type && bytes
93
+ mime_type = MIME::Types[ content_type ].first
94
+ if SUPPORTED_BINARY_MEDIA_TYPES.include?( mime_type&.media_type ) ||
95
+ SUPPORTED_BINARY_CONTENT_TYPES.include?( content_type )
96
+ result_message_parts << {
97
+ inline_data: {
98
+ mime_type: content_type,
99
+ data: Base64.strict_encode64( bytes )
100
+ }
101
+ }
102
+ else
103
+ raise UnsupportedContentError.new(
104
+ :google,
105
+ "does not support #{content_type} content type"
106
+ )
107
+ end
108
+ else
109
+ raise UnsupportedContentError.new(
110
+ :google,
111
+ 'requires binary content to include content type and ( packed ) bytes'
112
+ )
113
+ end
114
+ when :file
115
+ content_type = content[ :content_type ]
116
+ uri = content[ :uri ]
117
+ if content_type && uri
118
+ mime_type = MIME::Types[ content_type ].first
119
+ if SUPPORTED_FILE_MEDIA_TYPES.include?( mime_type&.media_type ) ||
120
+ SUPPORTED_FILE_CONTENT_TYPES.include?( content_type )
121
+ result_message_parts << {
122
+ file_data: {
123
+ mime_type: content_type,
124
+ file_uri: uri
125
+ }
126
+ }
127
+ else
128
+ raise UnsupportedContentError.new(
129
+ :google,
130
+ "does not support #{content_type} content type"
131
+ )
132
+ end
133
+ else
134
+ raise UnsupportedContentError.new(
135
+ :google,
136
+ 'requires file content to include content type and uri'
137
+ )
138
+ end
139
+ when :tool_call
140
+ result_message_parts << {
141
+ functionCall: {
142
+ name: content[ :tool_name ],
143
+ args: content[ :tool_parameters ]
144
+ }
145
+ }
146
+ when :tool_result
147
+ result_message_parts << {
148
+ functionResponse: {
149
+ name: content[ :tool_name ],
150
+ response: {
151
+ name: content[ :tool_name ],
152
+ content: content[ :tool_result ]
153
+ }
154
+ }
155
+ }
156
+ else
157
+ raise InvalidContentError.new( :google )
158
+ end
159
+ end
160
+
161
+ result_message[ :parts ] = result_message_parts
162
+ result[ :contents ] << result_message
163
+
164
+ end
165
+
166
+ tools_attributes = to_google_tools( conversation[ :tools ] )
167
+ result[ :tools ] = tools_attributes if tools_attributes && tools_attributes.length > 0
168
+
169
+ JSON.generate( result )
170
+ end
171
+
172
+ private
173
+
174
+ def to_google_system_message( system_message )
175
+ return nil if system_message.nil?
176
+
177
+ text = ''
178
+ system_message[ :contents ].each do | content |
179
+ text += content[ :text ] if content[ :type ] == :text
180
+ end
181
+
182
+ return nil if text.empty?
183
+
184
+ {
185
+ role: 'user',
186
+ parts: [
187
+ { text: text }
188
+ ]
189
+ }
190
+ end
191
+
192
+ def to_google_tools( tools )
193
+ properties_array_to_object = lambda do | properties |
194
+ return nil unless properties&.any?
195
+ object = {}
196
+ required = []
197
+ properties.each do | property |
198
+ name = property.delete( :name )
199
+ required << name if property.delete( :required )
200
+ if property[ :properties ]&.any?
201
+ property_properties, property_required =
202
+ properties_array_to_object.call( property[ :properties ] )
203
+ property[ :properties ] = property_properties
204
+ property[ :required ] = property_required if property_required.any?
205
+ end
206
+ object[ name ] = property
207
+ end
208
+ [ object, required.compact ]
209
+ end
210
+
211
+ return [ { function_declarations: tools&.map { | tool |
212
+ function = {
213
+ name: tool[ :name ],
214
+ description: tool[ :description ],
215
+ }
216
+ if tool[ :properties ]&.any?
217
+ properties_object, properties_required =
218
+ properties_array_to_object.call( tool[ :properties ] )
219
+ function[ :parameters ] = {
220
+ type: 'object',
221
+ properties: properties_object
222
+ }
223
+ function[ :parameters ][ :required ] = properties_required if properties_required.any?
224
+ end
225
+ function
226
+ } } ]
227
+ end
228
+
229
+ end
230
+
231
+ end
232
+
233
+ end
@@ -0,0 +1,236 @@
1
+ require 'uri'
2
+
3
+ module Intelligence
4
+ module Google
5
+ module ChatResponseMethods
6
+
7
+ def chat_result_attributes( response )
8
+
9
+ return nil unless response.success?
10
+
11
+ response_json = JSON.parse( response.body, symbolize_names: true ) rescue nil
12
+ return nil if response_json.nil? || response_json[ :candidates ].nil?
13
+
14
+ result = {}
15
+ result[ :choices ] = []
16
+
17
+ response_json[ :candidates ]&.each do | response_choice |
18
+
19
+ end_reason = translate_finish_reason( response_choice[ :finishReason ] )
20
+
21
+ role = nil
22
+ contents = []
23
+
24
+ response_content = response_choice[ :content ]
25
+ if response_content
26
+ role = ( response_content[ :role ] == 'model' ) ? 'assistant' : 'user'
27
+ contents = []
28
+ response_content[ :parts ]&.each do | response_content_part |
29
+ if response_content_part.key?( :text )
30
+ contents.push( {
31
+ type: 'text', text: response_content_part[ :text ]
32
+ } )
33
+ elsif function_call = response_content_part[ :functionCall ]
34
+ contents.push( {
35
+ type: :tool_call,
36
+ tool_name: function_call[ :name ],
37
+ tool_parameters: function_call[ :args ]
38
+ } )
39
+ # google does not indicate there is tool call in the stop reason so
40
+ # we will synthesize this end reason
41
+ end_reason = :tool_called if end_reason == :ended
42
+ end
43
+ end
44
+ end
45
+
46
+ result_message = nil
47
+ if role
48
+ result_message = { role: role }
49
+ result_message[ :contents ] = contents
50
+ end
51
+
52
+ result[ :choices ].push( { end_reason: end_reason, message: result_message } )
53
+
54
+ end
55
+
56
+ metrics_json = response_json[ :usageMetadata ]
57
+ unless metrics_json.nil?
58
+
59
+ metrics = {}
60
+ metrics[ :input_tokens ] = metrics_json[ :promptTokenCount ]
61
+ metrics[ :output_tokens ] = metrics_json[ :candidatesTokenCount ]
62
+ metrics = metrics.compact
63
+
64
+ result[ :metrics ] = metrics unless metrics.empty?
65
+
66
+ end
67
+
68
+ result
69
+
70
+ end
71
+
72
+ def chat_result_error_attributes( response )
73
+
74
+ error_type, error_description = translate_error_response_status( response.status )
75
+ result = { error_type: error_type.to_s, error_description: error_description }
76
+
77
+ response_body = JSON.parse( response.body, symbolize_names: true ) rescue nil
78
+ if response_body && response_body[ :error ]
79
+ error_details_reason = response_body[ :error ][ :details ]&.first&.[]( :reason )
80
+ # a special case for authentication
81
+ error_type = :authentication_error if error_details_reason == 'API_KEY_INVALID'
82
+ result = {
83
+ error_type: error_type.to_s,
84
+ error: error_details_reason || response_body[ :error ][ :status ] || error_type,
85
+ error_description: response_body[ :error ][ :message ]
86
+ }
87
+ end
88
+ result
89
+
90
+ end
91
+
92
+ def stream_result_chunk_attributes( context, chunk )
93
+
94
+ context ||= {}
95
+ buffer = context[ :buffer ] || ''
96
+ metrics = context[ :metrics ] || {
97
+ input_tokens: 0,
98
+ output_tokens: 0
99
+ }
100
+ choices = context[ :choices ] || Array.new( 1 , { message: {} } )
101
+
102
+ choices.each do | choice |
103
+ choice[ :message ][ :contents ] = choice[ :message ][ :contents ]&.map do | content |
104
+ case content[ :type ]
105
+ when :text
106
+ content[ :text ] = ''
107
+ else
108
+ content.clear
109
+ end
110
+ content
111
+ end
112
+ end
113
+
114
+ buffer += chunk
115
+ while ( eol_index = buffer.index( "\n" ) )
116
+
117
+ line = buffer.slice!( 0..eol_index )
118
+ line = line.strip
119
+ next if line.empty? || !line.start_with?( 'data:' )
120
+ line = line[ 6..-1 ]
121
+
122
+ data = JSON.parse( line, symbolize_names: true )
123
+ if data.is_a?( Hash )
124
+
125
+ data[ :candidates ]&.each do | data_candidate |
126
+
127
+ data_candidate_index = data_candidate[ :index ] || 0
128
+ data_candidate_content = data_candidate[ :content ]
129
+ data_candidate_finish_reason = data_candidate[ :finishReason ]
130
+ choices.fill( { message: { role: 'assistant' } }, choices.size, data_candidate_index + 1 ) \
131
+ if choices.size <= data_candidate_index
132
+ contents = choices[ data_candidate_index ][ :message ][ :contents ] || []
133
+ last_content = contents&.last
134
+
135
+ if data_candidate_content&.include?( :parts )
136
+ data_candidate_content_parts = data_candidate_content[ :parts ]
137
+ data_candidate_content_parts&.each do | data_candidate_content_part |
138
+ if data_candidate_content_part.key?( :text )
139
+ if last_content.nil? || last_content[ :type ] != :text
140
+ contents.push( { type: :text, text: data_candidate_content_part[ :text ] } )
141
+ else
142
+ last_content[ :text ] =
143
+ ( last_content[ :text ] || '' ) + data_candidate_content_part[ :text ]
144
+ end
145
+ end
146
+ end
147
+ end
148
+ choices[ data_candidate_index ][ :message ][ :contents ] = contents
149
+ choices[ data_candidate_index ][ :end_reason ] =
150
+ translate_finish_reason( data_candidate_finish_reason )
151
+ end
152
+
153
+ if usage = data[ :usageMetadata ]
154
+ metrics[ :input_tokens ] = usage[ :promptTokenCount ]
155
+ metrics[ :output_tokens ] = usage[ :candidatesTokenCount ]
156
+ end
157
+
158
+ end
159
+
160
+ end
161
+
162
+ context[ :buffer ] = buffer
163
+ context[ :metrics ] = metrics
164
+ context[ :choices ] = choices
165
+
166
+ [ context, choices.empty? ? nil : { choices: choices.dup } ]
167
+
168
+ end
169
+
170
+ def stream_result_attributes( context )
171
+
172
+ choices = context[ :choices ]
173
+ metrics = context[ :metrics ]
174
+
175
+ choices = choices.map do | choice |
176
+ { end_reason: choice[ :end_reason ] }
177
+ end
178
+
179
+ { choices: choices, metrics: context[ :metrics ] }
180
+
181
+ end
182
+
183
+ alias_method :stream_result_error_attributes, :chat_result_error_attributes
184
+
185
+ private
186
+
187
+ def translate_finish_reason( finish_reason )
188
+ case finish_reason
189
+ when 'STOP'
190
+ :ended
191
+ when 'MAX_TOKENS'
192
+ :token_limit_exceeded
193
+ when 'SAFETY', 'RECITATION', 'BLOCKLIST', 'PROHIBITED_CONTENT', 'SPII'
194
+ :filtered
195
+ else
196
+ nil
197
+ end
198
+ end
199
+
200
+ def translate_error_response_status( status )
201
+ case status
202
+ when 400
203
+ [ :invalid_request_error,
204
+ "There was an issue with the format or content of your request." ]
205
+ when 403
206
+ [ :permission_error,
207
+ "Your API key does not have permission to use the specified resource." ]
208
+ when 404
209
+ [ :not_found_error,
210
+ "The requested resource was not found." ]
211
+ when 413
212
+ [ :request_too_large,
213
+ "Request exceeds the maximum allowed number of bytes." ]
214
+ when 422
215
+ [ :invalid_request_error,
216
+ "There was an issue with the format or content of your request." ]
217
+ when 429
218
+ [ :rate_limit_error,
219
+ "Your account has hit a rate limit." ]
220
+ when 500, 502, 503
221
+ [ :api_error,
222
+ "An unexpected error has occurred internal to the providers systems." ]
223
+ when 529
224
+ [ :overloaded_error,
225
+ "The providers server is temporarily overloaded." ]
226
+ else
227
+ [ :unknown_error, "
228
+ An unknown error occurred." ]
229
+ end
230
+ end
231
+
232
+ end
233
+
234
+ end
235
+
236
+ end
@@ -7,41 +7,40 @@ module Intelligence
7
7
 
8
8
  chat_request_uri 'https://api.groq.com/openai/v1/chat/completions'
9
9
 
10
- configuration do
11
- parameter :key, String
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
10
+ schema do
11
+ key String
12
+ chat_options do
13
+ frequency_penalty Float
14
+ logit_bias
15
+ logprobs [ TrueClass, FalseClass ]
16
+ max_tokens Integer
17
+ model String
18
+ # the parallel_tool_calls is only allowed when 'tools' are specified
19
+ parallel_tool_calls [ TrueClass, FalseClass ]
20
+ presence_penalty Float
21
+ response_format do
23
22
  # 'text' and 'json_object' are the only supported types; you must also instruct
24
23
  # the model to output json
25
- parameter :type, String
24
+ type Symbol, in: [ :text, :json_object ]
26
25
  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 ]
26
+ seed Integer
27
+ stop String, array: true
28
+ stream [ TrueClass, FalseClass ]
29
+ stream_options do
30
+ include_usage [ TrueClass, FalseClass ]
32
31
  end
33
- parameter :temperature, Float
34
- group :tool_choice do
32
+ temperature Float
33
+ tool_choice do
35
34
  # 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
35
+ type Symbol, in: [ :auto, :none, :function ]
36
+ # the function parameters is required if you specify a type of 'function'
37
+ function do
38
+ name String
40
39
  end
41
40
  end
42
- parameter :top_logprobs, Integer
43
- parameter :top_p, Float
44
- parameter :user, String
41
+ top_logprobs Integer
42
+ top_p Float
43
+ user String
45
44
  end
46
45
  end
47
46
 
@@ -7,19 +7,19 @@ module Intelligence
7
7
 
8
8
  chat_request_uri "https://api.hyperbolic.xyz/v1/chat/completions"
9
9
 
10
- configuration do
11
- parameter :key, String
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
10
+ schema do
11
+ key String
12
+ chat_options do
13
+ model String
14
+ temperature Float
15
+ top_p Float
16
+ n Integer
17
+ max_tokens Integer
18
+ stop String, array: true
19
+ stream [ TrueClass, FalseClass ]
20
+ frequency_penalty Float
21
+ presence_penalty Float
22
+ user String
23
23
  end
24
24
  end
25
25
 
@@ -3,8 +3,7 @@ module Intelligence
3
3
  module ChatMethods
4
4
 
5
5
  def chat_request_body( conversation, options = {} )
6
- options = options ? self.class.configure( options ) : {}
7
- options = @options.merge( options )
6
+ options = @options.merge( to_options( options ) )
8
7
 
9
8
  result = options[ :chat_options ]&.compact || {}
10
9
  result[ :messages ] = []
@@ -7,26 +7,26 @@ module Intelligence
7
7
 
8
8
  chat_request_uri "https://api.mistral.ai/v1/chat/completions"
9
9
 
10
- configuration do
11
- parameter :key, String
12
- group :chat_options do
13
- parameter :model, String
14
- parameter :temperature, Float
15
- parameter :top_p, Float
16
- parameter :max_tokens, Integer
17
- parameter :min_tokens, Integer
18
- parameter :seed, Integer, as: :random_seed
19
- parameter :stop, String, array: true
20
- parameter :stream, [ TrueClass, FalseClass ]
10
+ schema do
11
+ key String
12
+ chat_options do
13
+ model String
14
+ temperature Float
15
+ top_p Float
16
+ max_tokens Integer
17
+ min_tokens Integer
18
+ seed Integer, as: :random_seed
19
+ stop String, array: true
20
+ stream [ TrueClass, FalseClass ]
21
21
 
22
- parameter :random_seed, Integer
23
- group :response_format do
24
- parameter :type, String
22
+ random_seed Integer
23
+ response_format do
24
+ type String
25
25
  end
26
- group :tool_choice do
27
- parameter :type, String
28
- group :function do
29
- parameter :name, String
26
+ tool_choice do
27
+ type String
28
+ function do
29
+ name String
30
30
  end
31
31
  end
32
32
  end