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
@@ -1,60 +1,67 @@
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 OpenAi
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
13
  # openai properties for all endpoints
13
- parameter :organization
14
- parameter :project
14
+ organization String
15
+ project String
15
16
 
16
17
  # properties for generative text endpoints
17
- group :chat_options do
18
+ chat_options do
18
19
 
19
20
  # normalized properties for openai generative text endpoint
20
- parameter :model, String
21
- parameter :n, Integer
22
- parameter :max_tokens, Integer, as: :max_completion_tokens
23
- parameter :temperature, Float
24
- parameter :top_p, Float
25
- parameter :seed, Integer
26
- parameter :stop, String, array: true
27
- parameter :stream, [ TrueClass, FalseClass ]
28
-
29
- parameter :frequency_penalty, Float
30
- parameter :presence_penalty, Float
21
+ model String, requried: true
22
+ n Integer
23
+ max_tokens Integer, as: :max_completion_tokens
24
+ temperature Float
25
+ top_p Float
26
+ seed Integer
27
+ stop String, array: true
28
+ stream [ TrueClass, FalseClass ]
29
+
30
+ frequency_penalty Float
31
+ presence_penalty Float
31
32
 
32
33
  # openai variant of normalized properties for openai generative text endpoints
33
- parameter :max_completion_tokens, Integer
34
+ max_completion_tokens Integer
34
35
 
35
36
  # openai properties for openai generative text endpoint
36
- parameter :logit_bias
37
- parameter :logprobs, [ TrueClass, FalseClass ]
38
- parameter :parallel_tool_calls, [ TrueClass, FalseClass ]
39
- group :response_format do
37
+ audio do
38
+ voice String
39
+ format String
40
+ end
41
+ logit_bias
42
+ logprobs [ TrueClass, FalseClass ]
43
+ modalities String, array: true
44
+ # the parallel_tool_calls parameter is only allowed when 'tools' are specified
45
+ parallel_tool_calls [ TrueClass, FalseClass ]
46
+ response_format do
40
47
  # 'text' and 'json_schema' are the only supported types
41
- parameter :type, String
42
- parameter :json_schema
48
+ type Symbol, in: [ :text, :json_schema ]
49
+ json_schema
43
50
  end
44
- parameter :service_tier, String
45
- group :stream_options do
46
- parameter :include_usage, [ TrueClass, FalseClass ]
51
+ service_tier String
52
+ stream_options do
53
+ include_usage [ TrueClass, FalseClass ]
47
54
  end
48
- parameter :tool_choice
49
- # the parallel_tool_calls parameter is only allowed when 'tools' are specified
50
- parameter :top_logprobs, Integer
51
- parameter :user
55
+ tool_choice
56
+ top_logprobs Integer
57
+ user
52
58
 
53
59
  end
54
60
 
55
61
  end
56
62
 
57
- include ChatMethods
63
+ include ChatRequestMethods
64
+ include ChatResponseMethods
58
65
 
59
66
  end
60
67
  end
