intelligence 0.6.0 → 0.8.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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +576 -0
  3. data/intelligence.gemspec +2 -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} +13 -137
  11. data/lib/intelligence/adapters/cerebras.rb +19 -19
  12. data/lib/intelligence/adapters/generic/adapter.rb +4 -2
  13. data/lib/intelligence/adapters/generic/chat_request_methods.rb +221 -0
  14. data/lib/intelligence/adapters/generic/chat_response_methods.rb +234 -0
  15. data/lib/intelligence/adapters/generic.rb +1 -1
  16. data/lib/intelligence/adapters/google/adapter.rb +33 -22
  17. data/lib/intelligence/adapters/google/chat_request_methods.rb +234 -0
  18. data/lib/intelligence/adapters/google/chat_response_methods.rb +236 -0
  19. data/lib/intelligence/adapters/groq.rb +29 -49
  20. data/lib/intelligence/adapters/hyperbolic.rb +13 -39
  21. data/lib/intelligence/adapters/mistral.rb +21 -42
  22. data/lib/intelligence/adapters/open_ai/adapter.rb +39 -32
  23. data/lib/intelligence/adapters/open_ai/chat_request_methods.rb +186 -0
  24. data/lib/intelligence/adapters/open_ai/chat_response_methods.rb +239 -0
  25. data/lib/intelligence/adapters/open_ai.rb +1 -1
  26. data/lib/intelligence/adapters/open_router.rb +18 -18
  27. data/lib/intelligence/adapters/samba_nova.rb +16 -18
  28. data/lib/intelligence/adapters/together_ai.rb +25 -23
  29. data/lib/intelligence/conversation.rb +11 -10
  30. data/lib/intelligence/message.rb +45 -29
  31. data/lib/intelligence/message_content/base.rb +2 -9
  32. data/lib/intelligence/message_content/binary.rb +3 -3
  33. data/lib/intelligence/message_content/file.rb +3 -3
  34. data/lib/intelligence/message_content/text.rb +10 -2
  35. data/lib/intelligence/message_content/tool_call.rb +61 -5
  36. data/lib/intelligence/message_content/tool_result.rb +11 -6
  37. data/lib/intelligence/tool.rb +139 -0
  38. data/lib/intelligence/version.rb +1 -1
  39. data/lib/intelligence.rb +3 -1
  40. metadata +31 -13
  41. data/lib/intelligence/adapter/class_methods/construction.rb +0 -17
  42. data/lib/intelligence/adapter/module_methods/construction.rb +0 -43
  43. data/lib/intelligence/adapters/generic/chat_methods.rb +0 -355
  44. data/lib/intelligence/adapters/google/chat_methods.rb +0 -393
  45. data/lib/intelligence/adapters/legacy/adapter.rb +0 -11
  46. data/lib/intelligence/adapters/legacy/chat_methods.rb +0 -54
  47. data/lib/intelligence/adapters/open_ai/chat_methods.rb +0 -345
