intelligence 0.5.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 (45) 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 +23 -3
  5. data/lib/intelligence/adapter/class_methods.rb +15 -0
  6. data/lib/intelligence/adapter/{construction_methods.rb → module_methods.rb} +8 -4
  7. data/lib/intelligence/adapter.rb +2 -2
  8. data/lib/intelligence/adapters/anthropic/adapter.rb +21 -30
  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 -124
  11. data/lib/intelligence/adapters/cerebras.rb +17 -17
  12. data/lib/intelligence/adapters/generic/adapter.rb +1 -12
  13. data/lib/intelligence/adapters/generic/chat_methods.rb +42 -11
  14. data/lib/intelligence/adapters/generic.rb +1 -1
  15. data/lib/intelligence/adapters/google/adapter.rb +33 -35
  16. data/lib/intelligence/adapters/google/chat_request_methods.rb +233 -0
  17. data/lib/intelligence/adapters/google/{chat_methods.rb → chat_response_methods.rb} +52 -162
  18. data/lib/intelligence/adapters/groq.rb +46 -28
  19. data/lib/intelligence/adapters/hyperbolic.rb +13 -13
  20. data/lib/intelligence/adapters/legacy/adapter.rb +0 -2
  21. data/lib/intelligence/adapters/legacy/chat_methods.rb +22 -6
  22. data/lib/intelligence/adapters/mistral.rb +57 -0
  23. data/lib/intelligence/adapters/open_ai/adapter.rb +38 -45
  24. data/lib/intelligence/adapters/open_ai/chat_request_methods.rb +186 -0
  25. data/lib/intelligence/adapters/open_ai/{chat_methods.rb → chat_response_methods.rb} +60 -131
  26. data/lib/intelligence/adapters/open_ai.rb +1 -1
  27. data/lib/intelligence/adapters/open_router.rb +62 -0
  28. data/lib/intelligence/adapters/samba_nova.rb +13 -13
  29. data/lib/intelligence/adapters/together_ai.rb +21 -19
  30. data/lib/intelligence/chat_request.rb +57 -7
  31. data/lib/intelligence/chat_result.rb +4 -0
  32. data/lib/intelligence/chat_result_choice.rb +4 -2
  33. data/lib/intelligence/conversation.rb +38 -9
  34. data/lib/intelligence/message.rb +103 -20
  35. data/lib/intelligence/message_content/base.rb +3 -0
  36. data/lib/intelligence/message_content/binary.rb +6 -0
  37. data/lib/intelligence/message_content/file.rb +35 -0
  38. data/lib/intelligence/message_content/text.rb +5 -0
  39. data/lib/intelligence/message_content/tool_call.rb +12 -1
  40. data/lib/intelligence/message_content/tool_result.rb +15 -3
  41. data/lib/intelligence/message_content.rb +12 -3
  42. data/lib/intelligence/tool.rb +139 -0
  43. data/lib/intelligence/version.rb +1 -1
  44. data/lib/intelligence.rb +6 -4
  45. metadata +18 -9
@@ -1,57 +1,48 @@
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 Anthropic
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, required: true
11
+ key
11
12
 
12
13
  # anthropic specific properties for all endpoints
13
- parameter :version, String, required: true, default: '2023-06-01'
14
+ version String, default: '2023-06-01'
14
15
 
15
- group :chat_options do
16
+ chat_options do
16
17
 
17
18
  # normalized properties for anthropic generative text endpoint
18
- parameter :model, String, required: true
19
- parameter :max_tokens, Integer, required: true
20
- parameter :temperature, Float
21
- parameter :top_k, Integer
22
- parameter :top_p, Float
23
- parameter :stop, String, array: true, as: :stop_sequences
24
- parameter :stream, [ TrueClass, FalseClass ]
19
+ model String
20
+ max_tokens Integer
21
+ temperature Float
22
+ top_k Integer
23
+ top_p Float
24
+ stop String, array: true, as: :stop_sequences
25
+ stream [ TrueClass, FalseClass ]
25
26
 
26
27
  # anthropic variant of normalized properties for anthropic generative text endpoints
27
- parameter :stop_sequences, String, array: true
28
+ stop_sequences String, array: true
28
29
 
29
30
  # anthropic specific properties for anthropic generative text endpoints
30
- parameter :tool_choice do
31
- parameter :type, String
31
+ tool_choice do
32
+ type String
32
33
  # the name parameter should only be set if type = 'tool'
33
- parameter :name, String
34
+ name String
34
35
  end
