intelligence 0.5.0

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 (46) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/intelligence.gemspec +47 -0
  4. data/lib/intelligence/adapter/base.rb +7 -0
  5. data/lib/intelligence/adapter/construction_methods.rb +37 -0
  6. data/lib/intelligence/adapter.rb +8 -0
  7. data/lib/intelligence/adapter_error.rb +8 -0
  8. data/lib/intelligence/adapters/anthropic/adapter.rb +59 -0
  9. data/lib/intelligence/adapters/anthropic/chat_methods.rb +358 -0
  10. data/lib/intelligence/adapters/anthropic.rb +2 -0
  11. data/lib/intelligence/adapters/cerebras.rb +35 -0
  12. data/lib/intelligence/adapters/generic/adapter.rb +21 -0
  13. data/lib/intelligence/adapters/generic/chat_methods.rb +331 -0
  14. data/lib/intelligence/adapters/generic.rb +2 -0
  15. data/lib/intelligence/adapters/google/adapter.rb +60 -0
  16. data/lib/intelligence/adapters/google/chat_methods.rb +346 -0
  17. data/lib/intelligence/adapters/google.rb +2 -0
  18. data/lib/intelligence/adapters/groq.rb +51 -0
  19. data/lib/intelligence/adapters/hyperbolic.rb +55 -0
  20. data/lib/intelligence/adapters/legacy/adapter.rb +13 -0
  21. data/lib/intelligence/adapters/legacy/chat_methods.rb +37 -0
  22. data/lib/intelligence/adapters/open_ai/adapter.rb +75 -0
  23. data/lib/intelligence/adapters/open_ai/chat_methods.rb +314 -0
  24. data/lib/intelligence/adapters/open_ai.rb +2 -0
  25. data/lib/intelligence/adapters/samba_nova.rb +64 -0
  26. data/lib/intelligence/adapters/together_ai.rb +46 -0
  27. data/lib/intelligence/chat_error_result.rb +11 -0
  28. data/lib/intelligence/chat_metrics.rb +32 -0
  29. data/lib/intelligence/chat_request.rb +117 -0
  30. data/lib/intelligence/chat_result.rb +32 -0
  31. data/lib/intelligence/chat_result_choice.rb +26 -0
  32. data/lib/intelligence/conversation.rb +48 -0
  33. data/lib/intelligence/error.rb +3 -0
  34. data/lib/intelligence/error_result.rb +24 -0
  35. data/lib/intelligence/invalid_content_error.rb +3 -0
  36. data/lib/intelligence/message.rb +53 -0
  37. data/lib/intelligence/message_content/base.rb +18 -0
  38. data/lib/intelligence/message_content/binary.rb +24 -0
  39. data/lib/intelligence/message_content/text.rb +17 -0
  40. data/lib/intelligence/message_content/tool_call.rb +20 -0
  41. data/lib/intelligence/message_content/tool_result.rb +20 -0
  42. data/lib/intelligence/message_content.rb +16 -0
  43. data/lib/intelligence/unsupported_content_error.rb +3 -0
  44. data/lib/intelligence/version.rb +3 -0
  45. data/lib/intelligence.rb +24 -0
  46. metadata +181 -0
