intelligence 0.6.0 → 0.8.0

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