intelligence 0.7.1 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,234 @@
1
+ module Intelligence
2
+ module Generic
3
+ module ChatResponseMethods
4
+
5
+ def chat_result_attributes( response )
6
+ return nil unless response.success?
7
+ response_json = JSON.parse( response.body, symbolize_names: true ) rescue nil
8
+ return nil if response_json.nil? || response_json[ :choices ].nil?
9
+
10
+ result = {}
11
+ result[ :choices ] = []
12
+
13
+ ( response_json[ :choices ] || [] ).each do | json_choice |
14
+ if ( json_message = json_choice[ :message ] )
15
+ result_message = { role: json_message[ :role ] }
16
+ if json_message[ :content ]
17
+ result_message[ :contents ] = [ { type: :text, text: json_message[ :content ] } ]
18
+ end
19
+ if json_message[ :tool_calls ] && !json_message[ :tool_calls ].empty?
20
+ result_message[ :contents ] ||= []
21
+ json_message[ :tool_calls ].each do | json_message_tool_call |
22
+ result_message_tool_call_parameters =
23
+ JSON.parse( json_message_tool_call[ :function ][ :arguments ], symbolize_names: true ) \
24
+ rescue json_message_tool_call[ :function ][ :arguments ]
25
+ result_message[ :contents ] << {
26
+ type: :tool_call,
27
+ tool_call_id: json_message_tool_call[ :id ],
28
+ tool_name: json_message_tool_call[ :function ][ :name ],
29
+ tool_parameters: result_message_tool_call_parameters
30
+ }
31
+ end
32
+ end
33
+ end
34
+ result[ :choices ].push( {
35
+ end_reason: to_end_reason( json_choice[ :finish_reason ] ),
36
+ message: result_message
37
+ } )
38
+ end
39
+
40
+ metrics_json = response_json[ :usage ]
41
+ unless metrics_json.nil?
42
+
43
+ metrics = {}
44
+ metrics[ :input_tokens ] = metrics_json[ :prompt_tokens ]
45
+ metrics[ :output_tokens ] = metrics_json[ :completion_tokens ]
46
+ metrics = metrics.compact
47
+
48
+ result[ :metrics ] = metrics unless metrics.empty?
49
+
50
+ end
51
+
52
+ result
53
+ end
54
+
55
+ def chat_result_error_attributes( response )
56
+ error_type, error_description = to_error_response( response.status )
57
+ error = error_type
58
+
59
+ parsed_body = JSON.parse( response.body, symbolize_names: true ) rescue nil
60
+ if parsed_body && parsed_body.respond_to?( :[] )
61
+ if parsed_body[ :error ].respond_to?( :[] )
62
+ error = parsed_body[ :error ][ :code ] || error_type
63
+ error_description = parsed_body[ :error ][ :message ] || error_description
64
+ elsif parsed_body[ :object ] == 'error'
65
+ error = parsed_body[ :type ] || error_type
66
+ error_description = parsed_body[ :detail ] || parsed_body[ :message ]
67
+ end
68
+ end
69
+
70
+ { error_type: error_type.to_s, error: error.to_s, error_description: error_description }
71
+ end
72
+
73
+ def stream_result_chunk_attributes( context, chunk )
74
+ context ||= {}
75
+ buffer = context[ :buffer ] || ''
76
+ metrics = context[ :metrics ] || {
77
+ input_tokens: 0,
78
+ output_tokens: 0
79
+ }
80
+ choices = context[ :choices ] || Array.new( 1 , { message: {} } )
81
+
82
+ choices.each do | choice |
83
+ choice[ :message ][ :contents ] = choice[ :message ][ :contents ]&.map do | content |
84
+ { type: content[ :type ] }
85
+ end
86
+ end
87
+
88
+ buffer += chunk
89
+ while ( eol_index = buffer.index( "\n" ) )
90
+ line = buffer.slice!( 0..eol_index )
91
+ line = line.strip
92
+ next if line.empty? || !line.start_with?( 'data:' )
93
+ line = line[ 6..-1 ]
94
+ next if line.end_with?( '[DONE]' )
95
+
96
+ data = JSON.parse( line ) rescue nil
97
+ if data.is_a?( Hash )
98
+ data[ 'choices' ]&.each do | data_choice |
99
+
100
+ data_choice_index = data_choice[ 'index' ]
101
+ data_choice_delta = data_choice[ 'delta' ]
102
+ data_choice_finish_reason = data_choice[ 'finish_reason' ]
103
+
104
+ choices.fill( { message: {} }, choices.size, data_choice_index + 1 ) \
105
+ if choices.size <= data_choice_index
106
+ contents = choices[ data_choice_index ][ :message ][ :contents ] || []
107
+
108
+ text_content = contents.first&.[]( :type ) == :text ? contents.first : nil
109
+ if data_choice_content = data_choice_delta[ 'content' ]
110
+ if text_content.nil?
111
+ contents.unshift( text_content = { type: :text, text: data_choice_content } )
112
+ else
113
+ text_content[ :text ] = ( text_content[ :text ] || '' ) + data_choice_content
114
+ end
115
+ end
116
+ if data_choice_tool_calls = data_choice_delta[ 'tool_calls' ]
117
+ data_choice_tool_calls.each_with_index do | data_choice_tool_call, data_choice_tool_call_index |
118
+ if data_choice_tool_call_function = data_choice_tool_call[ 'function' ]
119
+ data_choice_tool_index = data_choice_tool_call[ 'index' ] || data_choice_tool_call_index
120
+ data_choice_tool_id = data_choice_tool_call[ 'id' ]
121
+ data_choice_tool_name = data_choice_tool_call_function[ 'name' ]
122
+ data_choice_tool_parameters = data_choice_tool_call_function[ 'arguments' ]
123
+
124
+ tool_call_content_index = ( text_content.nil? ? 0 : 1 ) + data_choice_tool_index
125
+ if tool_call_content_index >= contents.length
126
+ contents.push( {
127
+ type: :tool_call,
128
+ tool_call_id: data_choice_tool_id,
129
+ tool_name: data_choice_tool_name,
130
+ tool_parameters: data_choice_tool_parameters
131
+ } )
132
+ else
133
+ tool_call = contents[ tool_call_content_index ]
134
+ tool_call[ :tool_call_id ] = ( tool_call[ :tool_call_id ] || '' ) + data_choice_tool_id \
135
+ if data_choice_tool_id
136
+ tool_call[ :tool_name ] = ( tool_call[ :tool_name ] || '' ) + data_choice_tool_name \
137
+ if data_choice_tool_name
138
+ tool_call[ :tool_parameters ] = ( tool_call[ :tool_parameters ] || '' ) + data_choice_tool_parameters \
139
+ if data_choice_tool_parameters
140
+ end
141
+ end
142
+ end
143
+ end
144
+ choices[ data_choice_index ][ :message ][ :contents ] = contents
145
+ choices[ data_choice_index ][ :end_reason ] ||=
146
+ to_end_reason( data_choice_finish_reason )
147
+ end
148
+
149
+ if usage = data[ 'usage' ]
150
+ # note: A number of providers will resend the input tokens as part of their usage
151
+ # payload.
152
+ metrics[ :input_tokens ] = usage[ 'prompt_tokens' ] \
153
+ if usage.include?( 'prompt_tokens' )
154
+ metrics[ :output_tokens ] += usage[ 'completion_tokens' ] \
155
+ if usage.include?( 'completion_tokens' )
156
+ end
157
+
158
+ end
159
+
160
+ end
161
+
162
+ context[ :buffer ] = buffer
163
+ context[ :metrics ] = metrics
164
+ context[ :choices ] = choices
165
+
166
+ [ context, choices.empty? ? nil : { choices: choices.dup } ]
167
+ end
168
+
169
+ def stream_result_attributes( context )
170
+ choices = context[ :choices ]
171
+ metrics = context[ :metrics ]
172
+
173
+ choices = choices.map do | choice |
174
+ { end_reason: choice[ :end_reason ] }
175
+ end
176
+
177
+ { choices: choices, metrics: context[ :metrics ] }
178
+ end
179
+
180
+ alias_method :stream_result_error_attributes, :chat_result_error_attributes
181
+
182
+ def to_end_reason( finish_reason )
183
+ case finish_reason
184
+ when 'stop'
185
+ :ended
186
+ when 'length'
187
+ :token_limit_exceeded
188
+ when 'tool_calls'
189
+ :tool_called
190
+ when 'content_filter'
191
+ :filtered
192
+ else
193
+ nil
194
+ end
195
+ end
196
+
197
+ def to_error_response( status )
198
+ case status
199
+ when 400
200
+ [ :invalid_request_error,
201
+ "There was an issue with the format or content of your request." ]
202
+ when 401
203
+ [ :authentication_error,
204
+ "There's an issue with your API key." ]
205
+ when 403
206
+ [ :permission_error,
207
+ "Your API key does not have permission to use the specified resource." ]
208
+ when 404
209
+ [ :not_found_error,
210
+ "The requested resource was not found." ]
211
+ when 413
212
+ [ :request_too_large,
213
+ "Request exceeds the maximum allowed number of bytes." ]
214
+ when 422
215
+ [ :invalid_request_error,
216
+ "There was an issue with the format or content of your request." ]
217
+ when 429
218
+ [ :rate_limit_error,
219
+ "Your account has hit a rate limit." ]
220
+ when 500, 502, 503
221
+ [ :api_error,
222
+ "An unexpected error has occurred internal to the providers systems." ]
223
+ when 529
224
+ [ :overloaded_error,
225
+ "The providers server is temporarily overloaded." ]
226
+ else
227
+ [ :unknown_error, "
228
+ An unknown error occurred." ]
229
+ end
230
+ end
231
+
232
+ end
233
+ end
234
+ end
@@ -164,8 +164,9 @@ module Intelligence
164
164
  end
165
165
 
166
166
  tools_attributes = to_google_tools( conversation[ :tools ] )
167
- result[ :tools ] = tools_attributes if tools_attributes && tools_attributes.length > 0
168
-
167
+ result[ :tools ] = [ { function_declarations: tools_attributes } ] \
168
+ if tools_attributes&.any?
169
+
169
170
  JSON.generate( result )
170
171
  end
171
172
 
@@ -208,7 +209,7 @@ module Intelligence
208
209
  [ object, required.compact ]
209
210
  end
210
211
 
211
- return [ { function_declarations: tools&.map { | tool |
212
+ return tools&.map { | tool |
212
213
  function = {
213
214
  name: tool[ :name ],
214
215
  description: tool[ :description ],
@@ -223,7 +224,7 @@ module Intelligence
223
224
  function[ :parameters ][ :required ] = properties_required if properties_required.any?
224
225
  end
225
226
  function
226
- } } ]
227
+ }
227
228
  end
228
229
 
229
230
  end
@@ -1,9 +1,9 @@
1
- require_relative 'legacy/adapter'
1
+ require_relative 'generic/adapter'
2
2
 
3
3
  module Intelligence
4
4
  module Groq
5
5
 
6
- class Adapter < Legacy::Adapter
6
+ class Adapter < Generic::Adapter
7
7
 
8
8
  chat_request_uri 'https://api.groq.com/openai/v1/chat/completions'
9
9
 
@@ -44,25 +44,6 @@ module Intelligence
44
44
  end
45
45
  end
46
46
 
47
- alias chat_request_generic_message_attributes chat_request_message_attributes
48
-
49
- # groq models only support the legacy Open AI message schema for the assistant
50
- # messages while supporting the modern message schema for user messages
51
- def chat_request_message_attributes( message )
52
- role = message[ :role ]&.to_sym
53
- case role
54
- when :user
55
- chat_request_generic_message_attributes( message )
56
- when :assistant
57
- chat_request_legacy_message_attributes( message )
58
- else
59
- raise UnsupportedContentError.new(
60
- :mistral,
61
- 'only supports user and assistant message roles'
62
- )
63
- end
64
- end
65
-
66
47
  end
67
48
 
68
49
  end
@@ -23,32 +23,6 @@ module Intelligence
23
23
  end
24
24
  end
25
25
 
26
- def chat_result_error_attributes( response )
27
-
28
- error_type, error_description = translate_error_response_status( response.status )
29
- result = {
30
- error_type: error_type.to_s,
31
- error_description: error_description
32
- }
33
- parsed_body = JSON.parse( response.body, symbolize_names: true ) rescue nil
34
- if parsed_body && parsed_body.respond_to?( :include? )
35
- if parsed_body.include?( :error )
36
- result = {
37
- error_type: error_type.to_s,
38
- error: parsed_body[ :error ][ :code ] || error_type.to_s,
39
- error_description: parsed_body[ :error ][ :message ] || error_description
40
- }
41
- elsif parsed_body.include?( :detail )
42
- result[ :error_description ] = parsed_body[ :detail ]
43
- elsif parsed_body[ :object ] == 'error'
44
- result[ :error_description ] = parsed_body[ :message ]
45
- end
46
- end
47
-
48
- result
49
-
50
- end
51
-
52
26
  end
53
27
 
54
28
  end
@@ -1,9 +1,8 @@
1
- require_relative 'legacy/adapter'
1
+ require_relative 'generic/adapter'
2
2
 
3
3
  module Intelligence
4
4
  module Mistral
5
-
6
- class Adapter < Legacy::Adapter
5
+ class Adapter < Generic::Adapter
7
6
 
8
7
  chat_request_uri "https://api.mistral.ai/v1/chat/completions"
9
8
 
@@ -31,27 +30,7 @@ module Intelligence
31
30
  end
32
31
  end
33
32
  end
34
-
35
- alias chat_request_generic_message_attributes chat_request_message_attributes
36
-
37
- # mistral vision models only support the legacy Open AI message schema for the assistant
38
- # messages while supporting the modern message schema for user messages :facepalm:
39
- def chat_request_message_attributes( message )
40
- role = message[ :role ]&.to_sym
41
- case role
42
- when :user
43
- chat_request_generic_message_attributes( message )
44
- when :assistant
45
- chat_request_legacy_message_attributes( message )
46
- else
47
- raise UnsupportedContentError.new(
48
- :mistral,
49
- 'only supports user and assistant message roles'
50
- )
51
- end
52
- end
53
-
33
+
54
34
  end
55
-
56
35
  end
57
36
  end
@@ -18,42 +18,42 @@ module Intelligence
18
18
  chat_options do
19
19
 
20
20
  # normalized properties for openai generative text endpoint
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 ]
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
29
 
30
- frequency_penalty Float
31
- presence_penalty Float
30
+ frequency_penalty Float
31
+ presence_penalty Float
32
32
 
33
33
  # openai variant of normalized properties for openai generative text endpoints
34
- max_completion_tokens Integer
34
+ max_completion_tokens Integer
35
35
 
36
36
  # openai properties for openai generative text endpoint
37
37
  audio do
38
- voice String
39
- format String
38
+ voice String
39
+ format String
40
40
  end
41
41
  logit_bias
42
- logprobs [ TrueClass, FalseClass ]
43
- modalities String, array: true
42
+ logprobs [ TrueClass, FalseClass ]
43
+ modalities String, array: true
44
44
  # the parallel_tool_calls parameter is only allowed when 'tools' are specified
45
- parallel_tool_calls [ TrueClass, FalseClass ]
45
+ parallel_tool_calls [ TrueClass, FalseClass ]
46
46
  response_format do
47
47
  # 'text' and 'json_schema' are the only supported types
48
- type Symbol, in: [ :text, :json_schema ]
48
+ type Symbol, in: [ :text, :json_schema ]
49
49
  json_schema
50
50
  end
51
- service_tier String
51
+ service_tier String
52
52
  stream_options do
53
- include_usage [ TrueClass, FalseClass ]
53
+ include_usage [ TrueClass, FalseClass ]
54
54
  end
55
55
  tool_choice
56
- top_logprobs Integer
56
+ top_logprobs Integer
57
57
  user
58
58
 
59
59
  end
@@ -122,7 +122,7 @@ module Intelligence
122
122
 
123
123
  tools_attributes = chat_request_tools_attributes( conversation[ :tools ] )
124
124
  result[ :tools ] = tools_attributes if tools_attributes && tools_attributes.length > 0
125
-
125
+
126
126
  JSON.generate( result )
127
127
  end
128
128
 
@@ -85,31 +85,21 @@ module Intelligence
85
85
 
86
86
  choices.each do | choice |
87
87
  choice[ :message ][ :contents ] = choice[ :message ][ :contents ]&.map do | content |
88
- case content[ :type ]
89
- when :text
90
- content[ :text ] = ''
91
- when :tool_call
92
- content[ :tool_parameters ] = ''
93
- else
94
- content.clear
95
- end
96
- content
88
+ { type: content[ :type ] }
97
89
  end
98
90
  end
99
91
 
100
92
  buffer += chunk
101
93
  while ( eol_index = buffer.index( "\n" ) )
102
-
103
94
  line = buffer.slice!( 0..eol_index )
104
95
  line = line.strip
105
96
  next if line.empty? || !line.start_with?( 'data:' )
106
97
  line = line[ 6..-1 ]
107
98
 
108
99
  next if line.end_with?( '[DONE]' )
109
- data = JSON.parse( line )
100
+ data = JSON.parse( line ) rescue nil
110
101
 
111
102
  if data.is_a?( Hash )
112
-
113
103
  data[ 'choices' ]&.each do | data_choice |
114
104
 
115
105
  data_choice_index = data_choice[ 'index' ]
@@ -119,39 +109,45 @@ module Intelligence
119
109
  choices.fill( { message: {} }, choices.size, data_choice_index + 1 ) \
120
110
  if choices.size <= data_choice_index
121
111
  contents = choices[ data_choice_index ][ :message ][ :contents ] || []
122
- last_content = contents&.last
123
-
124
- if data_choice_delta.include?( 'content' )
125
- data_choice_content = data_choice_delta[ 'content' ] || ''
126
- if last_content.nil? || last_content[ :type ] == :tool_call
127
- contents.push( { type: :text, text: data_choice_content } )
128
- elsif last_content[ :type ].nil?
129
- last_content[ :type ] = :text
130
- last_content[ :text ] = data_choice_content
131
- elsif last_content[ :type ] == :text
132
- last_content[ :text ] = ( last_content[ :text ] || '' ) + data_choice_content
133
- end
134
- elsif data_choice_delta.include?( 'function_call' )
135
- data_choice_tool_call = data_choice_delta[ 'function_call' ]
136
- data_choice_tool_name = data_choice_tool_call[ 'name' ]
137
- data_choice_tool_parameters = data_choice_tool_call[ 'arguments' ]
138
- if last_content.nil? || last_content[ :type ] == :text
139
- contents.push( {
140
- type: :tool_call,
141
- tool_name: data_choice_tool_name,
142
- tool_parameters: data_choice_tool_parameters
143
- } )
144
- elsif last_content[ :type ].nil?
145
- last_content[ :type ] = :tool_call
146
- last_content[ :tool_name ] = data_choice_tool_name if data_choice_tool_name.present?
147
- last_content[ :tool_parameters ] = tool_parameters
148
- elsif last_content[ :type ] == :tool_call
149
- last_content[ :tool_parameters ] =
150
- ( last_content[ :tool_parameters ] || '' ) + data_choice_tool_parameters
112
+
113
+ text_content = contents.first&.[]( :type ) == :text ? contents.first : nil
114
+ if data_choice_content = data_choice_delta[ 'content' ]
115
+ if text_content.nil?
116
+ contents.unshift( text_content = { type: :text, text: data_choice_content } )
117
+ else
118
+ text_content[ :text ] = ( text_content[ :text ] || '' ) + data_choice_content
151
119
  end
120
+ end
121
+ if data_choice_tool_calls = data_choice_delta[ 'tool_calls' ]
122
+ data_choice_tool_calls.each do | data_choice_tool_call |
123
+ if data_choice_tool_call_function = data_choice_tool_call[ 'function' ]
124
+ data_choice_tool_index = data_choice_tool_call[ 'index' ]
125
+ data_choice_tool_id = data_choice_tool_call[ 'id' ]
126
+ data_choice_tool_name = data_choice_tool_call_function[ 'name' ]
127
+ data_choice_tool_parameters = data_choice_tool_call_function[ 'arguments' ]
128
+
129
+ tool_call_content_index = ( text_content.nil? ? 0 : 1 ) + data_choice_tool_index
130
+ if tool_call_content_index >= contents.length
131
+ contents.push( {
132
+ type: :tool_call,
133
+ tool_call_id: data_choice_tool_id,
134
+ tool_name: data_choice_tool_name,
135
+ tool_parameters: data_choice_tool_parameters
136
+ } )
137
+ else
138
+ tool_call = contents[ tool_call_content_index ]
139
+ tool_call[ :tool_call_id ] = ( tool_call[ :tool_call_id ] || '' ) + data_choice_tool_id \
140
+ if data_choice_tool_id
141
+ tool_call[ :tool_name ] = ( tool_call[ :tool_name ] || '' ) + data_choice_tool_name \
142
+ if data_choice_tool_name
143
+ tool_call[ :tool_parameters ] = ( tool_call[ :tool_parameters ] || '' ) + data_choice_tool_parameters \
144
+ if data_choice_tool_parameters
145
+ end
146
+ end
147
+ end
152
148
  end
153
149
  choices[ data_choice_index ][ :message ][ :contents ] = contents
154
- choices[ data_choice_index ][ :end_reason ] =
150
+ choices[ data_choice_index ][ :end_reason ] ||=
155
151
  translate_end_result( data_choice_finish_reason )
156
152
  end
157
153
 
@@ -1,9 +1,9 @@
1
- require_relative 'legacy/adapter'
1
+ require_relative 'generic/adapter'
2
2
 
3
3
  module Intelligence
4
4
  module SambaNova
5
5
 
6
- class Adapter < Legacy::Adapter
6
+ class Adapter < Generic::Adapter
7
7
 
8
8
  chat_request_uri "https://api.sambanova.ai/v1/chat/completions"
9
9
 
@@ -35,8 +35,7 @@ module Intelligence
35
35
  end
36
36
 
37
37
  def chat_result_error_attributes( response )
38
-
39
- error_type, error_description = translate_error_response_status( response.status )
38
+ error_type, error_description = to_error_response( response.status )
40
39
  result = {
41
40
  error_type: error_type.to_s,
42
41
  error_description: error_description
@@ -55,7 +54,6 @@ module Intelligence
55
54
  end
56
55
 
57
56
  result
58
-
59
57
  end
60
58
 
61
59
  end
@@ -1,9 +1,9 @@
1
- require_relative 'legacy/adapter'
1
+ require_relative 'generic/adapter'
2
2
 
3
3
  module Intelligence
4
4
  module TogetherAi
5
5
 
6
- class Adapter < Legacy::Adapter
6
+ class Adapter < Generic::Adapter
7
7
 
8
8
  chat_request_uri "https://api.together.xyz/v1/chat/completions"
9
9
 
@@ -25,7 +25,7 @@ module Intelligence
25
25
  end
26
26
  end
27
27
 
28
- def translate_end_result( end_result )
28
+ def to_end_reason( end_result )
29
29
  case end_result
30
30
  when 'eos', 'stop'
31
31
  :ended
@@ -35,7 +35,7 @@ module Intelligence
35
35
  # :end_sequence_encountered
36
36
  when 'length'
37
37
  :token_limit_exceeded
38
- when 'function_call'
38
+ when 'tool_calls'
39
39
  :tool_called
40
40
  else
41
41
  nil
@@ -109,7 +109,7 @@ module Intelligence
109
109
  def text
110
110
  result = []
111
111
  each_content do | content |
112
- result << content.text if content.is_a?( MessageContent::Text )
112
+ result << content.text if content.is_a?( MessageContent::Text ) && content.text
113
113
  end
114
114
  result.join( "\n" )
115
115
  end
@@ -13,6 +13,14 @@ module Intelligence
13
13
  ( text || false ) && text.respond_to?( :empty? ) && !text.empty?
14
14
  end
15
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
+
16
24
  def to_h
17
25
  { type: :text, text: text }
18
26
  end