@@ -0,0 +1,221 @@
1
+ module Intelligence
2
+ module Generic
3
+ module ChatRequestMethods
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 = nil )
24
+ options = @options.merge( build_options( options ) )
25
+ result = {}
26
+
27
+ key = options[ :key ]
28
+
29
+ raise ArgumentError.new( "An API key is required to build a chat request." ) \
30
+ if key.nil?
31
+
32
+ result[ 'Content-Type' ] = 'application/json'
33
+ result[ 'Authorization' ] = "Bearer #{key}"
34
+
35
+ result
36
+ end
37
+
38
+ def chat_request_body( conversation, options = nil )
39
+ options = @options.merge( build_options( options ) )
40
+
41
+ result = options[ :chat_options ]
42
+ result[ :messages ] = []
43
+
44
+ system_message = chat_request_system_message_attributes( conversation[ :system_message ] )
45
+ result[ :messages ] << system_message if system_message
46
+
47
+ conversation[ :messages ]&.each do | message |
48
+ return nil unless message[ :contents ]&.any?
49
+
50
+ result_message = { role: message[ :role ] }
51
+ result_message_content = []
52
+
53
+ message_contents = message[ :contents ]
54
+
55
+ # tool calls in the open ai api are not content
56
+ tool_calls, message_contents = message_contents.partition do | content |
57
+ content[ :type ] == :tool_call
58
+ end
59
+
60
+ # tool results in the open ai api are not content
61
+ tool_results, message_contents = message_contents.partition do | content |
62
+ content[ :type ] == :tool_result
63
+ end
64
+
65
+ # many vendor api's, especially when hosting text only models, will only accept a single
66
+ # text content item; if the content is only text this will coalece multiple text content
67
+ # items into a single content item
68
+ unless message_contents.any? { | c | c[ :type ] != :text }
69
+ result_message_content = message_contents.map { | c | c[ :text ] || '' }.join( "\n" )
70
+ else
71
+ message_contents&.each do | content |
72
+ result_message_content << chat_request_message_content_attributes( content )
73
+ end
74
+ end
75
+
76
+ if tool_calls.any?
77
+ result_message[ :tool_calls ] = tool_calls.map { | tool_call |
78
+ {
79
+ id: tool_call[ :tool_call_id ],
80
+ type: 'function',
81
+ function: {
82
+ name: tool_call[ :tool_name ],
83
+ arguments: JSON.generate( tool_call[ :tool_parameters ] || {} )
84
+ }
85
+ }
86
+ }
87
+ end
88
+
89
+ result_message[ :content ] = result_message_content
90
+ unless result_message_content.empty? && tool_calls.empty?
91
+ result[ :messages ] << result_message
92
+ end
93
+
94
+ if tool_results.any?
95
+ result[ :messages ].concat( tool_results.map { | tool_result |
96
+ {
97
+ role: :tool,
98
+ tool_call_id: tool_result[ :tool_call_id ],
99
+ content: tool_result[ :tool_result ]
100
+ }
101
+ } )
102
+ end
103
+ end
104
+
105
+ tools_attributes = chat_request_tools_attributes( conversation[ :tools ] )
106
+ result[ :tools ] = tools_attributes if tools_attributes && tools_attributes.length > 0
107
+
108
+ JSON.generate( result )
109
+ end
110
+
111
+ def chat_request_message_content_attributes( content )
112
+ case content[ :type ]
113
+ when :text
114
+ { type: 'text', text: content[ :text ] }
115
+ when :binary
116
+ content_type = content[ :content_type ]
117
+ bytes = content[ :bytes ]
118
+ if content_type && bytes
119
+ mime_type = MIME::Types[ content_type ].first
120
+ if mime_type&.media_type == 'image'
121
+ {
122
+ type: 'image_url',
123
+ image_url: {
124
+ url: "data:#{content_type};base64,#{Base64.strict_encode64( bytes )}".freeze
125
+ }
126
+ }
127
+ else
128
+ raise UnsupportedContentError.new(
129
+ :generic,
130
+ 'only support content of type image/*'
131
+ )
132
+ end
133
+ else
134
+ raise UnsupportedContentError.new(
135
+ :generic,
136
+ 'requires binary content to include content type and ( packed ) bytes'
137
+ )
138
+ end
139
+ when :file
140
+ content_type = content[ :content_type ]
141
+ uri = content[ :uri ]
142
+ if content_type && uri
143
+ mime_type = MIME::Types[ content_type ].first
144
+ if mime_type&.media_type == 'image'
145
+ {
146
+ type: 'image_url',
147
+ image_url: { url: uri }
148
+ }
149
+ else
150
+ raise UnsupportedContentError.new(
151
+ :generic,
152
+ 'only support content of type image/*'
153
+ )
154
+ end
155
+ else
156
+ raise UnsupportedContentError.new(
157
+ :generic,
158
+ 'requires binary content to include content type and ( packed ) bytes'
159
+ )
160
+ end
161
+ end
162
+ end
163
+
164
+ def chat_request_system_message_attributes( system_message )
165
+ return nil if system_message.nil?
166
+
167
+ result = ''
168
+ system_message[ :contents ].each do | content |
169
+ result += content[ :text ] if content[ :type ] == :text
170
+ end
171
+
172
+ result.empty? ? nil : { role: 'system', content: result } if system_message
173
+ end
174
+
175
+ def chat_request_tools_attributes( tools )
176
+ properties_array_to_object = lambda do | properties |
177
+ return nil unless properties&.any?
178
+ object = {}
179
+ required = []
180
+ properties.each do | property |
181
+ name = property.delete( :name )
182
+ required << name if property.delete( :required )
183
+ if property[ :properties ]&.any?
184
+ property_properties, property_required =
185
+ properties_array_to_object.call( property[ :properties ] )
186
+ property[ :properties ] = property_properties
187
+ property[ :required ] = property_required if property_required.any?
188
+ end
189
+ object[ name ] = property
190
+ end
191
+ [ object, required.compact ]
192
+ end
193
+
194
+ tools&.map do | tool |
195
+ function = {
196
+ type: 'function',
197
+ function: {
198
+ name: tool[ :name ],
199
+ description: tool[ :description ],
200
+ }
201
+ }
202
+
203
+ if tool[ :properties ]&.any?
204
+ properties_object, properties_required =
205
+ properties_array_to_object.call( tool[ :properties ] )
206
+ function[ :function ][ :parameters ] = {
207
+ type: 'object',
208
+ properties: properties_object
209
+ }
210
+ function[ :function ][ :parameters ][ :required ] = properties_required \
211
+ if properties_required.any?
212
+ else
213
+ function[ :function ][ :parameters ] = {}
214
+ end
215
+ function
216
+ end
217
+ end
218
+
219
+ end
220
+ end
221
+ end
@@ -0,0 +1,234 @@
1
+ module Intelligence
2
+ module Generic
3
+ module ChatResponseMethods
4
+
5
+ def chat_result_attributes( response )
6
+ return nil unless response.success?
7
+ response_json = JSON.parse( response.body, symbolize_names: true ) rescue nil
8
+ return nil if response_json.nil? || response_json[ :choices ].nil?
9
+
10
+ result = {}
11
+ result[ :choices ] = []
12
+
13
+ ( response_json[ :choices ] || [] ).each do | json_choice |
14
+ if ( json_message = json_choice[ :message ] )
15
+ result_message = { role: json_message[ :role ] }
16
+ if json_message[ :content ]
17
+ result_message[ :contents ] = [ { type: :text, text: json_message[ :content ] } ]
18
+ end
19
+ if json_message[ :tool_calls ] && !json_message[ :tool_calls ].empty?
20
+ result_message[ :contents ] ||= []
21
+ json_message[ :tool_calls ].each do | json_message_tool_call |
22
+ result_message_tool_call_parameters =
23
+ JSON.parse( json_message_tool_call[ :function ][ :arguments ], symbolize_names: true ) \
24
+ rescue json_message_tool_call[ :function ][ :arguments ]
25
+ result_message[ :contents ] << {
26
+ type: :tool_call,
27
+ tool_call_id: json_message_tool_call[ :id ],
28
+ tool_name: json_message_tool_call[ :function ][ :name ],
29
+ tool_parameters: result_message_tool_call_parameters
30
+ }
31
+ end
32
+ end
33
+ end
34
+ result[ :choices ].push( {
35
+ end_reason: to_end_reason( json_choice[ :finish_reason ] ),
36
+ message: result_message
37
+ } )
38
+ end
39
+
40
+ metrics_json = response_json[ :usage ]
41
+ unless metrics_json.nil?
42
+
43
+ metrics = {}
44
+ metrics[ :input_tokens ] = metrics_json[ :prompt_tokens ]
45
+ metrics[ :output_tokens ] = metrics_json[ :completion_tokens ]
46
+ metrics = metrics.compact
47
+
48
+ result[ :metrics ] = metrics unless metrics.empty?
49
+
50
+ end
51
+
52
+ result
53
+ end
54
+
55
+ def chat_result_error_attributes( response )
56
+ error_type, error_description = to_error_response( response.status )
57
+ error = error_type
58
+
59
+ parsed_body = JSON.parse( response.body, symbolize_names: true ) rescue nil
60
+ if parsed_body && parsed_body.respond_to?( :[] )
61
+ if parsed_body[ :error ].respond_to?( :[] )
62
+ error = parsed_body[ :error ][ :code ] || error_type
63
+ error_description = parsed_body[ :error ][ :message ] || error_description
64
+ elsif parsed_body[ :object ] == 'error'
65
+ error = parsed_body[ :type ] || error_type
66
+ error_description = parsed_body[ :detail ] || parsed_body[ :message ]
67
+ end
68
+ end
69
+
70
+ { error_type: error_type.to_s, error: error.to_s, error_description: error_description }
71
+ end
72
+
73
+ def stream_result_chunk_attributes( context, chunk )
74
+ context ||= {}
75
+ buffer = context[ :buffer ] || ''
76
+ metrics = context[ :metrics ] || {
77
+ input_tokens: 0,
78
+ output_tokens: 0
79
+ }
80
+ choices = context[ :choices ] || Array.new( 1 , { message: {} } )
81
+
82
+ choices.each do | choice |
83
+ choice[ :message ][ :contents ] = choice[ :message ][ :contents ]&.map do | content |
84
+ { type: content[ :type ] }
85
+ end
86
+ end
87
+
88
+ buffer += chunk
89
+ while ( eol_index = buffer.index( "\n" ) )
90
+ line = buffer.slice!( 0..eol_index )
91
+ line = line.strip
92
+ next if line.empty? || !line.start_with?( 'data:' )
93
+ line = line[ 6..-1 ]
94
+ next if line.end_with?( '[DONE]' )
95
+
96
+ data = JSON.parse( line ) rescue nil
97
+ if data.is_a?( Hash )
98
+ data[ 'choices' ]&.each do | data_choice |
99
+
100
+ data_choice_index = data_choice[ 'index' ]
101
+ data_choice_delta = data_choice[ 'delta' ]
102
+ data_choice_finish_reason = data_choice[ 'finish_reason' ]
103
+
104
+ choices.fill( { message: {} }, choices.size, data_choice_index + 1 ) \
105
+ if choices.size <= data_choice_index
106
+ contents = choices[ data_choice_index ][ :message ][ :contents ] || []
107
+
108
+ text_content = contents.first&.[]( :type ) == :text ? contents.first : nil
109
+ if data_choice_content = data_choice_delta[ 'content' ]
110
+ if text_content.nil?
111
+ contents.unshift( text_content = { type: :text, text: data_choice_content } )
112
+ else
113
+ text_content[ :text ] = ( text_content[ :text ] || '' ) + data_choice_content
114
+ end
115
+ end
116
+ if data_choice_tool_calls = data_choice_delta[ 'tool_calls' ]
117
+ data_choice_tool_calls.each_with_index do | data_choice_tool_call, data_choice_tool_call_index |
118
+ if data_choice_tool_call_function = data_choice_tool_call[ 'function' ]
119
+ data_choice_tool_index = data_choice_tool_call[ 'index' ] || data_choice_tool_call_index
120
+ data_choice_tool_id = data_choice_tool_call[ 'id' ]
121
+ data_choice_tool_name = data_choice_tool_call_function[ 'name' ]
122
+ data_choice_tool_parameters = data_choice_tool_call_function[ 'arguments' ]
123
+
124
+ tool_call_content_index = ( text_content.nil? ? 0 : 1 ) + data_choice_tool_index
125
+ if tool_call_content_index >= contents.length
126
+ contents.push( {
127
+ type: :tool_call,
128
+ tool_call_id: data_choice_tool_id,
129
+ tool_name: data_choice_tool_name,
130
+ tool_parameters: data_choice_tool_parameters
131
+ } )
132
+ else
133
+ tool_call = contents[ tool_call_content_index ]
134
+ tool_call[ :tool_call_id ] = ( tool_call[ :tool_call_id ] || '' ) + data_choice_tool_id \
135
+ if data_choice_tool_id
136
+ tool_call[ :tool_name ] = ( tool_call[ :tool_name ] || '' ) + data_choice_tool_name \
137
+ if data_choice_tool_name
138
+ tool_call[ :tool_parameters ] = ( tool_call[ :tool_parameters ] || '' ) + data_choice_tool_parameters \
139
+ if data_choice_tool_parameters
140
+ end
141
+ end
142
+ end
143
+ end
144
+ choices[ data_choice_index ][ :message ][ :contents ] = contents
145
+ choices[ data_choice_index ][ :end_reason ] ||=
146
+ to_end_reason( data_choice_finish_reason )
147
+ end
148
+
149
+ if usage = data[ 'usage' ]
150
+ # note: A number of providers will resend the input tokens as part of their usage
151
+ # payload.
152
+ metrics[ :input_tokens ] = usage[ 'prompt_tokens' ] \
153
+ if usage.include?( 'prompt_tokens' )
154
+ metrics[ :output_tokens ] += usage[ 'completion_tokens' ] \
155
+ if usage.include?( 'completion_tokens' )
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
+ end
168
+
169
+ def stream_result_attributes( context )
170
+ choices = context[ :choices ]
171
+ metrics = context[ :metrics ]
172
+
173
+ choices = choices.map do | choice |
174
+ { end_reason: choice[ :end_reason ] }
175
+ end
176
+
177
+ { choices: choices, metrics: context[ :metrics ] }
178
+ end
179
+
180
+ alias_method :stream_result_error_attributes, :chat_result_error_attributes
181
+
182
+ def to_end_reason( finish_reason )
183
+ case finish_reason
184
+ when 'stop'
185
+ :ended
186
+ when 'length'
187
+ :token_limit_exceeded
188
+ when 'tool_calls'
189
+ :tool_called
190
+ when 'content_filter'
191
+ :filtered
192
+ else
193
+ nil
194
+ end
195
+ end
196
+
197
+ def to_error_response( status )
198
+ case status
199
+ when 400
200
+ [ :invalid_request_error,
201
+ "There was an issue with the format or content of your request." ]
202
+ when 401
203
+ [ :authentication_error,
204
+ "There's an issue with your API key." ]
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
+ end
234
+ end
@@ -1,2 +1,2 @@
1
1
  require_relative '../adapter'