35
- group :metadata do
36
- parameter :user_id, String
36
+ metadata do
37
+ user_id String
37
38
  end
38
39
 
39
40
  end
40
41
 
41
42
  end
42
43
 
43
- attr_reader :key
44
- attr_reader :version
45
- attr_reader :chat_options
46
-
47
- def initialize( attributes = nil, &block )
48
- configuration = self.class.configure( attributes, &block )
49
- @key = configuration[ :key ]
50
- @version = configuration[ :version ]
51
- @chat_options = configuration[ :chat_options ] || {}
52
- end
53
-
54
- include ChatMethods
44
+ include ChatRequestMethods
45
+ include ChatResponseMethods
55
46
 
56
47
  end
57
48
  end
@@ -0,0 +1,189 @@
1
+ module Intelligence
2
+ module Anthropic
3
+ module ChatRequestMethods
4
+
5
+ CHAT_REQUEST_URI = "https://api.anthropic.com/v1/messages"
6
+
7
+ def chat_request_uri( options )
8
+ CHAT_REQUEST_URI
9
+ end
10
+
11
+ def chat_request_headers( options = nil )
12
+ options = @options.merge( build_options( options ) )
13
+ result = {}
14
+
15
+ key = options[ :key ]
16
+ version = options[ :version ] || "2023-06-01"
17
+
18
+ raise ArgumentError.new(
19
+ "An Anthropic key is required to build an Anthropic chat request."
20
+ ) if key.nil?
21
+
22
+ result[ 'content-type' ] = 'application/json'
23
+ result[ 'x-api-key' ] = "#{key}"
24
+ result[ 'anthropic-version' ] = version unless version.nil?
25
+
26
+ result
27
+ end
28
+
29
+ def chat_request_body( conversation, options = nil )
30
+ options = @options.merge( build_options( options ) )
31
+ result = options[ :chat_options ]&.compact || {}
32
+
33
+ system_message = to_anthropic_system_message( conversation[ :system_message ] )
34
+ result[ :system ] = system_message unless system_message.nil?
35
+ result[ :messages ] = []
36
+
37
+ messages = conversation[ :messages ]
38
+ length = messages&.length || 0
39
+ index = 0; while index < length
40
+
41
+ message = messages[ index ]
42
+ unless message.nil?
43
+
44
+ # The Anthropic API will not accept a sequence of messages where the role of two
45
+ # sequentian messages is the same.
46
+ #
47
+ # The purpose of this code is to identify such occurences and coalece them such
48
+ # that the first message in the sequence aggregates the contents of all subsequent
49
+ # messages with the same role.
50
+ look_ahead_index = index + 1; while look_ahead_index < length
51
+ ahead_message = messages[ look_ahead_index ]
52
+ unless ahead_message.nil?
53
+ if ahead_message[ :role ] == message[ :role ]
54
+ message[ :contents ] =
55
+ ( message[ :contents ] || [] ) +
56
+ ( ahead_message[ :contents ] || [] )
57
+ messages[ look_ahead_index ] = nil
58
+ look_ahead_index += 1
59
+ else
60
+ break
61
+ end
62
+ end
63
+ end
64
+
65
+ result_message = { role: message[ :role ] }
66
+ result_message_content = []
67
+
68
+ message[ :contents ]&.each do | content |
69
+ case content[ :type ]
70
+ when :text
71
+ result_message_content << { type: 'text', text: content[ :text ] }
72
+ when :binary
73
+ content_type = content[ :content_type ]
74
+ bytes = content[ :bytes ]
75
+ if content_type && bytes
76
+ mime_type = MIME::Types[ content_type ].first
77
+ if mime_type&.media_type == 'image'
78
+ result_message_content << {
79
+ type: 'image',
80
+ source: {
81
+ type: 'base64',
82
+ media_type: content_type,
83
+ data: Base64.strict_encode64( bytes )
84
+ }
85
+ }
86
+ else
87
+ raise UnsupportedContentError.new(
88
+ :anthropic,
89
+ 'only support content of type image/*'
90
+ )
91
+ end
92
+ else
93
+ raise UnsupportedContentError.new(
94
+ :anthropic,
95
+ 'requires file content to include content type and (packed) bytes'
96
+ )
97
+ end
98
+ when :tool_call
99
+ result_message_content << {
100
+ type: 'tool_use',
101
+ id: content[ :tool_call_id ],
102
+ name: content[ :tool_name ],
103
+ input: content[ :tool_parameters ] || {}
104
+ }
105
+ when :tool_result
106
+ result_message_content << {
107
+ type: 'tool_result',
108
+ tool_use_id: content[ :tool_call_id ],
109
+ content: content[ :tool_result ]
110
+ }
111
+ else
112
+ raise InvalidContentError.new( :anthropic )
113
+ end
114
+ end
115
+
116
+ result_message[ :content ] = result_message_content
117
+ result[ :messages ] << result_message
118
+
119
+ end
120
+
121
+ index += 1
122
+
123
+ end
124
+
125
+ tools_attributes = chat_request_tools_attributes( conversation[ :tools ] )
126
+ result[ :tools ] = tools_attributes if tools_attributes && tools_attributes.length > 0
127
+
128
+ JSON.generate( result )
129
+ end
130
+
131
+ def chat_request_tools_attributes( tools )
132
+ properties_array_to_object = lambda do | properties |
133
+ return nil unless properties&.any?
134
+ object = {}
135
+ required = []
136
+ properties.each do | property |
137
+ name = property.delete( :name )
138
+ required << name if property.delete( :required )
139
+ if property[ :properties ]&.any?
140
+ property_properties, property_required =
141
+ properties_array_to_object.call( property[ :properties ] )
142
+ property[ :properties ] = property_properties
143
+ property[ :required ] = property_required if property_required.any?
144
+ end
145
+ object[ name ] = property
146
+ end
147
+ [ object, required.compact ]
148
+ end
149
+
150
+ tools&.map do | tool |
151
+ tool_attributes = {
152
+ name: tool[ :name ],
153
+ description: tool[ :description ],
154
+ input_schema: { type: 'object' }
155
+ }
156
+
157
+ if tool[ :properties ]&.any?
158
+ properties_object, properties_required =
159
+ properties_array_to_object.call( tool[ :properties ] )
160
+ input_schema = {
161
+ type: 'object',
162
+ properties: properties_object
163
+ }
164
+ input_schema[ :required ] = properties_required if properties_required.any?
165
+ tool_attributes[ :input_schema ] = input_schema
166
+ end
167
+ tool_attributes
168
+ end
169
+ end
170
+
171
+ private
172
+
173
+ def to_anthropic_system_message( system_message )
174
+ return nil if system_message.nil?
175
+
176
+ # note: the current version of the anthropic api simply takes a string as the
177
+ # system message but the beta version requires an array of objects akin
178
+ # to message contents.
179
+ result = ''
180
+ system_message[ :contents ].each do | content |
181
+ result += content[ :text ] if content[ :type ] == :text
182
+ end
183
+
184
+ result.empty? ? nil : result
185
+ end
186
+
187
+ end
188
+ end
189
+ end
@@ -1,112 +1,6 @@
1
- require 'base64'
2
-
3
1
  module Intelligence
