omniai 1.7.0 → 1.8.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +2 -0
  3. data/README.md +5 -12
  4. data/lib/omniai/chat/choice.rb +68 -0
  5. data/lib/omniai/chat/content.rb +10 -2
  6. data/lib/omniai/chat/file.rb +3 -3
  7. data/lib/omniai/chat/function.rb +57 -0
  8. data/lib/omniai/chat/message/builder.rb +67 -0
  9. data/lib/omniai/chat/message.rb +64 -45
  10. data/lib/omniai/chat/payload.rb +85 -0
  11. data/lib/omniai/chat/prompt.rb +30 -16
  12. data/lib/omniai/chat/response.rb +70 -0
  13. data/lib/omniai/chat/stream.rb +61 -0
  14. data/lib/omniai/chat/text.rb +2 -2
  15. data/lib/omniai/chat/tool_call.rb +54 -0
  16. data/lib/omniai/chat/tool_call_message.rb +61 -0
  17. data/lib/omniai/chat/tool_call_result.rb +51 -0
  18. data/lib/omniai/chat/url.rb +2 -2
  19. data/lib/omniai/chat/usage.rb +60 -0
  20. data/lib/omniai/chat.rb +61 -34
  21. data/lib/omniai/context.rb +12 -0
  22. data/lib/omniai/embed/response.rb +2 -2
  23. data/lib/omniai/tool/parameters.rb +2 -2
  24. data/lib/omniai/tool/property.rb +2 -2
  25. data/lib/omniai/tool.rb +12 -5
  26. data/lib/omniai/version.rb +1 -1
  27. metadata +12 -16
  28. data/lib/omniai/chat/response/choice.rb +0 -35
  29. data/lib/omniai/chat/response/chunk.rb +0 -15
  30. data/lib/omniai/chat/response/completion.rb +0 -15
  31. data/lib/omniai/chat/response/delta.rb +0 -11
  32. data/lib/omniai/chat/response/delta_choice.rb +0 -25
  33. data/lib/omniai/chat/response/function.rb +0 -25
  34. data/lib/omniai/chat/response/message.rb +0 -11
  35. data/lib/omniai/chat/response/message_choice.rb +0 -25
  36. data/lib/omniai/chat/response/part.rb +0 -38
  37. data/lib/omniai/chat/response/payload.rb +0 -72
  38. data/lib/omniai/chat/response/resource.rb +0 -22
  39. data/lib/omniai/chat/response/stream.rb +0 -27
  40. data/lib/omniai/chat/response/tool_call.rb +0 -30
  41. data/lib/omniai/chat/response/usage.rb +0 -35
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 24e77ca1695f294daa0762f9c4c9b32a39ba329d8745d10d8b9fdd5c98917838
4
- data.tar.gz: c6594df5e048bd4c7ea5d04bc3a66a01a2311ff2ff36ec5ee60db22afca7e412
3
+ metadata.gz: cca8dd5cc19989ca0a3fc89d1e8f2c5a56611378a202ea8da2fd49528454ea6c
4
+ data.tar.gz: ee0abcf49b13df01ed5b63ed557de44d42d640fb966bcfb5c7592e19aff06f93
5
5
  SHA512:
6
- metadata.gz: 274c978038080fbbb760dc3341a6ffdd103bd7ac176511189f1f36d8287084800dd5fdbb098138a3bb2b4405e7272763423b9815f1c2d1ca735a3f55f16271a4
7
- data.tar.gz: 39e42f5e5ea5031653b2d6474326c767183270f6ca3dd1ff0b873c64fdc1999df1bcc58015457f5c8673b1c4544ef4d4fe39c72e7eb450951a839a5cc27b7382
6
+ metadata.gz: eb48e8f76d7cd1836ad8dc0ceb577362a3f289026749e2cc49044e31d25758a625ff66fe8003be12dc09585d9ee571671dd4f6d0b8c355cc7962eb4c136ec9f0
7
+ data.tar.gz: 41cf6419a3cf3c1662e2f6c5925ae5a05e991f6f2e8f81bcf3d5dfb62ad029bf3d6c32565892ef81acc651a43533dc3a7e9efb2c75922bbdddf28bb29e18b3c2
data/Gemfile CHANGED
@@ -4,11 +4,13 @@ source 'https://rubygems.org'
4
4
 