@@ -0,0 +1,314 @@
1
+ module Intelligence
2
+ module OpenAi
3
+ module ChatMethods
4
+
5
+ CHAT_REQUEST_URI = "https://api.openai.com/v1/chat/completions"
6
+
7
+ def chat_request_uri( options )
8
+ CHAT_REQUEST_URI
9
+ end
10
+
11
+ def chat_request_headers( options = {} )
12
+ result = {}
13
+
14
+ key = options[ :key ] || self.key
15
+ organization = options[ :organization ] || self.organization
16
+ project = options[ :project ] || self.project
17
+
18
+ raise ArgumentError.new( "An OpenAI key is required to build an OpenAI chat request." ) \
19
+ if key.nil?
20
+
21
+ result[ 'Content-Type' ] = 'application/json'
22
+ result[ 'Authorization' ] = "Bearer #{key}"
23
+ result[ 'OpenAI-Organization' ] = organization unless organization.nil?
24
+ result[ 'OpenAI-Project' ] = project unless project.nil?
25
+
26
+ result
27
+ end
28
+
29
+ def chat_request_body( conversation, options = {} )
30
+ result = self.chat_options.merge( options ).compact
31
+ result[ :messages ] = []
32
+
33
+ system_message = translate_system_message( conversation[ :system_message ] )
34
+ result[ :messages ] << { role: 'system', content: system_message } if system_message
35
+
36
+ conversation[ :messages ]&.each do | message |
37
+
38
+ result_message = { role: message[ :role ] }
39
+ result_message_content = []
40
+
41
+ message[ :contents ]&.each do | content |
42
+ case content[ :type ]
43
+ when :text
44
+ result_message_content << { type: 'text', text: content[ :text ] }
45
+ when :binary
46
+ content_type = content[ :content_type ]
47
+ bytes = content[ :bytes ]
48
+ if content_type && bytes
49
+ mime_type = MIME::Types[ content_type ].first
50
+ if mime_type&.media_type == 'image'
51
+ result_message_content << {
52
+ type: 'image_url',
53
+ image_url: {
54
+ url: "data:#{content_type};base64,#{Base64.strict_encode64( bytes )}".freeze
55
+ }
56
+ }
57
+ else
58
+ raise UnsupportedContentError.new(
59
+ :open_ai,
60
+ 'only supports content of type image/*'
61
+ )
62
+ end
63
+ else
64
+ raise UnsupportedContentError.new(
65
+ :open_ai,
66
+ 'requires binary content to include content type and ( packed ) bytes'
67
+ )
68
+ end
69
+ end
70
+ end
71
+
72
+ result_message[ :content ] = result_message_content
73
+ result[ :messages ] << result_message
74
+
75
+ end
76
+
77
+ JSON.generate( result )
78
+ end
79
+
80
+ def chat_result_attributes( response )
81
+ return nil unless response.success?
82
+
83
+ response_json = JSON.parse( response.body, symbolize_names: true ) rescue nil
84
+ return nil \
85
+ if response_json.nil? || response_json[ :choices ].nil?
86
+
87
+ result = {}
88
+ result[ :choices ] = []
89
+
90
+ ( response_json[ :choices ] || [] ).each do | json_choice |
91
+ json_message = json_choice[ :message ]
92
+ result[ :choices ].push(
93
+ {
94
+ end_reason: translate_end_result( json_choice[ :finish_reason ] ),
95
+ message: {
96
+ role: json_message[ :role ],
97
+ contents: [ { type: 'text', text: json_message[ :content ] } ]
98
+ }
99
+ }
100
+ )
101
+ end
102
+
103
+ metrics_json = response_json[ :usage ]
104
+ unless metrics_json.nil?
105
+
106
+ metrics = {}
107
+ metrics[ :input_tokens ] = metrics_json[ :prompt_tokens ]
108
+ metrics[ :output_tokens ] = metrics_json[ :completion_tokens ]
109
+ metrics = metrics.compact
110
+
111
+ result[ :metrics ] = metrics unless metrics.empty?
112
+
113
+ end
114
+
115
+ result
116
+ end
117
+
118
+ def chat_result_error_attributes( response )
119
+ error_type, error_description = translate_error_response_status( response.status )
120
+ result = {
121
+ error_type: error_type.to_s,
122
+ error_description: error_description
123
+ }
124
+
125
+ parsed_body = JSON.parse( response.body, symbolize_names: true ) rescue nil
126
+ if parsed_body && parsed_body.respond_to?( :include? ) && parsed_body.include?( :error )
127
+ result = {
128
+ error_type: error_type.to_s,
129
+ error: parsed_body[ :error ][ :code ] || error_type.to_s,
130
+ error_description: parsed_body[ :error ][ :message ] || error_description
131
+ }
132
+ end
133
+
134
+ result
135
+ end
136
+
137
+ def stream_result_chunk_attributes( context, chunk )
138
+ context ||= {}
139
+ buffer = context[ :buffer ] || ''
140
+ metrics = context[ :metrics ] || {
141
+ input_tokens: 0,
142
+ output_tokens: 0
143
+ }
144
+ choices = context[ :choices ] || Array.new( 1 , { message: {} } )
145
+
146
+ choices.each do | choice |
147
+ choice[ :message ][ :contents ] = choice[ :message ][ :contents ]&.map do | content |
148
+ case content[ :type ]
149
+ when :text
150
+ content[ :text ] = ''
151
+ when :tool_call
152
+ content[ :tool_parameters ] = ''
153
+ else
154
+ content.clear
155
+ end
156
+ content
157
+ end
158
+ end
159
+
160
+ buffer += chunk
161
+ while ( eol_index = buffer.index( "\n" ) )
162
+
163
+ line = buffer.slice!( 0..eol_index )
164
+ line = line.strip
165
+ next if line.empty? || !line.start_with?( 'data:' )
166
+ line = line[ 6..-1 ]
167
+
168
+ next if line.end_with?( '[DONE]' )
169
+ data = JSON.parse( line )
170
+
171
+ if data.is_a?( Hash )
172
+
173
+ data[ 'choices' ]&.each do | data_choice |
174
+
175
+ data_choice_index = data_choice[ 'index' ]
176
+ data_choice_delta = data_choice[ 'delta' ]
177
+ data_choice_finish_reason = data_choice[ 'finish_reason' ]
178
+
179
+ choices.fill( { message: {} }, choices.size, data_choice_index + 1 ) \
180
+ if choices.size <= data_choice_index
181
+ contents = choices[ data_choice_index ][ :message ][ :contents ] || []
182
+ last_content = contents&.last
183
+
184
+ if data_choice_delta.include?( 'content' )
185
+ data_choice_content = data_choice_delta[ 'content' ] || ''
186
+ if last_content.nil? || last_content[ :type ] == :tool_call
187
+ contents.push( { type: :text, text: data_choice_content } )
188
+ elsif last_content[ :type ].nil?
189
+ last_content[ :type ] = :text
190
+ last_content[ :text ] = data_choice_content
191
+ elsif last_content[ :type ] == :text
192
+ last_content[ :text ] = ( last_content[ :text ] || '' ) + data_choice_content
193
+ end
194
+ elsif data_choice_delta.include?( 'function_call' )
195
+ data_choice_tool_call = data_choice_delta[ 'function_call' ]
196
+ data_choice_tool_name = data_choice_tool_call[ 'name' ]
197
+ data_choice_tool_parameters = data_choice_tool_call[ 'arguments' ]
198
+ if last_content.nil? || last_content[ :type ] == :text
199
+ contents.push( {
200
+ type: :tool_call,
201
+ tool_name: data_choice_tool_name,
202
+ tool_parameters: data_choice_tool_parameters
203
+ } )
204
+ elsif last_content[ :type ].nil?
205
+ last_content[ :type ] = :tool_call
206
+ last_content[ :tool_name ] = data_choice_tool_name if data_choice_tool_name.present?
207
+ last_content[ :tool_parameters ] = tool_parameters
208
+ elsif last_content[ :type ] == :tool_call
209
+ last_content[ :tool_parameters ] =
210
+ ( last_content[ :tool_parameters ] || '' ) + data_choice_tool_parameters
211
+ end
212
+ end
213
+ choices[ data_choice_index ][ :message ][ :contents ] = contents
214
+ choices[ data_choice_index ][ :end_reason ] =
215
+ translate_end_result( data_choice_finish_reason )
216
+ end
217
+
218
+ if usage = data[ 'usage' ]
219
+ metrics[ :input_tokens ] += usage[ 'prompt_tokens' ]
220
+ metrics[ :output_tokens ] += usage[ 'completion_tokens' ]
221
+ end
222
+
223
+ end
224
+
225
+ end
226
+
227
+ context[ :buffer ] = buffer
228
+ context[ :metrics ] = metrics
229
+ context[ :choices ] = choices
230
+
231
+ [ context, choices.empty? ? nil : { choices: choices.dup } ]
232
+ end
233
+
234
+ def stream_result_attributes( context )
235
+ choices = context[ :choices ]
236
+ metrics = context[ :metrics ]
237
+
238
+ choices = choices.map do | choice |
239
+ { end_reason: choice[ :end_reason ] }
240
+ end
241
+
242
+ { choices: choices, metrics: context[ :metrics ] }
243
+ end
244
+
245
+ alias_method :stream_result_error_attributes, :chat_result_error_attributes
246
+
247
+ private; def translate_system_message( system_message )
248
+
249
+ return nil if system_message.nil?
250
+
251
+ result = ''
252
+ system_message[ :contents ].each do | content |
253
+ result += content[ :text ] if content[ :type ] == :text
254
+ end
255
+
256
+ result.empty? ? nil : result
257
+
258
+ end
259
+
260
+ private; def translate_end_result( end_result )
261
+ case end_result
262
+ when 'stop'
263
+ :ended
264
+ when 'length'
265
+ :token_limit_exceeded
266
+ when 'function_call'
267
+ :tool_called
268
+ when 'content_filter'
269
+ :filtered
270
+ else
271
+ nil
272
+ end
273
+ end
274
+
275
+ def translate_error_response_status( status )
276
+ case status
277
+ when 400
278
+ [ :invalid_request_error,
279
+ "There was an issue with the format or content of your request." ]
280
+ when 401
281
+ [ :authentication_error,
282
+ "There's an issue with your API key." ]
283
+ when 403
284
+ [ :permission_error,
285
+ "Your API key does not have permission to use the specified resource." ]
286
+ when 404
287
+ [ :not_found_error,
288
+ "The requested resource was not found." ]
289
+ when 413
290
+ [ :request_too_large,
291
+ "Request exceeds the maximum allowed number of bytes." ]
292
+ when 422
293
+ [ :invalid_request_error,
294
+ "There was an issue with the format or content of your request." ]
295
+ when 429
296
+ [ :rate_limit_error,
297
+ "Your account has hit a rate limit." ]
298
+ when 500, 502, 503
299
+ [ :api_error,
300
+ "An unexpected error has occurred internal to the providers systems." ]
301
+ when 529
302
+ [ :overloaded_error,
303
+ "The providers server is temporarily overloaded." ]
304
+ else
305
+ [ :unknown_error, "
306
+ An unknown error occurred." ]
307
+ end
308
+ end
309
+
310
+ end
311
+
312
+ end
313
+
314
+ end
@@ -0,0 +1,2 @@
1
+ require_relative '../adapter'
2
+ require_relative 'open_ai/adapter'
@@ -0,0 +1,64 @@
1
+ require_relative 'legacy/adapter'
2
+
3
+ module Intelligence
4
+ module SambaNova
5
+
6
+ class Adapter < Legacy::Adapter
7
+
8
+ chat_request_uri "https://api.sambanova.ai/v1/chat/completions"
9
+
10
+ configuration do
11
+
12
+ # normalized properties for all endpoints
13
+ parameter :key, String, required: true
14
+
15
+ # properties for generative text endpoints
16
+ group :chat_options do
17
+
18
+ # normalized properties for samba nova generative text endpoint
19
+ parameter :model, String
20
+ parameter :max_tokens, Integer
21
+ parameter :temperature, Float
22
+ parameter :top_p, Float
23
+ parameter :top_k, Float
24
+ parameter :stop, String, array: true
25
+ parameter :stream, [ TrueClass, FalseClass ]
26
+
27
+ # samba nova properties for samba nova generative text endpoint
28
+ parameter :repetition_penalty, Float
29
+ group :stream_options do
30
+ parameter :include_usage, [ TrueClass, FalseClass ]
31
+ end
32
+
33
+ end
34
+
35
+ end
36
+
37
+ def chat_result_error_attributes( response )
38
+
39
+ error_type, error_description = translate_error_response_status( response.status )
40
+ result = {
41
+ error_type: error_type.to_s,
42
+ error_description: error_description
43
+ }
44
+
45
+ parsed_body = JSON.parse( response.body, symbolize_names: true ) rescue nil
46
+ if parsed_body && parsed_body.respond_to?( :include? ) && parsed_body.include?( :error )
47
+ result = {
48
+ error_type: error_type.to_s,
49
+ error: parsed_body[ :error ][ :code ] || error_type.to_s,
50
+ error_description: parsed_body[ :error ][ :message ] || error_description
51
+ }
52
+ elsif response.headers[ 'content-type' ].start_with?( 'text/plain' ) &&
53
+ response.body && response.body.length > 0
54
+ result[ :error_description ] = response.body
55
+ end
56
+
57
+ result
58
+
59
+ end
60
+
61
+ end
62
+
63
+ end
64
+ end
@@ -0,0 +1,46 @@
1
+ require_relative 'legacy/adapter'
2
+
3
+ module Intelligence
4
+ module TogetherAi
5
+
6
+ class Adapter < Legacy::Adapter
7
+
8
+ chat_request_uri "https://api.together.xyz/v1/chat/completions"
9
+
10
+ configuration do
11
+ parameter :key, String, required: true
12
+ group :chat_options do
13
+ parameter :model, String
14
+ parameter :temperature, Float
15
+ parameter :top_p, Float
16
+ parameter :top_k, Integer
17
+ parameter :n, Integer
18
+ parameter :max_tokens, Float
19
+ parameter :stop, String, array: true
20
+ parameter :stream, [ TrueClass, FalseClass ]
21
+ parameter :frequency_penalty, Float
22
+ parameter :presence_penalty, Float
23
+ parameter :repetition_penalty, Float
24
+ parameter :user, String
25
+ end
26
+ end
27
+
28
+ def translate_end_result( end_result )
29
+ case end_result
30
+ when 'eos'
31
+ :ended
32
+ when 'length'
33
+ :token_limit_exceeded
34
+ when 'stop'
35
+ :end_sequence_encountered
36
+ when 'function_call'
37
+ :tool_called
38
+ else
39
+ nil
40
+ end
41
+ end
42
+
43
+ end
44
+
45
+ end
46
+ end
@@ -0,0 +1,11 @@
1
+ module Intelligence
2
+
3
+ #
4
+ # class. ChatErrorResult
5
+ #
6
+ # The ChatErrorResult class encapsulates error result from a chat request.
7
+ #
8
+ class ChatErrorResult < ErrorResult
9
+ end
10
+
11
+ end
@@ -0,0 +1,32 @@
1
+ module Intelligence
2
+
3
+ #
4
+ # class. ChatMetrics
5
+ #
6
+ # The ChatMetrics class encapsulates metrics information. These metrics include the number of
7
+ # input tokens consumed, the number of output tokens generated, the total number of tokens,
8
+ # and the duration of the request in milliseconds.
9
+ #
10
+ class ChatMetrics
11
+ # ---------------
12
+
13
+ attr_reader :duration
14
+
15
+ attr_reader :input_tokens
16
+ attr_reader :output_tokens
17
+
18
+ def initialize( attributes )
19
+ attributes.each do | key, value |
20
+ instance_variable_set( "@#{key}", value ) if self.respond_to?( "#{key}" )
21
+ end
22
+ end
23
+
24
+ def total_tokens
25
+ @total_tokens = @input_tokens + @output_tokens \
26
+ if @total_tokens.nil? && @input_tokens && @output_tokens
27
+ @total_tokens
28
+ end
29
+
30
+ end
31
+
32
+ end
@@ -0,0 +1,117 @@
1
+ module Intelligence
2
+
3
+ #
4
+ # module. ChatRequestMethods
5
+ #
6
+ # The ChatRequestMethods module extends a Faraday request, adding the +receive_result+ method.
7
+ #
8
+ module ChatRequestMethods
9
+
10
+ def receive_result( &block )
11
+ @_intelligence_result_callback = block
12
+ end
13
+
14
+ end
15
+
16
+ #
17
+ # module. ChatResponseMethods
18
+ #
19
+ # The ChatResponseMethods module extends a Farada reponse, adding the +result+ method.
20
+ #
21
+ module ChatResponseMethods
22
+
23
+ def result
24
+ @_intelligence_result
25
+ end
26
+
27
+ end
28
+
29
+ #
30
+ # class. ChatRequest
31
+ #
32
+ class ChatRequest
33
+
34
+ DEFAULT_CONNECTION = Faraday.new { | builder | builder.adapter Faraday.default_adapter }
35
+
36
+ def initialize( connection: nil, adapter: , **options )
37
+ @connection = connection || DEFAULT_CONNECTION
38
+ @adapter = adapter
39
+ @options = options || {}
40
+
41
+ raise ArgumentError.new( 'An adapter must be configured before a request is constructed.' ) \
42
+ if @adapter.nil?
43
+ end
44
+
45
+ def chat( conversation, options = {} )
46
+
47
+ options = @options.merge( options )
48
+
49
+ uri = @adapter.chat_request_uri( options )
50
+ headers = @adapter.chat_request_headers( @options.merge( options ) )
51
+ payload = @adapter.chat_request_body(
52
+ conversation.to_h,
53
+ options
54
+ )
55
+
56
+ result_callback = nil
57
+ response = @connection.post( uri ) do | request |
58
+ headers.each { | key, value | request.headers[ key ] = value }
59
+ request.body = payload
60
+ yield request.extend( ChatRequestMethods ) if block_given?
61
+ result_callback = request.instance_variable_get( "@_intelligence_result_callback" )
62
+ end
63
+
64
+ result = nil
65
+ if response.success?
66
+ chat_result_attributes = @adapter.chat_result_attributes( response )
67
+ result = ChatResult.new( chat_result_attributes )
68
+ else
69
+ error_result_attributes = @adapter.chat_result_error_attributes( response )
70
+ result = ChatErrorResult.new( error_result_attributes )
71
+ end
72
+
73
+ response.instance_variable_set( "@_intelligence_result", result )
74
+ response.extend( ChatResponseMethods )
75
+
76
+ end
77
+
78
+ def stream( conversation, options = {} )
79
+
80
+ options = @options.merge( options )
81
+
82
+ uri = @adapter.chat_request_uri( options )
83
+ headers = @adapter.chat_request_headers( @options.merge( options ) )
84
+ payload = @adapter.chat_request_body( conversation.to_h, options )
85
+
86
+ context = nil
87
+ response = @connection.post( uri ) do | request |
88
+
89
+ headers.each { | key, value | request.headers[ key ] = value }
90
+ request.body = payload
91
+ yield request.extend( ChatRequestMethods )
92
+
93
+ result_callback = request.instance_variable_get( "@_intelligence_result_callback" )
94
+ request.options.on_data = Proc.new do | chunk, received_bytes |
95
+ context, attributes = @adapter.stream_result_chunk_attributes( context, chunk )
96
+ result_callback.call( ChatResult.new( attributes ) ) unless attributes.nil?
97
+ end
98
+
99
+ end
100
+
101
+ result = nil
102
+ if response.success?
103
+ stream_result_attributes = @adapter.stream_result_attributes( context )
104
+ result = ChatResult.new( stream_result_attributes )
105
+ else
106
+ error_result_attributes = @adapter.stream_result_error_attributes( response )
107
+ result = ChatErrorResult.new( error_result_attributes )
108
+ end
109
+
110
+ response.instance_variable_set( "@_intelligence_result", result )
111
+ response.extend( ChatResponseMethods )
112
+
113
+ end
114
+
115
+ end
116
+
117
+ end
@@ -0,0 +1,32 @@
1
+ module Intelligence
2
+
3
+ #
4
+ # class. ChatResult
5
+ #
6
+ # The ChatResult class encapsulates a successful result to a chat or stream request. A result
7
+ # includes an array of choices ( it is an array even if there is a single choice ) and the
8
+ # metrics associated with the generation of all result choices.
9
+ #
10
+ class ChatResult
11
+
12
+ attr_reader :choices
13
+ attr_reader :metrics
14
+
15
+ def initialize( chat_attributes )
16
+
17
+ @choices = []
18
+ chat_attributes[ :choices ]&.each do | json_choice |
19
+ @choices.push( ChatResultChoice.new( json_choice ) )
20
+ end
21
+ @metrics = ChatMetrics.new( chat_attributes[ :metrics ] ) \
22
+ if chat_attributes.key?( :metrics )
23
+ end
24
+
25
+ def message
26
+ return nil if @choices.empty?
27
+ @choices.first.message
28
+ end
29
+
30
+ end
31
+
32
+ end
@@ -0,0 +1,26 @@
1
+ module Intelligence
2
+
3
+ class ChatResultChoice
4
+
5
+ attr_reader :message
6
+ attr_reader :end_reason
7
+ attr_reader :end_sequence
8
+
9
+ def initialize( chat_choice_attributes )
10
+ @end_reason = chat_choice_attributes[ :end_reason ]
11
+ @end_sequence = chat_choice_attributes[ :end_sequence ]
12
+ @message = build_message( chat_choice_attributes[ :message ] ) \
13
+ if chat_choice_attributes[ :message ]
14
+ end
15
+
16
+ private; def build_message( json_message )
17
+ message = Message.new( json_message[ :role ]&.to_sym || :assistant )
18
+ json_message[ :contents ]&.each do | json_content |
19
+ message << MessageContent.build( json_content[ :type ], json_content )
20
+ end
21
+ message
22
+ end
23
+
24
+ end
25
+
26
+ end
@@ -0,0 +1,48 @@
1
+ module Intelligence
2
+
3
+ #
4
+ # class. Conversation
5
+ #
6
+ class Conversation
7
+
8
+ attr_reader :system_message
9
+ attr_reader :messages
10
+ attr_reader :tools
11
+
12
+ def initialize( attributes = {} )
13
+ @system_message = attributes[ :system_message ]&.dup
14
+ @messages = attributes[ :messages ]&.dup || []
15
+ @tools = attributes[ :tools ]&.dup || []
16
+ end
17
+
18
+ def has_system_message?
19
+ ( @system_message || false ) && !@system_message.empty?
20
+ end
21
+
22
+ def has_messages?
23
+ !@messages.empty?
24
+ end
25
+
26
+ def has_tools?
27
+ !@tools.empty?
28
+ end
29
+
30
+ def system_message=( message )
31
+ raise ArgumentError, "The system message must be a Intelligence::Message." \
32
+ unless message.is_a?( Intelligence::Message )
33
+ raise ArgumentError, "The system message MUST have a role of 'system'." \
34
+ unless message.role == :system
35
+ @system_message = message
36
+ end
37
+
38
+ def to_h
39
+ result = {}
40
+ result[ :system_message ] = @system_message.to_h if @system_message
41
+ result[ :messages ] = @messages.map { | m | m.to_h }
42
+ result[ :tools ] = @tools.map { | t | t.to_h }
43
+ result
44
+ end
45
+
46
+ end
47
+
48
+ end