@@ -0,0 +1,186 @@
1
+ module Intelligence
2
+ module OpenAi
3
+ module ChatRequestMethods
4
+
5
+ CHAT_REQUEST_URI = "https://api.openai.com/v1/chat/completions"
6
+
7
+ SUPPORTED_CONTENT_TYPES = [ 'image/jpeg', 'image/png' ]
8
+
9
+ def chat_request_uri( options )
10
+ CHAT_REQUEST_URI
11
+ end
12
+
13
+ def chat_request_headers( options = {} )
14
+ options = @options.merge( build_options( options ) )
15
+ result = {}
16
+
17
+ key = options[ :key ]
18
+ organization = options[ :organization ]
19
+ project = options[ :project ]
20
+
21
+ raise ArgumentError.new( "An OpenAI key is required to build an OpenAI chat request." ) \
22
+ if key.nil?
23
+
24
+ result[ 'Content-Type' ] = 'application/json'
25
+ result[ 'Authorization' ] = "Bearer #{key}"
26
+ result[ 'OpenAI-Organization' ] = organization unless organization.nil?
27
+ result[ 'OpenAI-Project' ] = project unless project.nil?
28
+
29
+ result
30
+ end
31
+
32
+ def chat_request_body( conversation, options = {} )
33
+ options = @options.merge( build_options( options ) )
34
+ result = options[ :chat_options ]&.compact || {}
35
+ result[ :messages ] = []
36
+
37
+ system_message = to_open_ai_system_message( conversation[ :system_message ] )
38
+ result[ :messages ] << { role: 'system', content: system_message } if system_message
39
+
40
+ conversation[ :messages ]&.each do | message |
41
+
42
+ result_message = { role: message[ :role ] }
43
+ result_message_content = []
44
+
45
+ message[ :contents ]&.each do | content |
46
+ case content[ :type ]
47
+ when :text
48
+ result_message_content << { type: 'text', text: content[ :text ] }
49
+ when :binary
50
+ content_type = content[ :content_type ]
51
+ bytes = content[ :bytes ]
52
+ if content_type && bytes
53
+ if SUPPORTED_CONTENT_TYPES.include?( content_type )
54
+ result_message_content << {
55
+ type: 'image_url',
56
+ image_url: {
57
+ url: "data:#{content_type};base64,#{Base64.strict_encode64( bytes )}".freeze
58
+ }
59
+ }
60
+ else
61
+ raise UnsupportedContentError.new(
62
+ :open_ai,
63
+ "only supports content of type #{SUPPORTED_CONTENT_TYPES.join( ', ' )}"
64
+ )
65
+ end
66
+ else
67
+ raise UnsupportedContentError.new(
68
+ :open_ai,
69
+ 'requires binary content to include content type and ( packed ) bytes'
70
+ )
71
+ end
72
+ when :file
73
+ content_type = content[ :content_type ]
74
+ uri = content[ :uri ]
75
+ if content_type && uri
76
+ if SUPPORTED_CONTENT_TYPES.include?( content_type )
77
+ result_message_content << {
78
+ type: 'image_url',
79
+ image_url: {
80
+ url: uri
81
+ }
82
+ }
83
+ else
84
+ raise UnsupportedContentError.new(
85
+ :open_ai,
86
+ "only supports content of type #{SUPPORTED_CONTENT_TYPES.join( ', ' )}"
87
+ )
88
+ end
89
+ else
90
+ raise UnsupportedContentError.new(
91
+ :open_ai,
92
+ 'requires file content to include content type and uri'
93
+ )
94
+ end
95
+ when :tool_call
96
+ tool_calls = result_message[ :tool_calls ] || []
97
+ function = {
98
+ name: content[ :tool_name ]
99
+ }
100
+ function[ :arguments ] = JSON.generate( content[ :tool_parameters ] || {} )
101
+ tool_calls << { id: content[ :tool_call_id ], type: 'function', function: function }
102
+ result_message[ :tool_calls ] = tool_calls
103
+ when :tool_result
104
+ # open-ai returns tool results as a message with a role of 'tool'
105
+ result[ :messages ] << {
106
+ role: :tool,
107
+ tool_call_id: content[ :tool_call_id ],
108
+ content: content[ :tool_result ]
109
+ }
110
+ else
111
+ raise InvalidContentError.new( :open_ai )
112
+ end
113
+
114
+ end
115
+
116
+ result_message[ :content ] = result_message_content
117
+ result[ :messages ] << result_message \
118
+ if result_message[ :content ]&.any? || result_message[ :tool_calls ]&.any?
119
+ result
120
+
121
+ end
122
+
123
+ tools_attributes = chat_request_tools_attributes( conversation[ :tools ] )
124
+ result[ :tools ] = tools_attributes if tools_attributes && tools_attributes.length > 0
125
+
126
+ JSON.generate( result )
127
+ end
128
+
129
+ def chat_request_tools_attributes( tools )
130
+ properties_array_to_object = lambda do | properties |
131
+ return nil unless properties&.any?
132
+ object = {}
133
+ required = []
134
+ properties.each do | property |
135
+ name = property.delete( :name )
136
+ required << name if property.delete( :required )
137
+ if property[ :properties ]&.any?
138
+ property_properties, property_required =
139
+ properties_array_to_object.call( property[ :properties ] )
140
+ property[ :properties ] = property_properties
141
+ property[ :required ] = property_required if property_required.any?
142
+ end
143
+ object[ name ] = property
144
+ end
145
+ [ object, required.compact ]
146
+ end
147
+
148
+ tools&.map do | tool |
149
+ function = {
150
+ type: 'function',
151
+ function: {
152
+ name: tool[ :name ],
153
+ description: tool[ :description ],
154
+ }
155
+ }
156
+
157
+ if tool[ :properties ]&.any?
158
+ properties_object, properties_required =
159
+ properties_array_to_object.call( tool[ :properties ] )
160
+ function[ :function ][ :parameters ] = {
161
+ type: 'object',
162
+ properties: properties_object
163
+ }
164
+ function[ :function ][ :parameters ][ :required ] = properties_required \
165
+ if properties_required.any?
166
+ end
167
+ function
168
+ end
169
+ end
170
+
171
+ private
172
+
173
+ def to_open_ai_system_message( system_message )
174
+ return nil if system_message.nil?
175
+
176
+ result = ''
177
+ system_message[ :contents ].each do | content |
178
+ result += content[ :text ] if content[ :type ] == :text
179
+ end
180
+
181
+ result.empty? ? nil : result
182
+ end
183
+
184
+ end
185
+ end
186
+ end
@@ -1,116 +1,9 @@
1
1
  module Intelligence