5
5
  gemspec
6
6
 
7
+ gem 'factory_bot'
7
8
  gem 'logger'
8
9
  gem 'rake'
9
10
  gem 'rspec'
10
11
  gem 'rspec_junit_formatter'
11
12
  gem 'rubocop'
13
+ gem 'rubocop-factory_bot'
12
14
  gem 'rubocop-rake'
13
15
  gem 'rubocop-rspec'
14
16
  gem 'simplecov'
data/README.md CHANGED
@@ -128,7 +128,7 @@ Generating a completion is as simple as sending in the text:
128
128
 
129
129
  ```ruby
130
130
  completion = client.chat('Tell me a joke.')
131
- completion.choice.message.content # 'Why don't scientists trust atoms? They make up everything!'
131
+ completion.text # 'Why don't scientists trust atoms? They make up everything!'
132
132
  ```
133
133
 
134
134
  #### Completions using a Complex Prompt
@@ -145,7 +145,7 @@ completion = client.chat do |prompt|
145
145
  message.file('./hamster.jpeg', "image/jpeg")
146
146
  end
147
147
  end
148
- completion.choice.message.content # 'They are photos of a cat, a cat, and a hamster.'
148
+ completion.text # 'They are photos of a cat, a cat, and a hamster.'
149
149
  ```
150
150
 
151
151
  #### Completions using Streaming via Proc
@@ -154,7 +154,7 @@ A real-time stream of messages can be generated by passing in a proc:
154
154
 
155
155
  ```ruby
156
156
  stream = proc do |chunk|
157
- print(chunk.choice.delta.content) # '...'
157
+ print(chunk.text) # '...'
158
158
  end
159
159
  client.chat('Tell me a joke.', stream:)
160
160
  ```
@@ -179,12 +179,12 @@ tool = OmniAI::Tool.new(
179
179
  parameters: OmniAI::Tool::Parameters.new(
180
180
  properties: {
181
181
  location: OmniAI::Tool::Property.string(description: 'e.g. Toronto'),
182
- unit: OmniAI::Tool::Property.string(enum: %w[celcius farenheit]),
182
+ unit: OmniAI::Tool::Property.string(enum: %w[celcius fahrenheit]),
183
183
  },
184
184
  required: %i[location]
185
185
  )
186
186
  )
187
- client.chat('What is the weather in "London" and "Madrid"?', tools: [tool])
187
+ client.chat('What is the weather in "London" in celcius and "Paris" in fahrenheit?', tools: [tool])
188
188
  ```
189
189
 
190
190
  ### Transcribe
@@ -315,10 +315,3 @@ Type 'exit' or 'quit' to abort.
315
315
  0.0
316
316
  ...