2
- require_relative 'generic/adapter'
2
+ require_relative 'generic/adapter'
@@ -1,44 +1,55 @@
1
- require_relative 'chat_methods'
1
+ require_relative 'chat_request_methods'
2
+ require_relative 'chat_response_methods'
2
3
 
3
4
  module Intelligence
4
5
  module Google
5
6
  class Adapter < Adapter::Base
6
7
 
7
- configuration do
8
+ schema do
8
9
 
9
10
  # normalized properties for all endpoints
10
- parameter :key, String
11
+ key String
11
12
 
12
- group :chat_options, as: :generationConfig do
13
+ chat_options as: :generationConfig do
13
14
 
14
15
  # normalized properties for google generative text endpoint
15
- parameter :model, String
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
16
+ model String
17
+ max_tokens Integer, as: :maxOutputTokens
18
+ n Integer, as: :candidateCount
19
+ temperature Float
20
+ top_k Integer, as: :topK
21
+ top_p Float, as: :topP
22
+ seed Integer
23
+ stop String, array: true, as: :stopSequences
24
+ stream [ TrueClass, FalseClass ]
25
+
26
+ frequency_penalty Float, as: :frequencyPenalty
27
+ presence_penalty Float, as: :presencePenalty
27
28
 
28
29
  # 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
30
+ candidate_count Integer, as: :candidateCount
31
+ max_output_tokens Integer, as: :maxOutputTokens
32
+ stop_sequences String, array: true, as: :stopSequences
32
33
 
33
34
  # google specific properties for google generative text endpoints
34
- parameter :response_mime_type, String, as: :responseMimeType
35
- parameter :response_schema, as: :responseSchema
35
+ response_mime_type String, as: :responseMimeType
36
+ response_schema as: :responseSchema
37
+
38
+ # google specific tool configuration
39
+ tool_configuration as: :tool_config do
40
+ function_calling as: :function_calling_config do
41
+ mode Symbol, in: [ :auto, :any, :none ]
42
+ allowed_function_names String, array: true
43
+ end
44
+ end
45
+
36
46
 
37
47
  end
38
48
 
39
49
  end
40
50
 
41
- include ChatMethods
51
+ include ChatRequestMethods
52
+ include ChatResponseMethods
42
53
 
43
54
  end
44
55