2
2
  module OpenAi
3
- module ChatMethods
4
-
5
- CHAT_REQUEST_URI = "https://api.openai.com/v1/chat/completions"
6
-
7
- SUPPORTED_CONTENT_TYPES = [ 'image/jpeg', 'image/png' ]
8
-
9
- def chat_request_uri( options )
10
- CHAT_REQUEST_URI
11
- end
12
-
13
- def chat_request_headers( options = {} )
14
- options = options ? self.class.configure( options ) : {}
15
- options = @options.merge( options )
16
- result = {}
17
-
18
- key = options[ :key ]
19
- organization = options[ :organization ]
20
- project = options[ :project ]
21
-
22
- raise ArgumentError.new( "An OpenAI key is required to build an OpenAI chat request." ) \
23
- if key.nil?
24
-
25
- result[ 'Content-Type' ] = 'application/json'
26
- result[ 'Authorization' ] = "Bearer #{key}"
27
- result[ 'OpenAI-Organization' ] = organization unless organization.nil?
28
- result[ 'OpenAI-Project' ] = project unless project.nil?
29
-
30
- result
31
- end
32
-
33
- def chat_request_body( conversation, options = {} )
34
- options = options ? self.class.configure( options ) : {}
35
- options = @options.merge( options )
36
- result = options[ :chat_options ]&.compact || {}
37
- result[ :messages ] = []
38
-
39
- system_message = translate_system_message( conversation[ :system_message ] )
40
- result[ :messages ] << { role: 'system', content: system_message } if system_message
41
-
42
- conversation[ :messages ]&.each do | message |
43
-
44
- result_message = { role: message[ :role ] }
45
- result_message_content = []
46
-
47
- message[ :contents ]&.each do | content |
48
- case content[ :type ]
49
- when :text
50
- result_message_content << { type: 'text', text: content[ :text ] }
51
- when :binary
52
- content_type = content[ :content_type ]
53
- bytes = content[ :bytes ]
54
- if content_type && bytes
55
- if SUPPORTED_CONTENT_TYPES.include?( content_type )
56
- result_message_content << {
57
- type: 'image_url',
58
- image_url: {
59
- url: "data:#{content_type};base64,#{Base64.strict_encode64( bytes )}".freeze
60
- }
61
- }
62
- else
63
- raise UnsupportedContentError.new(
64
- :open_ai,
65
- "only supports content of type #{SUPPORTED_CONTENT_TYPES.join( ', ' )}"
66
- )
67
- end
68
- else
69
- raise UnsupportedContentError.new(
70
- :open_ai,
71
- 'requires binary content to include content type and ( packed ) bytes'
72
- )
73
- end
74
- when :file
75
- content_type = content[ :content_type ]
76
- uri = content[ :uri ]
77
- if content_type && uri
78
- if SUPPORTED_CONTENT_TYPES.include?( content_type )
79
- result_message_content << {
80
- type: 'image_url',
81
- image_url: {
82
- url: uri
83
- }
84
- }
85
- else
86
- raise UnsupportedContentError.new(
87
- :open_ai,
88
- "only supports content of type #{SUPPORTED_CONTENT_TYPES.join( ', ' )}"
89
- )
90
- end
91
- else
92
- raise UnsupportedContentError.new(
93
- :open_ai,
94
- 'requires file content to include content type and uri'
95
- )
96
- end
97
- else
98
- raise InvalidContentError.new( :open_ai )
99
- end
100
-
101
- end
102
-
103
- result_message[ :content ] = result_message_content
104
- result[ :messages ] << result_message
105
-
106
- end
107
-
108
- JSON.generate( result )
109
- end
3
+ module ChatResponseMethods
110
4
 
