intelligence 1.0.0.beta04 → 1.0.0.beta06

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: df9c6c9d4ecd7c8adde5eb20067ceef7f9f88ab5602c862b50635039fad13e81
4
- data.tar.gz: 4af029c32fba747acd6a1fff0daa3e6de6ab049e20240e9dfbcbf4b5c454a867
3
+ metadata.gz: 2882de87c5b4c5e0ce5c59b91ca981788b1435b4e8b68e82c592d67b5b33512e
4
+ data.tar.gz: 92072e12828beec46b8d988c0b4e6ddaa9aac1d9d4cee7fbdf1d2c02cb8ccc83
5
5
  SHA512:
6
- metadata.gz: 7cfdef5d95d37a78f73ad283565f74e706fdcff384c224ff9a92012ee0888179f536f93d34580fd49ee0663800da3066d6174231a5536e917cabde04b1c72ae8
7
- data.tar.gz: 54b476462cf87767cc7b10c95bdc6e149a1b1b4de59ef1795c70761813594d971e8d6690aeeee22756caf8fce8d107edfd97d78b07b7c9db4c2ee7f2981b0ad4
6
+ metadata.gz: 0a80eedb98958100ca842a66352bb43f68123285a3abc503b1a563e9161e237f7bf095541e22272ae4e73c7d738c9ad7aa23ee06d6ee76d4f7059d2c72b98041
7
+ data.tar.gz: b0d7a59ade65eb893e976875d42075aacf8edfe32eada98af10de1ab39a8145913f37cb830a09498345d01cd59e1fc6e8eb511efe72e3e9976fc48c0dd2bec41
data/intelligence.gemspec CHANGED
@@ -37,7 +37,7 @@ Gem::Specification.new do | spec |
37
37
  spec.require_paths = [ "lib" ]
38
38
 
39
39
  spec.add_runtime_dependency 'faraday', '~> 2.7'
40
- spec.add_runtime_dependency 'dynamicschema', '~> 1.0.0.beta03'
40
+ spec.add_runtime_dependency 'dynamicschema', '~> 1.0'
41
41
  spec.add_runtime_dependency 'mime-types', '~> 3.6'
42
42
  spec.add_runtime_dependency 'json-repair', '~> 0.2'
43
43
 
@@ -2,7 +2,7 @@ module Intelligence
2
2
  class AdapterError < Error;
3
3
  def initialize( adapter_type, text )
4
4
  adapter_class_name = adapter_type.to_s.split( '_' ).map( &:capitalize ).join
5
- super( "The #{adapter_class_name} #{text}." )
5
+ super( "The #{adapter_class_name} adapter #{text}." )
6
6
  end
7
7
  end
8
8
  end
@@ -5,10 +5,13 @@ module Intelligence
5
5
 
6
6
  class Adapter < Generic::Adapter
7
7
 
8
- chat_request_uri "https://api.cerebras.ai/v1/chat/completions"
8
+ DEFAULT_BASE_URI = 'https://api.cerebras.ai/v1'
9
9
 
10
10
  schema do
11
+
12
+ base_uri String, default: DEFAULT_BASE_URI
11
13
  key String
14
+
12
15
  chat_options do
13
16
  model String
14
17
  max_tokens Integer
@@ -26,8 +29,9 @@ module Intelligence
26
29
  type String
27
30
  mame String
28
31
  end
29
- user
32
+ user String
30
33
  end
34
+
31
35
  end
32
36
 
33
37
  end
