intelligence 0.6.0 → 0.7.1

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