4
2
  module Anthropic
5
- module ChatMethods
6
-
7
- CHAT_REQUEST_URI = "https://api.anthropic.com/v1/messages"
8
-
9
- def chat_request_uri( options )
10
- CHAT_REQUEST_URI
11
- end
12
-
13
- def chat_request_headers( options = {} )
14
- result = {}
15
-
16
- key = options[ :key ] || self.key
17
- version = options[ :version ] || self.version || "2023-06-01"
18
-
19
- raise ArgumentError.new(
20
- "An Anthropic key is required to build an Anthropic chat request."
21
- ) if key.nil?
22
-
23
- result[ 'content-type' ] = 'application/json'
24
- result[ 'x-api-key' ] = "#{key}"
25
- result[ 'anthropic-version' ] = version unless version.nil?
26
-
27
- result
28
- end
29
-
30
- def chat_request_body( conversation, options = {} )
31
- options = self.class.configure( options ) unless options.empty?
32
- result = self.chat_options.merge( options ).compact
33
-
34
- system_message = translate_system_message( conversation[ :system_message ] )
35
- result[ :system ] = system_message unless system_message.nil?
36
- result[ :messages ] = []
37
-
38
- messages = conversation[ :messages ]
39
- length = messages&.length || 0
40
- index = 0; while index < length
41
-
42
- message = messages[ index ]
43
- unless message.nil?
44
-
45
- # The Anthropic API will not accept a sequence of messages where the role of two
46
- # sequentian messages is the same.
47
- #
48
- # The purpose of this code is to identify such occurences and coalece them such
49
- # that the first message in the sequence aggregates the contents of all subsequent
50
- # messages with the same role.
51
- look_ahead_index = index + 1; while look_ahead_index < length
52
- ahead_message = messages[ look_ahead_index ]
53
- unless ahead_message.nil?
54
- if ahead_message[ :role ] == message[ :role ]
55
- message[ :contents ] =
56
- ( message[ :contents ] || [] ) +
57
- ( ahead_message[ :contents ] || [] )
58
- messages[ look_ahead_index ] = nil
59
- look_ahead_index += 1
60
- else
61
- break
62
- end
63
- end
64
- end
65
-
66
- result_message = { role: message[ :role ] }
67
- result_message_content = []
68
-
69
- message[ :contents ]&.each do | content |
70
- case content[ :type ]
71
- when :text
72
- result_message_content << { type: 'text', text: content[ :text ] }
73
- when :binary
74
- content_type = content[ :content_type ]
75
- bytes = content[ :bytes ]
76
- if content_type && bytes
77
- mime_type = MIME::Types[ content_type ].first
78
- if mime_type&.media_type == 'image'
79
- result_message_content << {
80
- type: 'image',
81
- source: {
82
- type: 'base64',
83
- media_type: content_type,
84
- data: Base64.strict_encode64( bytes )
85
- }
86
- }
87
- else
88
- raise UnsupportedContentError.new(
89
- :anthropic,
90
- 'only support content of type image/*'
91
- )
92
- end
93
- else
94
- raise InvalidContentError.new( :anthropic )
95
- end
96
- end
97
- end
98
-
99
- result_message[ :content ] = result_message_content
100
- result[ :messages ] << result_message
101
-
102
- end
103
-
104
- index += 1
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?
@@ -129,6 +23,13 @@ module Intelligence
129
23
  response_json[ :content ].each do | content |