@@ -0,0 +1,47 @@
1
+ require_relative '../../adapter'
2
+ require_relative 'chat_request_methods'
3
+ require_relative 'chat_response_methods'
4
+
5
+ module Intelligence
6
+ module Deepseek
7
+ class Adapter < Adapter::Base
8
+ include ChatRequestMethods
9
+ include ChatResponseMethods
10
+
11
+ chat_request_uri 'https://api.deepseek.com/v1/chat/completions'
12
+
13
+ schema do
14
+ key String
15
+ chat_options do
16
+ frequency_penalty Float
17
+ logprobs [ TrueClass, FalseClass ]
18
+ max_tokens Integer
19
+ model String
20
+ presence_penalty Float, in: [ -2..2 ]
21
+ response_format do
22
+ # 'text' and 'json_object' are the only supported types; you must also instruct
23
+ # the model to output json
24
+ type Symbol, in: [ :text, :json_object ]
25
+ end
26
+ stop String, array: true
27
+ stream [ TrueClass, FalseClass ]
28
+ stream_options do
29
+ include_usage [ TrueClass, FalseClass ]
30
+ end
31
+ temperature Float, in: [ 0..2 ]
32
+ tool array: true, as: :tools, &Tool.schema
33
+ tool_choice do
34
+ # one of 'auto', 'none' or 'function'
35
+ type Symbol, in: [ :auto, :none, :required ]
36
+ # the function parameters is required if you specify a type of 'function'
37
+ function do
38
+ name String
39
+ end
40
+ end
41
+ top_logprobs Integer
42
+ top_p Float
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,233 @@
1
+ module Intelligence
2
+ module Deepseek
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
+ CHAT_COMPLETIONS_PATH = 'chat/completions'
16
+
17
+ def self.included( base )
18
+ base.extend( ClassMethods )
19
+ end
20
+
21
+ def chat_request_uri( options = nil )
22
+ options = merge_options( @options, build_options( options ) )
23
+ self.class.chat_request_uri || begin
24
+ base_uri = options[ :base_uri ]
25
+ if base_uri
26
+ # because URI join is dumb
27
+ base_uri += '/' unless base_uri.end_with?( '/' )
28
+ URI.join( base_uri, CHAT_COMPLETIONS_PATH )
29
+ else
30
+ nil
31
+ end
32
+ end
33
+ end
34
+
35
+ def chat_request_headers( options = nil )
36
+ options = merge_options( @options, build_options( options ) )
37
+ result = { 'Content-Type': 'application/json' }
38
+
39
+ key = options[ :key ]
40
+ result[ 'Authorization' ] = "Bearer #{key}" if key
41
+
42
+ result
43
+ end
44
+
45
+ def chat_request_body( conversation, options = nil )
46
+ tools = options.delete( :tools ) || []
47
+
48
+ options = merge_options( @options, build_options( options ) )
49
+
50
+ result = options[ :chat_options ]
51
+ result[ :messages ] = []
52
+
53
+ system_message = chat_request_system_message_attributes( conversation[ :system_message ] )
54
+ result[ :messages ] << system_message if system_message
55
+
56
+ conversation[ :messages ]&.each do | message |
57
+ return nil unless message[ :contents ]&.any?
58
+
59
+ result_message = { role: message[ :role ] }
60
+ result_message_content = []
61
+
62
+ message_contents = message[ :contents ]
63
+
64
+ # tool calls in the open ai api are not content
65
+ tool_calls, message_contents = message_contents.partition do | content |
66
+ content[ :type ] == :tool_call
67
+ end
68
+
69
+ # tool results in the open ai api are not content
70
+ tool_results, message_contents = message_contents.partition do | content |
71
+ content[ :type ] == :tool_result
72
+ end
73
+
74
+ # many vendor api's, especially when hosting text only models, will only accept a single
75
+ # text content item; if the content is only text this will coalece multiple text content
76
+ # items into a single content item
77
+ unless message_contents.any? { | c | c[ :type ] != :text }
78
+ result_message_content = message_contents.map { | c | c[ :text ] || '' }.join( "\n" )
79
+ else
80
+ message_contents&.each do | content |
81
+ result_message_content << chat_request_message_content_attributes( content )
82
+ end
83
+ end
84
+
85
+ if tool_calls.any?
86
+ result_message[ :tool_calls ] = tool_calls.map { | tool_call |
87
+ {
88
+ id: tool_call[ :tool_call_id ],
89
+ type: 'function',
90
+ function: {
91
+ name: tool_call[ :tool_name ],
92
+ arguments: JSON.generate( tool_call[ :tool_parameters ] || {} )
93
+ }
94
+ }
95
+ }
96
+ end
97
+
98
+ result_message[ :content ] = result_message_content
99
+ unless result_message_content.empty? && tool_calls.empty?
100
+ result[ :messages ] << result_message
101
+ end
102
+
103
+ if tool_results.any?
104
+ result[ :messages ].concat( tool_results.map { | tool_result |
105
+ {
106
+ role: :tool,
107
+ tool_call_id: tool_result[ :tool_call_id ],
108
+ content: tool_result[ :tool_result ]
109
+ }
110
+ } )
111
+ end
112
+ end
113
+
114
+ tools_attributes = chat_request_tools_attributes(
115
+ ( result[ :tools ] || [] ).concat( tools )
116
+ )
117
+ result[ :tools ] = tools_attributes if tools_attributes && tools_attributes.length > 0
118
+
119
+ JSON.generate( result )
120
+ end
121
+
122
+ def chat_request_message_content_attributes( content )
123
+ case content[ :type ]
124
+ when :text
125
+ { type: 'text', text: content[ :text ] }
126
+ when :binary
127
+ content_type = content[ :content_type ]
128
+ bytes = content[ :bytes ]
129
+ if content_type && bytes
130
+ mime_type = MIME::Types[ content_type ].first
131
+ if mime_type&.media_type == 'image'
132
+ {
133
+ type: 'image_url',
134
+ image_url: {
135
+ url: "data:#{content_type};base64,#{Base64.strict_encode64( bytes )}".freeze
136
+ }
137
+ }
138
+ else
139
+ raise UnsupportedContentError.new(
140
+ :generic,
141
+ 'only support content of type image/*'
142
+ )
143
+ end
144
+ else
145
+ raise UnsupportedContentError.new(
146
+ :generic,
147
+ 'requires binary content to include content type and ( packed ) bytes'
148
+ )
149
+ end
150
+ when :file
151
+ content_type = content[ :content_type ]
152
+ uri = content[ :uri ]
153
+ if content_type && uri
154
+ mime_type = MIME::Types[ content_type ].first
155
+ if mime_type&.media_type == 'image'
156
+ {
157
+ type: 'image_url',
158
+ image_url: { url: uri }
159
+ }
160
+ else
161
+ raise UnsupportedContentError.new(
162
+ :generic,
163
+ 'only support content of type image/*'
164
+ )
165
+ end
166
+ else
167
+ raise UnsupportedContentError.new(
168
+ :generic,
169
+ 'requires binary content to include content type and ( packed ) bytes'
170
+ )
171
+ end
172
+ end
173
+ end
174
+
175
+ def chat_request_system_message_attributes( system_message )
176
+ return nil if system_message.nil?
177
+
178
+ result = ''
179
+ system_message[ :contents ].each do | content |
180
+ result += content[ :text ] if content[ :type ] == :text
181
+ end
182
+
183
+ result.empty? ? nil : { role: 'system', content: result } if system_message
184
+ end
185
+
186
+ def chat_request_tools_attributes( tools )
187
+ properties_array_to_object = lambda do | properties |
188
+ return nil unless properties&.any?
189
+ object = {}
190
+ required = []
191
+ properties.each do | property |
192
+ name = property.delete( :name )
193
+ required << name if property.delete( :required )
194
+ if property[ :properties ]&.any?
195
+ property_properties, property_required =
196
+ properties_array_to_object.call( property[ :properties ] )
197
+ property[ :properties ] = property_properties
198
+ property[ :required ] = property_required if property_required.any?
199
+ end
200
+ object[ name ] = property
201
+ end
202
+ [ object, required.compact ]
203
+ end
204
+
205
+ tools&.map do | tool |
206
+ function = {
207
+ type: 'function',
208
+ function: {
209
+ type: 'object',
210
+ name: tool[ :name ],
211
+ description: tool[ :description ],
212
+ }
213
+ }
214
+
215
+ if tool[ :properties ]&.any?
216
+ properties_object, properties_required =
217
+ properties_array_to_object.call( tool[ :properties ] )
218
+ function[ :function ][ :parameters ] = {
219
+ type: 'object',
220
+ properties: properties_object
221
+ }
222
+ function[ :function ][ :parameters ][ :required ] = properties_required \
223
+ if properties_required.any?
224
+ else
225
+ # function[ :function ][ :parameters ] = {}
226
+ end
227
+ function
228
+ end
229
+ end
230
+
231
+ end
232
+ end
233
+ end
@@ -0,0 +1,248 @@
1
+ module Intelligence
2
+ module Deepseek
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
+
89
+ buffer += chunk
90
+ while ( eol_index = buffer.index( "\n" ) )
91
+ line = buffer.slice!( 0..eol_index )
92
+ line = line.strip
93
+ next if line.empty? || !line.start_with?( 'data:' )
94
+ line = line[ 6..-1 ]
95
+ next if line.end_with?( '[DONE]' )
96
+
97
+ data = JSON.parse( line ) rescue nil
98
+ if data.is_a?( Hash )
99
+ data[ 'choices' ]&.each do | data_choice |
100
+
101
+ data_choice_index = data_choice[ 'index' ]
102
+ data_choice_delta = data_choice[ 'delta' ]
103
+ data_choice_finish_reason = data_choice[ 'finish_reason' ]
104
+
105
+ choices.fill( { message: {} }, choices.size, data_choice_index + 1 ) \
106
+ if choices.size <= data_choice_index
107
+ contents = choices[ data_choice_index ][ :message ][ :contents ] || []
108
+
109
+ # this deepseek response doesn't place reasoning in a separate index ( presumably
110
+ # because they are simply detecting the thought tag in the first text )
111
+
112
+ if data_choice_content = data_choice_delta[ 'reasoning_content' ]
113
+ thought_content = contents.find { | content | content[ :type ] == :thought }
114
+ if thought_content.nil?
115
+ thought_content = { type: :thought, text: data_choice_content }
116
+ contents << thought_content
117
+ else
118
+ thought_content[ :text ] = ( thought_content[ :text ] || '' ) + data_choice_content
119
+ end
120
+ end
121
+
122
+ if data_choice_content = data_choice_delta[ 'content' ]
123
+ text_content = contents.find { | content | content[ :type ] == :text }
124
+ if text_content.nil?
125
+ text_content = { type: :text, text: data_choice_content }
126
+ contents << text_content
127
+ else
128
+ text_content[ :text ] = ( text_content[ :text ] || '' ) + data_choice_content
129
+ end
130
+ end
131
+
132
+ if data_choice_tool_calls = data_choice_delta[ 'tool_calls' ]
133
+ data_choice_tool_calls.each_with_index do | data_choice_tool_call, data_choice_tool_call_index |
134
+ if data_choice_tool_call_function = data_choice_tool_call[ 'function' ]
135
+ data_choice_tool_index = data_choice_tool_call[ 'index' ] || data_choice_tool_call_index
136
+ data_choice_tool_id = data_choice_tool_call[ 'id' ]
137
+ data_choice_tool_name = data_choice_tool_call_function[ 'name' ]
138
+ data_choice_tool_parameters = data_choice_tool_call_function[ 'arguments' ]
139
+
140
+ if data_choice_tool_id
141
+ contents.push( {
142
+ type: :tool_call,
143
+ tool_call_id: data_choice_tool_id,
144
+ tool_name: data_choice_tool_name,
145
+ tool_parameters: data_choice_tool_parameters
146
+ } )
147
+ else
148
+ tool_call_content_index = contents.rindex do | content |
149
+ content[ :type ] == :tool_call
150
+ end
151
+ tool_call = contents[ tool_call_content_index ]
152
+ tool_call[ :tool_parameters ] = ( tool_call[ :tool_parameters ] || '' ) + data_choice_tool_parameters \
153
+ if data_choice_tool_parameters
154
+ end
155
+ end
156
+ end
157
+ end
158
+ choices[ data_choice_index ][ :message ][ :contents ] = contents
159
+ choices[ data_choice_index ][ :end_reason ] ||=
160
+ to_end_reason( data_choice_finish_reason )
161
+ end
162
+
163
+ if usage = data[ 'usage' ]
164
+ # note: A number of providers will resend the input tokens as part of their usage
165
+ # payload.
166
+ metrics[ :input_tokens ] = usage[ 'prompt_tokens' ] \
167
+ if usage.include?( 'prompt_tokens' )
168
+ metrics[ :output_tokens ] += usage[ 'completion_tokens' ] \
169
+ if usage.include?( 'completion_tokens' )
170
+ end
171
+
172
+ end
173
+
174
+ end
175
+
176
+ context[ :buffer ] = buffer
177
+ context[ :metrics ] = metrics
178
+ context[ :choices ] = choices
179
+
180
+ [ context, choices.empty? ? nil : { choices: choices.dup } ]
181
+ end
182
+
183
+ def stream_result_attributes( context )
184
+ choices = context[ :choices ]
185
+ metrics = context[ :metrics ]
186
+
187
+ choices = choices.map do | choice |
188
+ { end_reason: choice[ :end_reason ] }
189
+ end
190
+
191
+ { choices: choices, metrics: context[ :metrics ] }
192
+ end
193
+
194
+ alias_method :stream_result_error_attributes, :chat_result_error_attributes
195
+
196
+ def to_end_reason( finish_reason )
197
+ case finish_reason
198
+ when 'stop'
199
+ :ended
200
+ when 'length'
201
+ :token_limit_exceeded
202
+ when 'tool_calls'
203
+ :tool_called
204
+ when 'content_filter'
205
+ :filtered
206
+ else
207
+ nil
208
+ end
209
+ end
210
+
211
+ def to_error_response( status )
212
+ case status
213
+ when 400
214
+ [ :invalid_request_error,
215
+ "There was an issue with the format or content of your request." ]
216
+ when 401
217
+ [ :authentication_error,
218
+ "There's an issue with your API key." ]
219
+ when 403
220
+ [ :permission_error,
221
+ "Your API key does not have permission to use the specified resource." ]
222
+ when 404
223
+ [ :not_found_error,
224
+ "The requested resource was not found." ]
225
+ when 413
226
+ [ :request_too_large,
227
+ "Request exceeds the maximum allowed number of bytes." ]
228
+ when 422
229
+ [ :invalid_request_error,
230
+ "There was an issue with the format or content of your request." ]
231
+ when 429
232
+ [ :rate_limit_error,
233
+ "Your account has hit a rate limit." ]
234
+ when 500, 502, 503
235
+ [ :api_error,
236
+ "An unexpected error has occurred internal to the providers systems." ]
237
+ when 529
238
+ [ :overloaded_error,
239
+ "The providers server is temporarily overloaded." ]
240
+ else
241
+ [ :unknown_error, "
242
+ An unknown error occurred." ]
243
+ end
244
+ end
245
+
246
+ end
247
+ end
248
+ end
@@ -1,44 +1 @@
1
- require_relative 'generic/adapter'
2
-
3
- module Intelligence
4
- module Deepseek
5
- class Adapter < Generic::Adapter
6
-
7
- chat_request_uri 'https://api.deepseek.com/v1/chat/completions'
8
-
9
- schema do
10
- key String
11
- chat_options do
12
- frequency_penalty Float
13
- logprobs [ TrueClass, FalseClass ]
14
- max_tokens Integer
15
- model String
16
- presence_penalty Float, in: [ -2..2 ]
17
- response_format do
18
- # 'text' and 'json_object' are the only supported types; you must also instruct
19
- # the model to output json
20
- type Symbol, in: [ :text, :json_object ]
21
- end
22
- stop String, array: true
23
- stream [ TrueClass, FalseClass ]
24
- stream_options do
25
- include_usage [ TrueClass, FalseClass ]
26
- end
27
- temperature Float, in: [ 0..2 ]
28
- tool array: true, as: :tools, &Tool.schema
29
- tool_choice do
30
- # one of 'auto', 'none' or 'function'
31
- type Symbol, in: [ :auto, :none, :required ]
32
- # the function parameters is required if you specify a type of 'function'
33
- function do
34
- name String
35
- end
36
- end
37
- top_logprobs Integer
38
- top_p Float
39
- end
40
- end
41
-
42
- end
43
- end
44
- end
1
+ require_relative 'deepseek/adapter'
@@ -12,25 +12,32 @@ module Intelligence
12
12
  end
13
13
  end
14
14
 
15
+ CHAT_COMPLETIONS_PATH = 'chat/completions'
16
+
15
17
  def self.included( base )
16
18
  base.extend( ClassMethods )
17
19
  end
18
20
 
19
- def chat_request_uri( options )
20
- self.class.chat_request_uri
21
+ def chat_request_uri( options = nil )
22
+ options = merge_options( @options, build_options( options ) )
23
+ self.class.chat_request_uri || begin
24
+ base_uri = options[ :base_uri ]
25
+ if base_uri
26
+ # because URI join is dumb
27
+ base_uri = ( base_uri.end_with?( '/' ) ? base_uri : base_uri + '/' )
28
+ URI.join( base_uri, CHAT_COMPLETIONS_PATH )
29
+ else
30
+ nil
31
+ end
32
+ end
21
33
  end
22
34
 
23
35
  def chat_request_headers( options = nil )
24
36
  options = merge_options( @options, build_options( options ) )
25
- result = {}
37
+ result = { 'Content-Type': 'application/json' }
26
38
 
27
39
  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}"