111
5
  def chat_result_attributes( response )
112
6
  return nil unless response.success?
113
-
114
7
  response_json = JSON.parse( response.body, symbolize_names: true ) rescue nil
115
8
  return nil \
116
9
  if response_json.nil? || response_json[ :choices ].nil?
@@ -120,15 +13,31 @@ module Intelligence
120
13
 
121
14
  ( response_json[ :choices ] || [] ).each do | json_choice |
122
15
  json_message = json_choice[ :message ]
123
- result[ :choices ].push(
124
- {
125
- end_reason: translate_end_result( json_choice[ :finish_reason ] ),
126
- message: {
127
- role: json_message[ :role ],
128
- contents: [ { type: 'text', text: json_message[ :content ] } ]
129
- }
130
- }
131
- )
16
+ result_message = nil
17
+ if ( json_message )
18
+ result_message = { role: json_message[ :role ] }
19
+ if json_message[ :content ]
20
+ result_message[ :contents ] = [ { type: :text, text: json_message[ :content ] } ]
21
+ end
22
+ if json_message[ :tool_calls ] && !json_message[ :tool_calls ].empty?
23
+ result_message[ :contents ] ||= []
24
+ json_message[ :tool_calls ].each do | json_message_tool_call |
25
+ result_message_tool_call_parameters =
26
+ JSON.parse( json_message_tool_call[ :function ][ :arguments ], symbolize_names: true ) \
27
+ rescue json_message_tool_call[ :function ][ :arguments ]
28
+ result_message[ :contents ] << {
29
+ type: :tool_call,
30
+ tool_call_id: json_message_tool_call[ :id ],
31
+ tool_name: json_message_tool_call[ :function ][ :name ],
32
+ tool_parameters: result_message_tool_call_parameters
33
+ }
34
+ end
35
+ end
36
+ end
37
+ result[ :choices ].push( {
38
+ end_reason: translate_end_result( json_choice[ :finish_reason ] ),
39
+ message: result_message
40
+ } )
132
41
  end
133
42
 
134
43
  metrics_json = response_json[ :usage ]
@@ -275,26 +184,15 @@ module Intelligence
275
184
 
276
185
  alias_method :stream_result_error_attributes, :chat_result_error_attributes
277
186
 
278
- private; def translate_system_message( system_message )
279
-
280
- return nil if system_message.nil?
281
-
282
- result = ''
283
- system_message[ :contents ].each do | content |
284
- result += content[ :text ] if content[ :type ] == :text
285
- end
286
-
287
- result.empty? ? nil : result
288
-
289
- end
187
+ private
290
188
 
291
- private; def translate_end_result( end_result )
189
+ def translate_end_result( end_result )
292
190
  case end_result
293
191
  when 'stop'
294
192
  :ended
295
193
  when 'length'
296
194
  :token_limit_exceeded
297
- when 'function_call'
195
+ when 'tool_calls'
298
196
  :tool_called
299
197
  when 'content_filter'
300
198
  :filtered
@@ -305,37 +203,37 @@ module Intelligence
305
203
 
306
204
  def translate_error_response_status( status )
307
205
  case status
