intelligence 0.5.0 → 0.7.1

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