130
24
  if content[ :type ] == 'text'
131
25
  result_content.push( { type: 'text', text: content[ :text ] } )
26
+ elsif content[ :type ] == 'tool_use'
27
+ result_content.push( {
28
+ type: :tool_call,
29
+ tool_call_id: content[ :id ],
30
+ tool_name: content[ :name ],
31
+ tool_parameters: content[ :input ]
32
+ } )
132
33
  end
133
34
  end
134
35
 
@@ -288,23 +189,6 @@ module Intelligence
288
189
 
289
190
  private
290
191
 
291
- def translate_system_message( system_message )
292
-
293
- return nil if system_message.nil?
294
-
295
- # note: the current version of the anthropic api simply takes a string as the
296
- # system message but the beta version requires an array of objects akin
297
- # to message contents.
298
-
299
- result = ''
300
- system_message[ :contents ].each do | content |
301
- result += content[ :text ] if content[ :type ] == :text
302
- end
303
-
304
- result.empty? ? nil : result
305
-
306
- end
307
-
308
192
  def translate_end_result( end_result )
309
193
  case end_result
310
194
  when 'end_turn'
@@ -7,25 +7,25 @@ module Intelligence
7
7
 
8
8
  chat_request_uri "https://api.cerebras.ai/v1/chat/completions"
9
9
 
10
- configuration do
11
- parameter :key, required: true
12
- group :chat_options do
13
- parameter :model, String, required: true
14
- parameter :max_tokens, Integer, required: true
15
- parameter :response_format do
16
- parameter :type, String, default: 'json_schema'
17
- parameter :json_schema
10
+ schema do
11
+ key String
12
+ chat_options do
13
+ model String
14
+ max_tokens Integer
15
+ response_format do
16
+ type String, default: 'json_schema'
17
+ json_schema
18
18
  end
19
- parameter :seed, Integer
20
- parameter :stop, array: true
21
- parameter :stream, [ TrueClass, FalseClass ]
22
- parameter :temperature, Float
23
- parameter :top_p, Float
24
- parameter :tool_choice do
25
- parameter :type, String
26
- parameter :mame, String
19
+ seed Integer
20
+ stop array: true
21
+ stream [ TrueClass, FalseClass ]
22
+ temperature Float
23
+ top_p Float
24
+ tool_choice do
25
+ type String
26
+ mame String
27
27
  end
28
- parameter :user
28
+ user
29
29
  end
30
30
  end
31
31
 
@@ -4,18 +4,7 @@ require_relative 'chat_methods'
4
4
  module Intelligence
5
5
  module Generic
6
6
  class Adapter < Adapter::Base
7
-
8
- attr_reader :key
9
- attr_reader :chat_options
10
-
11
- def initialize( attributes = nil, &block )
12
- configuration = self.class.configure( attributes, &block )
13
- @key = configuration[ :key ]
14
- @chat_options = configuration[ :chat_options ] || {}
15
- end
16
-
17
7
  include ChatMethods
