intelligence 0.5.0

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