317
317
  ```
318
-
319
- 0.0
320
- ...
321
-
322
- ```
323
-
324
- ```
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniAI
4
+ class Chat
5
+ # For for.
6
+ class Choice
7
+ # @return [Integer]
8
+ attr_accessor :index
9
+
10
+ # @return [Message]
11
+ attr_accessor :message
12
+
13
+ # @param message [Message]
14
+ # @param index [Integer]
15
+ # @param tool_call_list [Array<ToolCall>]
16
+ def initialize(message:, index: 0)
17
+ @message = message
18
+ @index = index
19
+ end
20
+
21
+ # @return [String]
22
+ def inspect
23
+ "#<#{self.class.name} index=#{@index} message=#{@message.inspect}>"
24
+ end
25
+
26
+ # @param data [Hash]
27
+ # @param context [OmniAI::Context] optional
28
+ #
29
+ # @return [Choice]
30
+ def self.deserialize(data, context: nil)
31
+ deserialize = context&.deserializer(:choice)
32
+ return deserialize.call(data, context:) if deserialize
33
+
34
+ index = data['index']
35
+ message = Message.deserialize(data['message'] || data['delta'], context:)
36
+
37
+ new(message:, index:)
38
+ end
39
+
40
+ # @param context [OmniAI::Context] optional
41
+ # @return [Hash]
42
+ def serialize(context: nil)
43
+ serialize = context&.serializer(:choice)
44
+ return serialize.call(self, context:) if serialize
45
+
46
+ {
47
+ index:,
48
+ message: message.serialize(context:),
49
+ }
50
+ end
51
+
52
+ # @return [Message]
53
+ def delta
54
+ message
55
+ end
56
+
57
+ # @return [Array<Content>, String]
58
+ def content
59
+ message.content
60
+ end
61
+
62
+ # @return [Array<ToolCall, nil]
63
+ def tool_call_list
64
+ message.tool_call_list
65
+ end
66
+ end
67
+ end
68
+ end
@@ -16,14 +16,22 @@ module OmniAI
16
16
  #
17
17
  # @return [String]
18
18
  def serialize(context: nil)
19
- raise NotImplementedError, ' # {self.class}#serialize undefined'
19
+ raise NotImplementedError, "#{self.class}#serialize undefined"
20
20
  end
21
21
 
22
- # @param data [hash]
22
+ # @param data [Hash, Array, String]
23
23
  # @param context [Context] optional
24
24
  #
25
25
  # @return [Content]
26
26
  def self.deserialize(data, context: nil)
27
+ return data if data.nil?
28
+ return data.map { |data| deserialize(data, context:) } if data.is_a?(Array)
29
+
30
+ deserialize = context&.deserializer(:content)
31
+ return deserialize.call(data, context:) if deserialize
32
+
33
+ return data if data.is_a?(String)
34
+
27
35
  raise ArgumentError, "untyped data=#{data.inspect}" unless data.key?('type')
28
36
 
29
37
  case data['type']
@@ -21,8 +21,8 @@ module OmniAI
21
21
  # @return [String]
22
22
  def fetch!
23
23
  case @io
24
- when IO then @io.read
25
- else ::File.binread(@io)
24
+ when String then ::File.binread(@io)
25
+ else @io.read
26
26
  end
27
27
  end
28
28
 
@@ -33,7 +33,7 @@ module OmniAI
33
33
  content = fetch!
34
34
  Text.new("<file>#{filename}: #{content}</file>").serialize(context:)
35
35
  else
36
- serializer = context&.serializers&.[](:file)
36
+ serializer = context&.serializer(:file)
37
37
  return serializer.call(self, context:) if serializer
38
38
 
39
39
  { type: "#{kind}_url", "#{kind}_url": { url: data_uri } }
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniAI
4
+ class Chat
5
+ # A function that includes a name / arguments.
6
+ class Function
7
+ # @return [String]
8
+ attr_accessor :name
9
+
10
+ # @return [Hash]
11
+ attr_accessor :arguments
12
+
13
+ # @param name [String]
14
+ # @param arguments [Hash]
15
+ def initialize(name:, arguments:)
16
+ @name = name
17
+ @arguments = arguments
18
+ end
19
+
20
+ # @return [String]
21
+ def inspect
22
+ "#<#{self.class.name} name=#{name.inspect} arguments=#{arguments.inspect}>"
23
+ end
24
+
25
+ # @param data [Hash]
26
+ # @param context [Context] optional
27
+ #
28
+ # @return [Function]
29
+ def self.deserialize(data, context: nil)
30
+ deserialize = context&.deserializer(:function)
31
+ return deserialize.call(data, context:) if deserialize
32
+
33
+ name = data['name']
34
+ arguments = begin
35
+ JSON.parse(data['arguments']) if data['arguments']
36
+ rescue JSON::ParserError
37
+ data['arguments']
38
+ end
39
+
40
+ new(name:, arguments:)
41
+ end
42
+
43
+ # @param context [Context] optional
44
+ #
45
+ # @return [Hash]
46
+ def serialize(context: nil)
47
+ serializer = context&.serializer(:function)
48
+ return serializer.call(self, context:) if serializer
49
+
50
+ {
51
+ name: @name,
52
+ arguments: (JSON.generate(@arguments) if @arguments),
53
+ }
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniAI
4
+ class Chat
5
+ class Message
6
+ # Used to build a message.
7
+ class Builder
8
+ # @param role [String]
9
+ # @return [Message]
10
+ def self.build(role:)
11
+ builder = new(role:)
12
+ yield(builder)
13
+ builder.build
14
+ end
15
+
16
+ # @param role [String]
17
+ def initialize(role: Role::USER)
18
+ @role = role
19
+ @content = []
20
+ end
21
+
22
+ # @return [Message]
23
+ def build
24
+ Message.new(content: @content, role: @role)
25
+ end
26
+
27
+ # @example
28
+ # message.text('What are these photos of?')
29
+ #
30
+ # @param value [String]
31
+ #
32
+ # @return [Text]
33
+ def text(value)
34
+ Text.new(value).tap do |text|
35
+ @content << text
36
+ end
37
+ end
38
+
39
+ # @example
40
+ # message.url('https://example.com/hamster.jpg', type: "image/jpeg")
41
+ #
42
+ # @param uri [String]
43
+ # @param type [String]
44
+ #
45
+ # @return [URL]
46
+ def url(uri, type)
47
+ URL.new(uri, type).tap do |url|
48
+ @content << url
49
+ end
50
+ end
51
+
52
+ # @example
53
+ # message.file(File.open('hamster.jpg'), type: "image/jpeg")
54
+ #
55
+ # @param io [IO]
56
+ # @param type [String]
57
+ #
58
+ # @return [File]
59
+ def file(io, type)
60
+ File.new(io, type).tap do |file|
61
+ @content << file
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -10,20 +10,50 @@ module OmniAI
10
10
  # message.url 'https://example.com/cat.jpg', type: "image/jpeg"
11
11
  # message.url 'https://example.com/dog.jpg', type: "image/jpeg"
12
12
  # message.file File.open('hamster.jpg'), type: "image/jpeg"
13
+ # message.
13
14
  # end
14
15
  # end
15
16
  class Message
17
+ # @example
18
+ # prompt.build('What is the capital of Canada?')
19
+ #
20
+ # @example
21
+ # prompt.build(role: 'user') do |message|
22
+ # message.text 'What is the capital of Canada?'
23
+ # end
24
+ #
25
+ # @param content [String, nil]
26
+ # @param role [Symbol]
27
+ #
28
+ # @yield [builder]
29
+ # @yieldparam builder [Message::Builder]
30
+ #
31
+ # @return [Message]
32
+ def self.build(content = nil, role: Role::USER, &block)
33
+ raise ArgumentError, 'content or block is required' if content.nil? && block.nil?
34
+
35
+ Builder.build(role:) do |builder|
36
+ builder.text(content) if content
37
+ block&.call(builder)
38
+ end
39
+ end
40
+
16
41
  # @return [Array<Content>, String]
17
42
  attr_accessor :content
18
43
 
19
44
  # @return [String]
20
45
  attr_accessor :role
21
46
 
47
+ # @return [Array<ToolCall>, nil]
48
+ attr_accessor :tool_call_list
49
+
22
50
  # @param content [String, nil]
23
51
  # @param role [String]
24
- def initialize(content: nil, role: Role::USER)
25
- @content = content || []
52
+ # @param tool_call_list [Array<ToolCall>, nil]
53
+ def initialize(content:, role: Role::USER, tool_call_list: nil)
54
+ @content = content
26
55
  @role = role
56
+ @tool_call_list = tool_call_list
27
57
  end
28
58
 
29
59
  # @return [String]
@@ -48,13 +78,14 @@ module OmniAI
48
78
  #
49
79
  # @return [Message]
50
80
  def self.deserialize(data, context: nil)
51
- deserialize = context&.deserializers&.[](:message)
81
+ deserialize = context&.deserializer(:message)
52
82
  return deserialize.call(data, context:) if deserialize
53
83
 
54
- new(
55
- content: data['content'].map { |content| Content.deserialize(content, context:) },
56
- role: data['role']
57
- )
84
+ role = data['role']
85
+ content = Content.deserialize(data['content'], context:)
86
+ tool_call_list = data['tool_calls']&.map { |subdata| ToolCall.deserialize(subdata, context:) }
87
+
88
+ new(content:, role:, tool_call_list:)
58
89
  end
59
90
 
60
91
  # Usage:
@@ -66,12 +97,13 @@ module OmniAI
66
97
  #
67
98
  # @return [Hash]
68
99
  def serialize(context: nil)
69
- serializer = context&.serializers&.[](:message)
100
+ serializer = context&.serializer(:message)
70
101
  return serializer.call(self, context:) if serializer
71
102
 
72
- content = @content.is_a?(String) ? @content : @content.map { |content| content.serialize(context:) }
103
+ content = @content.is_a?(Array) ? @content.map { |content| content.serialize(context:) } : @content
104
+ tool_calls = @tool_call_list&.map { |tool_call| tool_call.serialize(context:) }
73
105
 
74
- { role: @role, content: }
106
+ { role: @role, content:, tool_calls: }.compact
75
107
  end
76
108
 
77
109
  # @return [Boolean]
@@ -89,45 +121,32 @@ module OmniAI
89
121
  role?(Role::USER)
90
122
  end
91
123
 
92
- # Usage:
93
- #
94
- # message.text('What are these photos of?')
95
- #
96
- # @param value [String]
97
- #
98
- # @return [Text]
99
- def text(value)
100
- Text.new(value).tap do |text|
101
- @content << text
102
- end
124
+ # @return [Boolean]
125
+ def tool?
126
+ role?(Role::TOOL)
103
127
  end
104
128
 
105
- # Usage:
106
- #
107
- # message.url('https://example.com/hamster.jpg', type: "image/jpeg")
108
- #
109
- # @param uri [String]
110
- # @param type [String]
111
- #
112
- # @return [URL]
113
- def url(uri, type)
114
- URL.new(uri, type).tap do |url|
115
- @content << url
116
- end
129
+ # @return [Boolean]
130
+ def text?
131
+ !text.nil?
117
132
  end
118
133
 
119
- # Usage:
120
- #
121
- # message.file(File.open('hamster.jpg'), type: "image/jpeg")
122
- #
123
- # @param io [IO]
124
- # @param type [String]
125
- #
126
- # @return [File]
127
- def file(io, type)
128
- File.new(io, type).tap do |file|
129
- @content << file
130
- end
134
+ # @return [String, nil]
135
+ def text
136
+ return if @content.nil?
137
+ return @content if @content.is_a?(String)
138
+
139
+ parts = arrayify(@content).filter { |content| content.is_a?(Text) }
140
+ parts.map(&:text).join("\n") unless parts.empty?
141
+ end
142
+
143
+ # @param object [Object]
144
+ # @return [Array]
145
+ def arrayify(object)
146
+ return if object.nil?
147
+ return object if object.is_a?(Array)
148
+
149
+ [object]
131
150
  end
132
151
  end
133
152
  end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniAI
4
+ class Chat
5
+ # A chunk or completion.
6
+ class Payload
7
+ # @return [Array<Choice>]
8
+ attr_accessor :choices
9
+
10
+ # @return [Usage, nil]
11
+ attr_accessor :usage
12
+
13
+ # @param choices [Array<Choice>]
14
+ # @param usage [Usage, nil]
15
+ def initialize(choices:, usage: nil)
16
+ @choices = choices
17
+ @usage = usage
18
+ end
19
+
20
+ # @return [String]
21
+ def inspect
22
+ "#<#{self.class.name} choices=#{choices.inspect} usage=#{usage.inspect}>"
23
+ end
24
+
25
+ # @param data [Hash]
26
+ # @param context [OmniAI::Context] optional
27
+ def self.deserialize(data, context: nil)
28
+ deserialize = context&.deserializer(:payload)
29
+ return deserialize.call(data, context:) if deserialize
30
+
31
+ choices = data['choices'].map { |choice_data| Choice.deserialize(choice_data, context:) }
32
+ usage = Usage.deserialize(data['usage'], context:) if data['usage']
33
+
34
+ new(choices:, usage:)
35
+ end
36
+
37
+ # @param context [OmniAI::Context] optional
38
+ # @return [Hash]
39
+ def serialize(context:)
40
+ serialize = context&.serializer(:payload)
41
+ return serialize.call(self, context:) if serialize
42
+
43
+ {
44
+ choices: choices.map { |choice| choice.serialize(context:) },
45
+ usage: usage&.serialize(context:),
46
+ }
47
+ end
48
+
49
+ # @param index [Integer]
50
+ # @return [Choice]
51
+ def choice(index: 0)
52
+ @choices[index]
53
+ end
54
+
55
+ # @param index [Integer]
56
+ # @return [Message]
57
+ def message(index: 0)
58
+ choice(index:).message
59
+ end
60
+
61
+ # @return [Array<Message>]
62
+ def messages
63
+ @choices.map(&:message)
64
+ end
65
+
66
+ # @param index [Integer]
67
+ # @return [String, nil]
68
+ def text(index: 0)
69
+ message(index:).text
70
+ end
71
+
72
+ # @param index [Integer]
73
+ # @return [Boolean]
74
+ def text?(index: 0)
75
+ message(index:).text?
76
+ end
77
+
78
+ # @param index [Integer]
79
+ # @return [Array<ToolCall>]
80
+ def tool_call_list(index:)
81
+ message(index:).tool_call_list
82
+ end
83
+ end
84
+ end
85
+ end
@@ -57,6 +57,11 @@ module OmniAI
57
57
  @messages = messages
58
58
  end
59
59
 
60
+ # @return [Prompt]
61
+ def dup
62
+ self.class.new(messages: @messages.dup)
63
+ end
64
+
60
65
  # @return [String]
61
66
  def inspect
62
67
  "#<#{self.class.name} messages=#{@messages.inspect}>"
@@ -75,57 +80,66 @@ module OmniAI
75
80
  #
76
81
  # @return [Array<Hash>]
77
82
  def serialize(context: nil)
78
- serializer = context&.serializers&.[](:prompt)
83
+ serializer = context&.serializer(:prompt)
79
84
  return serializer.call(self, context:) if serializer
80
85
 
81
86
  @messages.map { |message| message.serialize(context:) }
82
87
  end
83
88
 
84
- # Usage:
89
+ # @example
90
+ # prompt.message 'What is the capital of Canada?', role: '...'
85
91
  #
86
- # prompt.message('What is the capital of Canada?')
92
+ # @example
93
+ # prompt.message role: '...' do |message|
94
+ # message.text 'What is the capital of Canada?'
95
+ # end
87
96
  #
88
97
  # @param content [String, nil]
89
98
  # @param role [Symbol]
90
99
  #
91
- # @yield [Message]
100
+ # @yield [builder]
101
+ # @yieldparam builder [Message::Builder]
102
+ #
92
103
  # @return [Message]
93
- def message(content = nil, role: :user, &block)
94
- raise ArgumentError, 'content or block is required' if content.nil? && block.nil?
95
-
96
- Message.new(content:, role:).tap do |message|
97
- block&.call(message)
104
+ def message(content = nil, role: Role::USER, &)
105
+ Message.build(content, role:, &).tap do |message|
98
106
  @messages << message
99
107
  end
100
108
  end
101
109
 
102
- # Usage:
103
- #
104
- # prompt.system('You are a helpful assistant.')
110
+ # @example
111
+ # prompt.system 'You are a helpful assistant.'
105
112
  #
113
+ # @example
106
114
  # prompt.system do |message|
107
115
  # message.text 'You are a helpful assistant.'
108
116
  # end
109
117
  #
110
118
  # @param content [String, nil]
111
119
  #
112
- # @yield [Message]
120
+ # @yield [builder]
121
+ # @yieldparam builder [Message::Builder]
122
+ #
113
123
  # @return [Message]
114
124
  def system(content = nil, &)
115
125
  message(content, role: Role::SYSTEM, &)
116
126
  end
117
127
 
118
- # Usage:
119
- #
128
+ # @example
120
129
  # prompt.user('What is the capital of Canada?')
121
130
  #
131
+ # @example
122
132
  # prompt.user do |message|
123
133
  # message.text 'What is the capital of Canada?'
134
+ # message.url 'https://...', type: "image/gif"
135
+ # message.file File.open('...'), type: "image/gif"
124
136
  # end
125
137
  #
126
138
  # @param content [String, nil]
127
139
  #
128
- # @yield [Message]
140
+ # @yield [builder]
141
+ # @yieldparam builder [Message::Builder]
142
+ #
129
143
  # @return [Message]
130
144
  def user(content = nil, &)
131
145
  message(content, role: Role::USER, &)