18
-
19
8
  end
20
9
  end
21
- end
10
+ end
@@ -20,10 +20,11 @@ module Intelligence
20
20
  self.class.chat_request_uri
21
21
  end
22
22
 
23
- def chat_request_headers( options = {} )
23
+ def chat_request_headers( options = nil )
24
+ options = @options.merge( build_options( options ) )
24
25
  result = {}
25
26
 
26
- key = options[ :key ] || self.key
27
+ key = options[ :key ]
27
28
 
28
29
  raise ArgumentError.new( "An API key is required to build a chat request." ) \
29
30
  if key.nil?
@@ -34,8 +35,10 @@ module Intelligence
34
35
  result
35
36
  end
36
37
 
37
- def chat_request_body( conversation, options = {} )
38
- result = self.chat_options.merge( options ).compact
38
+ def chat_request_body( conversation, options = nil )
39
+ options = @options.merge( build_options( options ) )
40
+
41
+ result = options[ :chat_options ]
39
42
  result[ :messages ] = []
40
43
 
41
44
  system_message = system_message_to_s( conversation[ :system_message ] )
@@ -79,6 +82,28 @@ module Intelligence
79
82
  'requires binary content to include content type and ( packed ) bytes'
80
83
  )
81
84
  end
85
+ when :file
86
+ content_type = content[ :content_type ]
87
+ uri = content[ :uri ]
88
+ if content_type && uri
89
+ mime_type = MIME::Types[ content_type ].first
90
+ if mime_type&.media_type == 'image'
91
+ result_message_content << {
92
+ type: 'image_url',
93
+ image_url: { url: uri }
94
+ }
95
+ else
96
+ raise UnsupportedContentError.new(
97
+ :generic,
98
+ 'only support content of type image/*'
99
+ )
100
+ end
101
+ else
102
+ raise UnsupportedContentError.new(
103
+ :generic,
104
+ 'requires binary content to include content type and ( packed ) bytes'
105
+ )
106
+ end
82
107
  end
83
108
  end
84
109
  result_message[ :content ] = result_message_content
@@ -124,7 +149,7 @@ module Intelligence
124
149
  end
125
150
 
126
151
  def chat_result_error_attributes( response )
127
-
152
+
128
153
  error_type, error_description = translate_error_response_status( response.status )
129
154
  result = {
130
155
  error_type: error_type.to_s,
@@ -178,7 +203,6 @@ module Intelligence
178
203
 
179
204
  next if line.end_with?( '[DONE]' )
180
205
  data = JSON.parse( line )
181
-
182
206
  if data.is_a?( Hash )
183
207
 
184
208
  data[ 'choices' ]&.each do | data_choice |
@@ -196,10 +220,8 @@ module Intelligence
196
220
  data_choice_content = data_choice_delta[ 'content' ] || ''
197
221
  if last_content.nil? || last_content[ :type ] == :tool_call
198
222
  contents.push( { type: :text, text: data_choice_content } )
199
- elsif last_content[ :type ].nil?
223
+ elsif last_content[ :type ] == :text || last_content[ :type ].nil?
200
224
  last_content[ :type ] = :text
201
- last_content[ :text ] = data_choice_content
202
- elsif last_content[ :type ] == :text
203
225
  last_content[ :text ] = ( last_content[ :text ] || '' ) + data_choice_content
204
226
  end
205
227
  elsif data_choice_delta.include?( 'function_call' )
@@ -223,7 +245,7 @@ module Intelligence
223
245
  end
224
246
  end
225
247
  choices[ data_choice_index ][ :message ][ :contents ] = contents
226
- choices[ data_choice_index ][ :end_reason ] =
248
+ choices[ data_choice_index ][ :end_reason ] ||=
227
249
  translate_end_result( data_choice_finish_reason )
228
250
  end
229
251
 
@@ -313,7 +335,16 @@ module Intelligence
313
335
  end
314
336
  end
315
337
 
316
- private; def system_message_to_s( system_message )
338
+ private
339
+
340
+ def to_options( options, &block )
341
+ return {} unless options&.any?
342
+ @options_builder ||= DynamicSchema::Builder.new.define( &self.class.schema )
343
+ @options_builder.build( options, &block )
344
+ end
345
+
346
+
347
+ def system_message_to_s( system_message )
317
348
 
318
349
  return nil if system_message.nil?
319
350
 
@@ -1,2 +1,2 @@
1
1
  require_relative '../adapter'
2
- require_relative 'generic/adapter'
2
+ require_relative 'generic/adapter'