40
+ result[ 'Authorization' ] = "Bearer #{key}" if key
34
41
 
35
42
  result
36
43
  end
@@ -71,6 +71,7 @@ module Intelligence
71
71
  end
72
72
 
73
73
  def stream_result_chunk_attributes( context, chunk )
74
+
74
75
  context ||= {}
75
76
  buffer = context[ :buffer ] || ''
76
77
  metrics = context[ :metrics ] || {
@@ -99,20 +100,21 @@ module Intelligence
99
100
 
100
101
  data_choice_index = data_choice[ 'index' ]
101
102
  data_choice_delta = data_choice[ 'delta' ]
102
- data_choice_finish_reason = data_choice[ 'finish_reason' ]
103
+ end_reason = to_end_reason( data_choice[ 'finish_reason' ] )
103
104
 
104
105
  choices.fill( { message: {} }, choices.size, data_choice_index + 1 ) \
105
106
  if choices.size <= data_choice_index
106
107
  contents = choices[ data_choice_index ][ :message ][ :contents ] || []
107
108
 
108
- text_content = contents.first&.[]( :type ) == :text ? contents.first : nil
109
109
  if data_choice_content = data_choice_delta[ 'content' ]
110
+ text_content = contents.last&.[]( :type ) == :text ? contents.last : nil
110
111
  if text_content.nil?
111
- contents.unshift( text_content = { type: :text, text: data_choice_content } )
112
+ contents.push( text_content = { type: :text, text: data_choice_content } )
112
113
  else
113
114
  text_content[ :text ] = ( text_content[ :text ] || '' ) + data_choice_content
114
115
  end
115
116
  end
117
+
116
118
  if data_choice_tool_calls = data_choice_delta[ 'tool_calls' ]
117
119
  data_choice_tool_calls.each_with_index do | data_choice_tool_call, data_choice_tool_call_index |
118
120
  if data_choice_tool_call_function = data_choice_tool_call[ 'function' ]
@@ -121,29 +123,29 @@ module Intelligence
121
123
  data_choice_tool_name = data_choice_tool_call_function[ 'name' ]
122
124
  data_choice_tool_parameters = data_choice_tool_call_function[ 'arguments' ]
123
125
 
124
- tool_call_content_index = ( text_content.nil? ? 0 : 1 ) + data_choice_tool_index
125
- if tool_call_content_index >= contents.length
126
+ # if the data_choice_tool_id is present this indicates a new tool call.
127
+ if data_choice_tool_id
126
128
  contents.push( {
127
129
  type: :tool_call,
128
130
  tool_call_id: data_choice_tool_id,
129
131
  tool_name: data_choice_tool_name,
130
132
  tool_parameters: data_choice_tool_parameters
131
133
  } )
134
+ # otherwise the tool is being aggregated
132
135
  else
136
+ tool_call_content_index = contents.rindex do | content |
137
+ content[ :type ] == :tool_call
138
+ end
133
139
  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
140
  tool_call[ :tool_parameters ] = ( tool_call[ :tool_parameters ] || '' ) + data_choice_tool_parameters \
139
141
  if data_choice_tool_parameters
140
142
  end
141
143
  end
142
144
  end
143
145
  end
146
+
144
147
  choices[ data_choice_index ][ :message ][ :contents ] = contents
145
- choices[ data_choice_index ][ :end_reason ] ||=
146
- to_end_reason( data_choice_finish_reason )
148
+ choices[ data_choice_index ][ :end_reason ] ||= end_reason
147
149
  end
148
150
 
149
151
  if usage = data[ 'usage' ]
@@ -164,6 +166,7 @@ module Intelligence
164
166
  context[ :choices ] = choices
165
167
 
166
168
  [ context, choices.empty? ? nil : { choices: choices.dup } ]
169
+
167
170
  end
168
171
 
169
172
  def stream_result_attributes( context )
@@ -17,7 +17,7 @@ module Intelligence
17
17
 
18
18
  SUPPORTED_FILE_MEDIA_TYPES = %w[ text ]
19
19
 
20
- SUPPORTED_CONTENT_TYPES = %w[
20
+ SUPPORTED_FILE_CONTENT_TYPES = %w[
21
21
  image/png image/jpeg image/webp image/heic image/heif
22
22
  video/x-flv video/quicktime video/mpeg video/mpegps video/mpg video/mp4 video/webm
23
23
  video/wmv video/3gpp
@@ -5,11 +5,15 @@ module Intelligence
5
5
 
6
6
  class Adapter < Generic::Adapter
7
7
 
8
- chat_request_uri 'https://api.groq.com/openai/v1/chat/completions'
8
+ DEFAULT_BASE_URI = 'https://api.groq.com/openai/v1'
9
9
 
10
10
  schema do
11
+
12
+ base_uri String, default: DEFAULT_BASE_URI
11
13
  key String
14
+
12
15
  chat_options do
16
+
13
17
  frequency_penalty Float
14
18
  logit_bias
15
19
  logprobs [ TrueClass, FalseClass ]
@@ -25,10 +29,12 @@ module Intelligence
25
29
  end
26
30
  seed Integer
27
31
  stop String, array: true
32
+
28
33
  stream [ TrueClass, FalseClass ]
29
34
  stream_options do
30
35
  include_usage [ TrueClass, FalseClass ]
31
36
  end
37
+
32
38
  temperature Float
33
39
  tool array: true, as: :tools, &Tool.schema
34
40
  tool_choice do
@@ -39,10 +45,13 @@ module Intelligence
39
45
  name String
40
46
  end
41
47
  end
48
+
42
49
  top_logprobs Integer
43
50
  top_p Float
44
51
  user String
52
+
45
53
  end
54
+
46
55
  end
47
56
 
48
57
  end
@@ -5,9 +5,10 @@ module Intelligence
5
5
 
6
6
  class Adapter < Generic::Adapter
7
7
 
8
- chat_request_uri "https://api.hyperbolic.xyz/v1/chat/completions"
8
+ DEFAULT_BASE_URI = 'https://api.hyperbolic.xyz/v1'
9
9
 
10
10
  schema do
11
+ base_uri String, default: DEFAULT_BASE_URI
11
12
  key String
12
13
  chat_options do
13
14
  model String
@@ -15,7 +16,7 @@ module Intelligence
15
16
  top_p Float
16
17
  n Integer
17
18
  max_tokens Integer
18
- stop String, array: true
19
+ stop String, array: true
19
20
  stream [ TrueClass, FalseClass ]
20
21
  frequency_penalty Float
21
22
  presence_penalty Float
@@ -0,0 +1,28 @@
1
+ require_relative 'generic/adapter'
2
+
3
+ module Intelligence
4
+ module Ollama
5
+
6
+ class Adapter < Generic::Adapter
7
+
8
+ DEFAULT_BASE_URI = 'http://localhost:11435/v1'
9
+
10
+ schema do
11
+
12
+ base_uri String, default: DEFAULT_BASE_URI
13
+ key String
14
+
15
+ chat_options do
16
+ max_tokens Integer
17
+ model String
18
+ stop String, array: true
19
+ stream [ TrueClass, FalseClass ]
20
+ temperature Float
21
+ end
22
+
23
+ end
24
+
25
+ end
26
+
27
+ end
28
+ end
@@ -5,11 +5,14 @@ module Intelligence
5
5
 
6
6
  class Adapter < Generic::Adapter
7
7
 
8
- chat_request_uri "https://openrouter.ai/api/v1/chat/completions"
8
+ DEFAULT_BASE_URI = 'https://openrouter.ai/api/v1'
9
9
 
10
10
  schema do
11
+ base_uri String, default: DEFAULT_BASE_URI
11
12
  key String
13
+
12
14
  chat_options do
15
+
13
16
  model String
14
17
  temperature Float
15
18
  top_k Integer
@@ -27,7 +30,9 @@ module Intelligence
27
30
  require_parameters [ TrueClass, FalseClass ]
28
31
  allow_fallbacks [ TrueClass, FalseClass ]
29
32
  end
33
+
30
34
  end
35
+
31
36
  end
32
37
 
33
38
  # def chat_result_error_attributes( response )
@@ -5,11 +5,12 @@ module Intelligence
5
5
 
6
6
  class Adapter < Generic::Adapter
7
7
 
8
- chat_request_uri "https://api.sambanova.ai/v1/chat/completions"
8
+ DEFAULT_BASE_URI = "https://api.sambanova.ai/v1"
9
9
 
10
10
  schema do
11
11
 
12
12
  # normalized properties for all endpoints
13
+ base_uri String, default: DEFAULT_BASE_URI
13
14
  key String
14
15
 
15
16
  # properties for generative text endpoints
@@ -42,12 +43,10 @@ module Intelligence
42
43
  }
43
44
 
44
45
  parsed_body = JSON.parse( response.body, symbolize_names: true ) rescue nil
45
- if parsed_body && parsed_body.respond_to?( :include? ) && parsed_body.include?( :error )
46
- result = {
47
- error_type: error_type.to_s,
48
- error: parsed_body[ :error ][ :code ] || error_type.to_s,
49
- error_description: parsed_body[ :error ][ :message ] || error_description
50
- }
46
+ if parsed_body &&
47
+ parsed_body.respond_to?( :include? ) && parsed_body.include?( :error ) &&
48
+ parsed_body[ :error ].is_a?( String )
49
+ result[ :error_description ] = parsed_body[ :error ]
51
50
  elsif response.headers[ 'content-type' ].start_with?( 'text/plain' ) &&
52
51
  response.body && response.body.length > 0
53
52
  result[ :error_description ] = response.body
@@ -5,9 +5,10 @@ module Intelligence
5
5
 
6
6
  class Adapter < Generic::Adapter
7
7
 
8
- chat_request_uri "https://api.together.xyz/v1/chat/completions"
9
-
8
+ DEFAULT_BASE_URI = "https://api.together.xyz/v1"
9
+
10
10
  schema do
11
+ base_uri String, default: DEFAULT_BASE_URI
11
12
  key String
12
13
  chat_options do
13
14
  model String
@@ -5,10 +5,13 @@ module Intelligence
5
5
 
6
6
  class Adapter < Generic::Adapter
7
7
 
8
- chat_request_uri "https://api.x.ai/v1/chat/completions"
9
-
8
+ DEFAULT_BASE_URI = "https://api.x.ai/v1"
9
+
10
10
  schema do
11
+
12
+ base_uri String, default: DEFAULT_BASE_URI
11
13
  key String
14
+
12
15
  chat_options do
13
16
  model String
14
17
  frequency_penalty Float, in: -2..2
@@ -31,6 +34,7 @@ module Intelligence
31
34
  top_p Float
32
35
 
33
36
  user String
37
+
34
38
  end
35
39
  end
36
40
 
@@ -83,102 +87,6 @@ module Intelligence
83
87
  result
84
88
  end
85
89
 
86
- def stream_result_chunk_attributes( context, chunk )
87
- context ||= {}
88
- buffer = context[ :buffer ] || ''
89
- metrics = context[ :metrics ] || {
90
- input_tokens: 0,
91
- output_tokens: 0
92
- }
93
- choices = context[ :choices ] || Array.new( 1 , { message: {} } )
94
-
95
- choices.each do | choice |
96
- choice[ :message ][ :contents ] = choice[ :message ][ :contents ]&.map do | content |
97
- { type: content[ :type ] }
98
- end
99
- end
100
-
101
- buffer += chunk
102
- while ( eol_index = buffer.index( "\n" ) )
103
- line = buffer.slice!( 0..eol_index )
104
- line = line.strip
105
- next if line.empty? || !line.start_with?( 'data:' )
106
- line = line[ 6..-1 ]
107
- next if line.end_with?( '[DONE]' )
108
-
109
- data = JSON.parse( line ) rescue nil
110
- if data.is_a?( Hash )
111
- data[ 'choices' ]&.each do | data_choice |
112
-
113
- data_choice_index = data_choice[ 'index' ]
114
- data_choice_delta = data_choice[ 'delta' ]
115
- end_reason = to_end_reason( data_choice[ 'finish_reason' ] )
116
-
117
- choices.fill( { message: {} }, choices.size, data_choice_index + 1 ) \
118
- if choices.size <= data_choice_index
119
- contents = choices[ data_choice_index ][ :message ][ :contents ] || []
120
-
121
- text_content = contents.first&.[]( :type ) == :text ? contents.first : nil
122
- if data_choice_content = data_choice_delta[ 'content' ]
123
- if text_content.nil?
124
- contents.unshift( text_content = { type: :text, text: data_choice_content } )
125
- else
126
- text_content[ :text ] = ( text_content[ :text ] || '' ) + data_choice_content
127
- end
128
- end
129
- if data_choice_tool_calls = data_choice_delta[ 'tool_calls' ]
130
- end_reason = :tool_called
131
- data_choice_tool_calls.each_with_index do | data_choice_tool_call, data_choice_tool_call_index |
132
- if data_choice_tool_call_function = data_choice_tool_call[ 'function' ]
133
- data_choice_tool_index = data_choice_tool_call[ 'index' ] || data_choice_tool_call_index
134
- data_choice_tool_id = data_choice_tool_call[ 'id' ]
135
- data_choice_tool_name = data_choice_tool_call_function[ 'name' ]
136
- data_choice_tool_parameters = data_choice_tool_call_function[ 'arguments' ]
137
-
138
- tool_call_content_index = ( text_content.nil? ? 0 : 1 ) + data_choice_tool_index
139
- if tool_call_content_index >= contents.length
140
- contents.push( {
141
- type: :tool_call,
142
- tool_call_id: data_choice_tool_id,
143
- tool_name: data_choice_tool_name,
144
- tool_parameters: data_choice_tool_parameters
145
- } )
146
- else
147
- tool_call = contents[ tool_call_content_index ]
148
- tool_call[ :tool_call_id ] = ( tool_call[ :tool_call_id ] || '' ) + data_choice_tool_id \
149
- if data_choice_tool_id
150
- tool_call[ :tool_name ] = ( tool_call[ :tool_name ] || '' ) + data_choice_tool_name \
151
- if data_choice_tool_name
152
- tool_call[ :tool_parameters ] = ( tool_call[ :tool_parameters ] || '' ) + data_choice_tool_parameters \
153
- if data_choice_tool_parameters
154
- end
155
- end
156
- end
157
- end
158
- choices[ data_choice_index ][ :message ][ :contents ] = contents
159
- choices[ data_choice_index ][ :end_reason ] ||= end_reason
160
- end
161
-
162
- if usage = data[ 'usage' ]
163
- # note: A number of providers will resend the input tokens as part of their usage
164
- # payload.
165
- metrics[ :input_tokens ] = usage[ 'prompt_tokens' ] \
166
- if usage.include?( 'prompt_tokens' )
167
- metrics[ :output_tokens ] += usage[ 'completion_tokens' ] \
168
- if usage.include?( 'completion_tokens' )
169
- end
170
-
171
- end
172
-
173
- end
174
-
175
- context[ :buffer ] = buffer
176
- context[ :metrics ] = metrics
177
- context[ :choices ] = choices
178
-
179
- [ context, choices.empty? ? nil : { choices: choices.dup } ]
180
- end
181
-
182
90
  def chat_result_error_attributes( response )
183
91
  error_type, error_description = to_error_response( response.status )
184
92
  error = error_type
@@ -90,7 +90,7 @@ module Intelligence
90
90
  options[ :tools ] = options[ :tools ].to_a.map!( &:to_h ) if options[ :tools ]
91
91
 
92
92
  uri = @adapter.chat_request_uri( options )
93
- headers = @adapter.chat_request_headers( @options.merge( options ) )
93
+ headers = @adapter.chat_request_headers( options )
94
94
  payload = @adapter.chat_request_body( conversation, options )
95
95
 
96
96
  result_callback = nil
@@ -0,0 +1,30 @@
1
+ module Intelligence
2
+ module MessageContent
3
+
4
+ class Thought < Base
5
+
6
+ schema do
7
+ text String, required: true
8
+ end
9
+
10
+ attr_reader :text
11
+
12
+ def valid?
13
+ true
14
+ end
15
+
16
+ def merge( other )
17
+ other_text = other.text
18
+ text = @text
19
+ text = ( @text || '' ) + other_text if other_text
20
+
21
+ self.class.new( text: text )
22
+ end
23
+
24
+ def to_h
25
+ { type: :text, text: text }
26
+ end
27
+ end
28
+
29
+ end
30
+ end
@@ -1,3 +1,3 @@
1
1
  module Intelligence
2
- VERSION = "1.0.0.beta04"
2
+ VERSION = "1.0.0.beta06"
3
3
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: intelligence
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0.beta04
4
+ version: 1.0.0.beta06
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kristoph Cichocki-Romanov
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-01-21 00:00:00.000000000 Z
10
+ date: 2025-06-08 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: faraday
@@ -29,14 +29,14 @@ dependencies:
29
29
  requirements:
30
30
  - - "~>"
31
31
  - !ruby/object:Gem::Version
32
- version: 1.0.0.beta03
32
+ version: '1.0'
33
33
  type: :runtime
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
- version: 1.0.0.beta03
39
+ version: '1.0'
40
40
  - !ruby/object:Gem::Dependency
41
41
  name: mime-types
42
42
  requirement: !ruby/object:Gem::Requirement
@@ -135,6 +135,9 @@ files:
135
135
  - lib/intelligence/adapters/anthropic/chat_response_methods.rb
136
136
  - lib/intelligence/adapters/cerebras.rb
137
137
  - lib/intelligence/adapters/deepseek.rb
138
+ - lib/intelligence/adapters/deepseek/adapter.rb
139
+ - lib/intelligence/adapters/deepseek/chat_request_methods.rb
140
+ - lib/intelligence/adapters/deepseek/chat_response_methods.rb
138
141
  - lib/intelligence/adapters/generic.rb
139
142
  - lib/intelligence/adapters/generic/adapter.rb
140
143
  - lib/intelligence/adapters/generic/chat_request_methods.rb
@@ -146,6 +149,7 @@ files:
146
149
  - lib/intelligence/adapters/groq.rb
147
150
  - lib/intelligence/adapters/hyperbolic.rb
148
151
  - lib/intelligence/adapters/mistral.rb
152
+ - lib/intelligence/adapters/ollama.rb
149
153
  - lib/intelligence/adapters/open_ai.rb
150
154
  - lib/intelligence/adapters/open_ai/adapter.rb
151
155
  - lib/intelligence/adapters/open_ai/chat_request_methods.rb
@@ -169,6 +173,7 @@ files:
169
173
  - lib/intelligence/message_content/binary.rb
170
174
  - lib/intelligence/message_content/file.rb
171
175
  - lib/intelligence/message_content/text.rb
176
+ - lib/intelligence/message_content/thought.rb
172
177
  - lib/intelligence/message_content/tool_call.rb
173
178
  - lib/intelligence/message_content/tool_result.rb
174
179
  - lib/intelligence/tool.rb