308
- when 400
309
- [ :invalid_request_error,
310
- "There was an issue with the format or content of your request." ]
311
- when 401
312
- [ :authentication_error,
313
- "There's an issue with your API key." ]
314
- when 403
315
- [ :permission_error,
316
- "Your API key does not have permission to use the specified resource." ]
317
- when 404
318
- [ :not_found_error,
319
- "The requested resource was not found." ]
320
- when 413
321
- [ :request_too_large,
322
- "Request exceeds the maximum allowed number of bytes." ]
323
- when 422
324
- [ :invalid_request_error,
325
- "There was an issue with the format or content of your request." ]
326
- when 429
327
- [ :rate_limit_error,
328
- "Your account has hit a rate limit." ]
329
- when 500, 502, 503
330
- [ :api_error,
331
- "An unexpected error has occurred internal to the providers systems." ]
332
- when 529
333
- [ :overloaded_error,
334
- "The providers server is temporarily overloaded." ]
335
- else
336
- [ :unknown_error, "
337
- An unknown error occurred." ]
338
- end
206
+ when 400
207
+ [ :invalid_request_error,
208
+ "There was an issue with the format or content of your request." ]
209
+ when 401
210
+ [ :authentication_error,
211
+ "There's an issue with your API key." ]
212
+ when 403
213
+ [ :permission_error,
214
+ "Your API key does not have permission to use the specified resource." ]
215
+ when 404
216
+ [ :not_found_error,
217
+ "The requested resource was not found." ]
218
+ when 413
219
+ [ :request_too_large,
220
+ "Request exceeds the maximum allowed number of bytes." ]
221
+ when 422
222
+ [ :invalid_request_error,
223
+ "There was an issue with the format or content of your request." ]
224
+ when 429
225
+ [ :rate_limit_error,
226
+ "Your account has hit a rate limit." ]
227
+ when 500, 502, 503
228
+ [ :api_error,
229
+ "An unexpected error has occurred internal to the providers systems." ]
230
+ when 529
231
+ [ :overloaded_error,
232
+ "The providers server is temporarily overloaded." ]
233
+ else
234
+ [ :unknown_error, "
235
+ An unknown error occurred." ]
236
+ end
339
237
  end
340
238
 
341
239
  end
@@ -1,2 +1,2 @@
1
1
  require_relative '../adapter'
2
- require_relative 'open_ai/adapter'
2
+ require_relative 'open_ai/adapter'
@@ -7,25 +7,25 @@ module Intelligence
7
7
 
8
8
  chat_request_uri "https://openrouter.ai/api/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_k, Integer
16
- parameter :top_p, Float
17
- parameter :max_tokens, Integer
18
- parameter :seed, Integer
19
- parameter :stop, String, array: true
20
- parameter :stream, [ TrueClass, FalseClass ]
21
- parameter :frequency_penalty, Float
22
- parameter :repetition_penalty, Float
23
- parameter :presence_penalty, Float
10
+ schema do
11
+ key String
12
+ chat_options do
13
+ model String
14
+ temperature Float
15
+ top_k Integer
16
+ top_p Float
17
+ max_tokens Integer
18
+ seed Integer
19
+ stop String, array: true
20
+ stream [ TrueClass, FalseClass ]
21
+ frequency_penalty Float
22
+ repetition_penalty Float
23
+ presence_penalty Float
24
24
 
25
- group :provider do
26
- parameter :order, String, array: true
27
- parameter :require_parameters, [ TrueClass, FalseClass ]
28
- parameter :allow_fallbacks, [ TrueClass, FalseClass ]
25
+ provider do
26
+ order String, array: true
27
+ require_parameters [ TrueClass, FalseClass ]
28
+ allow_fallbacks [ TrueClass, FalseClass ]
29
29
  end
30
30
  end
31
31
  end
@@ -7,27 +7,27 @@ module Intelligence
7
7
 
8
8
  chat_request_uri "https://api.sambanova.ai/v1/chat/completions"
9
9
 
10
- configuration do
10
+ schema do
11
11
 
12
12
  # normalized properties for all endpoints
13
- parameter :key, String
13
+ key String
14
14
 
15
15
  # properties for generative text endpoints
16
- group :chat_options do
16
+ chat_options do
17
17
 
18
18
  # normalized properties for samba nova generative text endpoint
19
- parameter :model, String
20
- parameter :max_tokens, Integer
21
- parameter :temperature, Float
22
- parameter :top_p, Float
23
- parameter :top_k, Float
24
- parameter :stop, String, array: true
25
- parameter :stream, [ TrueClass, FalseClass ]
19
+ model String
20
+ max_tokens Integer
21
+ temperature Float
22
+ top_p Float
23
+ top_k Float
24
+ stop String, array: true
25
+ stream [ TrueClass, FalseClass ]
26
26
 
27
27
  # samba nova properties for samba nova generative text endpoint
28
- parameter :repetition_penalty, Float
29
- group :stream_options do
30
- parameter :include_usage, [ TrueClass, FalseClass ]
28
+ repetition_penalty Float
29
+ stream_options do
30
+ include_usage [ TrueClass, FalseClass ]
31
31
  end
32
32
 
33
33
  end