intelligence 0.5.0 → 0.7.1

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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +555 -0
  3. data/intelligence.gemspec +1 -1
  4. data/lib/intelligence/adapter/base.rb +23 -3
  5. data/lib/intelligence/adapter/class_methods.rb +15 -0
  6. data/lib/intelligence/adapter/{construction_methods.rb → module_methods.rb} +8 -4
  7. data/lib/intelligence/adapter.rb +2 -2
  8. data/lib/intelligence/adapters/anthropic/adapter.rb +21 -30
  9. data/lib/intelligence/adapters/anthropic/chat_request_methods.rb +189 -0
  10. data/lib/intelligence/adapters/anthropic/{chat_methods.rb → chat_response_methods.rb} +8 -124
  11. data/lib/intelligence/adapters/cerebras.rb +17 -17
  12. data/lib/intelligence/adapters/generic/adapter.rb +1 -12
  13. data/lib/intelligence/adapters/generic/chat_methods.rb +42 -11
  14. data/lib/intelligence/adapters/generic.rb +1 -1
  15. data/lib/intelligence/adapters/google/adapter.rb +33 -35
  16. data/lib/intelligence/adapters/google/chat_request_methods.rb +233 -0
  17. data/lib/intelligence/adapters/google/{chat_methods.rb → chat_response_methods.rb} +52 -162
  18. data/lib/intelligence/adapters/groq.rb +46 -28
  19. data/lib/intelligence/adapters/hyperbolic.rb +13 -13
  20. data/lib/intelligence/adapters/legacy/adapter.rb +0 -2
  21. data/lib/intelligence/adapters/legacy/chat_methods.rb +22 -6
  22. data/lib/intelligence/adapters/mistral.rb +57 -0
  23. data/lib/intelligence/adapters/open_ai/adapter.rb +38 -45
  24. data/lib/intelligence/adapters/open_ai/chat_request_methods.rb +186 -0
  25. data/lib/intelligence/adapters/open_ai/{chat_methods.rb → chat_response_methods.rb} +60 -131
  26. data/lib/intelligence/adapters/open_ai.rb +1 -1
  27. data/lib/intelligence/adapters/open_router.rb +62 -0
  28. data/lib/intelligence/adapters/samba_nova.rb +13 -13
  29. data/lib/intelligence/adapters/together_ai.rb +21 -19
  30. data/lib/intelligence/chat_request.rb +57 -7
  31. data/lib/intelligence/chat_result.rb +4 -0
  32. data/lib/intelligence/chat_result_choice.rb +4 -2
  33. data/lib/intelligence/conversation.rb +38 -9
  34. data/lib/intelligence/message.rb +103 -20
  35. data/lib/intelligence/message_content/base.rb +3 -0
  36. data/lib/intelligence/message_content/binary.rb +6 -0
  37. data/lib/intelligence/message_content/file.rb +35 -0
  38. data/lib/intelligence/message_content/text.rb +5 -0
  39. data/lib/intelligence/message_content/tool_call.rb +12 -1
  40. data/lib/intelligence/message_content/tool_result.rb +15 -3
  41. data/lib/intelligence/message_content.rb +12 -3
  42. data/lib/intelligence/tool.rb +139 -0
  43. data/lib/intelligence/version.rb +1 -1
  44. data/lib/intelligence.rb +6 -4
  45. metadata +18 -9
@@ -0,0 +1,62 @@
1
+ require_relative 'generic/adapter'
2
+
3
+ module Intelligence
4
+ module OpenRouter
5
+
6
+ class Adapter < Generic::Adapter
7
+
8
+ chat_request_uri "https://openrouter.ai/api/v1/chat/completions"
9
+
10
+ schema do
11
+ key String
12
+ chat_options do
13
+ model String
14
+ temperature Float
15
+ top_k Integer
16
+ top_p Float
17
+ max_tokens Integer
18
+ seed Integer
19
+ stop String, array: true
20
+ stream [ TrueClass, FalseClass ]
21
+ frequency_penalty Float
22
+ repetition_penalty Float
23
+ presence_penalty Float
24
+
25
+ provider do
26
+ order String, array: true
27
+ require_parameters [ TrueClass, FalseClass ]
28
+ allow_fallbacks [ TrueClass, FalseClass ]
29
+ end
30
+ end
31
+ end
32
+
33
+ # def chat_result_error_attributes( response )
34
+ #
35
+ # error_type, error_description = translate_error_response_status( response.status )
36
+ # result = {
37
+ # error_type: error_type.to_s,
38
+ # error_description: error_description
39
+ # }
40
+ # parsed_body = JSON.parse( response.body, symbolize_names: true ) rescue nil
41
+ # if parsed_body && parsed_body.respond_to?( :include? )
42
+ # if parsed_body.include?( :error )
43
+ # result = {
44
+ # error_type: error_type.to_s,
45
+ # error: parsed_body[ :error ][ :code ] || error_type.to_s,
46
+ # error_description: parsed_body[ :error ][ :message ] || error_description
47
+ # }
48
+ # elsif parsed_body.include?( :detail )
49
+ # result[ :error_description ] = parsed_body[ :detail ]
50
+ # elsif parsed_body[ :object ] == 'error'
51
+ # result[ :error_description ] = parsed_body[ :message ]
52
+ # end
53
+ # end
54
+ #
55
+ # result
56
+ #
57
+ # end
58
+
59
+ end
60
+
61
+ end
62
+ end
@@ -7,27 +7,27 @@ module Intelligence
7
7
 
8
8
  chat_request_uri "https://api.sambanova.ai/v1/chat/completions"
9
9
 
10
- configuration do
10
+ schema do
11
11
 
12
12
  # normalized properties for all endpoints
13
- parameter :key, String, required: true
13
+ key String
14
14
 
15
15
  # properties for generative text endpoints
16
- group :chat_options do
16
+ chat_options do
17
17
 
18
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 ]
19
+ model String
20
+ max_tokens Integer
21
+ temperature Float
22
+ top_p Float
23
+ top_k Float
24
+ stop String, array: true
25
+ stream [ TrueClass, FalseClass ]
26
26
 
27
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 ]
28
+ repetition_penalty Float
29
+ stream_options do
30
+ include_usage [ TrueClass, FalseClass ]
31
31
  end
32
32
 
33
33
  end
@@ -7,32 +7,34 @@ module Intelligence
7
7
 
8
8
  chat_request_uri "https://api.together.xyz/v1/chat/completions"
9
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
10
+ schema do
11
+ key String
12
+ chat_options do
13
+ model String
14
+ temperature Float
15
+ top_p Float
16
+ top_k Integer
17
+ n Integer
18
+ max_tokens Float
19
+ stop String, array: true
20
+ stream [ TrueClass, FalseClass ]
21
+ frequency_penalty Float
22
+ presence_penalty Float
23
+ repetition_penalty Float
24
+ user String
25
25
  end
26
26
  end
27
27
 
28
28
  def translate_end_result( end_result )
29
29
  case end_result
30
- when 'eos'
30
+ when 'eos', 'stop'
31
31
  :ended
32
- when 'length'
32
+ # unfortunatelly eos seems to only work with certain models while others always return
33
+ # stop so for now tomorrow ai will not support :end_sequence_encountered
34
+ # when 'stop'
35
+ # :end_sequence_encountered
36
+ when 'length'
33
37
  :token_limit_exceeded
34
- when 'stop'
35
- :end_sequence_encountered
36
38
  when 'function_call'
37
39
  :tool_called
38
40
  else
@@ -1,7 +1,5 @@
1
1
  module Intelligence
2
2
 
3
- #
4
- # module. ChatRequestMethods
5
3
  #
6
4
  # The ChatRequestMethods module extends a Faraday request, adding the +receive_result+ method.
7
5
  #
@@ -14,9 +12,7 @@ module Intelligence
14
12
  end
15
13
 
16
14
  #
17
- # module. ChatResponseMethods
18
- #
19
- # The ChatResponseMethods module extends a Farada reponse, adding the +result+ method.
15
+ # The ChatResponseMethods module extends a Faraday reponse, adding the +result+ method.
20
16
  #
21
17
  module ChatResponseMethods
22
18
 
@@ -26,13 +22,38 @@ module Intelligence
26
22
 
27
23
  end
28
24
 
25
+ ##
26
+ # The +ChatRequest+ class encapsulates a request to an LLM. After creating a new +ChatRequest+
27
+ # instance you can make the actual request by calling the +chat+ or +stream+ methods. In order
28
+ # to construct a +ChatRequest+ you must first construct and configure an adapter.
29
+ #
30
+ # === example
31
+ #
32
+ # adapter = Intelligence::Adapter.build( :open_ai ) do
33
+ # key ENV[ 'OPENAI_API_KEY' ]
34
+ # chat_options do
35
+ # model 'gpt-4o'
36
+ # max_tokens 512
37
+ # end
38
+ # end
29
39
  #
30
- # class. ChatRequest
40
+ # request = Intelligence::ChatRequest.new( adapter: adapter )
41
+ # response = request.chat( 'Hello!' )
31
42
  #
43
+ # if response.success?
44
+ # puts response.result.text
45
+ # else
46
+ # puts response.result.error_description
47
+ # end
48
+ #
32
49
  class ChatRequest
33
50
 
34
51
  DEFAULT_CONNECTION = Faraday.new { | builder | builder.adapter Faraday.default_adapter }
35
52
 
53
+ ##
54
+ # The +initialize+ method initializes the +ChatRequest+ instance. You MUST pass a previously
55
+ # constructed and configured +adapter+ and optionally a (Faraday) +connection+.
56
+ #
36
57
  def initialize( connection: nil, adapter: , **options )
37
58
  @connection = connection || DEFAULT_CONNECTION
38
59
  @adapter = adapter
@@ -42,8 +63,25 @@ module Intelligence
42
63
  if @adapter.nil?
43
64
  end
44
65
 
66
+ ##
67
+ # The +chat+ method leverages the adapter associated with this +ChatRequest+ instance to
68
+ # construct and make an HTTP request - through Faraday - to an LLM service. The +chat+ method
69
+ # always returns a +Faraday::Respose+ which is augmented with a +result+ method.
70
+ #
71
+ # If the response is successful ( if +response.success?+ returns true ) the +result+ method
72
+ # returns a +ChatResponse+ instance. If the response is not successful a +ChatErrorResult+
73
+ # instance is returned.
74
+ #
75
+ # === arguments
76
+ # * +conversation+ - an instance of +Intelligence::Conversation+ or String; this encapsulates
77
+ # the content to be sent to the LLM
78
+ # * +options+ - a Hash with options; these options overide any of the configuration
79
+ # options used to configure the adapter; you can, for example, pass
80
+ # +{ chat_options: { max_tokens: 1024 }+ to limit the response to 1024
81
+ # tokens.
45
82
  def chat( conversation, options = {} )
46
83
 
84
+ conversation = build_quick_conversation( conversation ) if conversation.is_a?( String )
47
85
  options = @options.merge( options )
48
86
 
49
87
  uri = @adapter.chat_request_uri( options )
@@ -77,6 +115,7 @@ module Intelligence
77
115
 
78
116
  def stream( conversation, options = {} )
79
117
 
118
+ conversation = build_quick_conversation( conversation ) if conversation.is_a?( String )
80
119
  options = @options.merge( options )
81
120
 
82
121
  uri = @adapter.chat_request_uri( options )
@@ -112,6 +151,17 @@ module Intelligence
112
151
 
113
152
  end
114
153
 
154
+ private
155
+
156
+ def build_quick_conversation( text )
157
+ conversation = Conversation.new()
158
+ conversation.messages << Message.build! do
159
+ role :user
160
+ content text: text
161
+ end
162
+ conversation
163
+ end
164
+
115
165
  end
116
166
 
117
- end
167
+ end
@@ -27,6 +27,10 @@ module Intelligence
27
27
  @choices.first.message
28
28
  end
29
29
 
30
+ def text
31
+ return self.message&.text || ''
32
+ end
33
+
30
34
  end
31
35
 
32
36
  end
@@ -13,7 +13,9 @@ module Intelligence
13
13
  if chat_choice_attributes[ :message ]
14
14
  end
15
15
 
16
- private; def build_message( json_message )
16
+ private
17
+
18
+ def build_message( json_message )
17
19
  message = Message.new( json_message[ :role ]&.to_sym || :assistant )
18
20
  json_message[ :contents ]&.each do | json_content |
19
21
  message << MessageContent.build( json_content[ :type ], json_content )
@@ -23,4 +25,4 @@ module Intelligence
23
25
 
24
26
  end
25
27
 
26
- end
28
+ end
@@ -1,18 +1,36 @@
1
1
  module Intelligence
2
-
3
- #
4
- # class. Conversation
5
- #
6
2
  class Conversation
7
3
 
4
+ include DynamicSchema::Definable
5
+ include DynamicSchema::Buildable
6
+
7
+ schema do
8
+ system_message default: { role: :system }, &Message.schema
9
+ message as: :messages, array: true, &Message.schema
10
+ end
11
+
8
12
  attr_reader :system_message
9
13
  attr_reader :messages
10
14
  attr_reader :tools
11
15
 
12
- def initialize( attributes = {} )
13
- @system_message = attributes[ :system_message ]&.dup
14
- @messages = attributes[ :messages ]&.dup || []
15
- @tools = attributes[ :tools ]&.dup || []
16
+ def initialize( attributes = nil )
17
+
18
+ @messages = []
19
+ @tools = []
20
+ if attributes
21
+ if attributes[ :system_message ]&.any?
22
+ system_message = Message.new(
23
+ attributes[ :system_message ][ :role ],
24
+ attributes[ :system_message ]
25
+ )
26
+ @system_message = system_message unless system_message.empty?
27
+ end
28
+
29
+ attributes[ :messages ]&.each do | message_attributes |
30
+ @messages << Message.new( message_attributes[ :role ], message_attributes )
31
+ end
32
+ end
33
+
16
34
  end
17
35
 
18
36
  def has_system_message?
@@ -35,6 +53,18 @@ module Intelligence
35
53
  @system_message = message
36
54
  end
37
55
 
56
+ def append_message( *messages )
57
+ @messages.concat( messages.flatten )
58
+ self
59
+ end
60
+
61
+ alias :<< :append_message
62
+
63
+ def append_tool( *tools )
64
+ @tools.concat( tools.flatten )
65
+ self
66
+ end
67
+
38
68
  def to_h
39
69
  result = {}
40
70
  result[ :system_message ] = @system_message.to_h if @system_message
@@ -44,5 +74,4 @@ module Intelligence
44
74
  end
45
75
 
46
76
  end
47
-
48
77
  end
@@ -1,46 +1,129 @@
1
1
  module Intelligence
2
2
  class Message
3
3
 
4
+ include DynamicSchema::Definable
5
+
4
6
  ROLES = [ :system, :user, :assistant ]
7
+ schema do
8
+ role Symbol, required: true
9
+ content array: true, as: :contents do
10
+ type Symbol, default: :text
11
+
12
+ # note: we replicate these schema elements of the individual content types here to
13
+ # provide more semantic flexibility when building a message; we don't delegate to the
14
+ # individual content type schemas because, unlike for a specific content type, not all
15
+ # attributes are required
16
+
17
+ # text
18
+ text String
19
+ # binary and file
20
+ content_type String
21
+ bytes String
22
+ uri URI
23
+ # tool call and tool result
24
+ tool_call_id String
25
+ tool_name String
26
+ tool_parameters [ Hash, String ]
27
+ tool_result [ Hash, String ]
28
+ end
29
+ end
5
30
 
6
31
  attr_reader :role
7
32
  attr_reader :contents
8
33
 
9
- def initialize( role, content_type = nil, content_attributes = nil )
10
- raise ArgumentError.new( "The role is invalid. It must be one of #{ROLES.join( ', ' )}." ) \
11
- unless ROLES.include?( role.to_sym )
34
+ ##
35
+ # The +build!+ class method constructs and returns a new +Message+ instance. The +build!+
36
+ # method accepts message +attributes+ and a block, which may be combined when constructing
37
+ # a +Message+. The +role+ is required, either as an attribute or in the block. If the +role+
38
+ # is not present, an exception will be raised.
39
+ #
40
+ # The block offers the +role+ method, as well as a +content+ method permiting the caller to
41
+ # set the role and add content respectivelly.
42
+ #
43
+ # Note that there is no corresponding +build+ method because a +Message+ strictly requires a
44
+ # +role+. It cannot be constructed without one.
45
+ #
46
+ # === examples
47
+ #
48
+ # message = Message.build!( role: :user )
49
+ #
50
+ # message = Message.build! do
51
+ # role :user
52
+ # content do
53
+ # type :text
54
+ # text 'this is a user message'
55
+ # end
56
+ # end
57
+ #
58
+ # message = Message.build!( role: :user ) do
59
+ # content text: 'this is a user message'
60
+ # end
61
+ #
62
+ # message = Message.build!( role: :user ) do
63
+ # content text: 'what do you see in this image?'
64
+ # content type: :binary do
65
+ # content_type 'image/png'
66
+ # bytes File.binread( '99_red_balloons.png' )
67
+ # end
68
+ # end
69
+ #
70
+ def self.build!( attributes = nil, &block )
71
+ attributes = self.builder.build!( attributes, &block )
72
+ self.new( attributes[ :role ], attributes )
73
+ end
12
74
 
13
- @role = role.to_sym
75
+ def initialize( role, attributes = nil )
76
+ @role = role&.to_sym
14
77
  @contents = []
15
78
 
16
- @contents << MessageContent.build( content_type, content_attributes ) unless content_type.nil?
79
+ raise ArgumentError.new( "The role is invalid. It must be one of #{ROLES.join( ', ' )}." ) \
80
+ unless ROLES.include?( @role )
81
+
82
+ if attributes && attributes[ :contents ]
83
+ attributes[ :contents ].each do | content |
84
+ @contents << MessageContent.build!( content[ :type ], content )
85
+ end
86
+ end
17
87
  end
18
88
 
19
- def append_content( content )
20
- @contents.push( content ) unless content.nil?
21
- self
89
+ ##
90
+ # The empty? method return true if the message has no content.
91
+ #
92
+ def empty?
93
+ @contents.empty?
22
94
  end
23
95
 
24
- def build_and_append_content( content_type = nil, content_attributes = nil )
25
- append_content(
26
- MessageContent.build( content_type, content_attributes )
27
- )
28
- self
96
+ ##
97
+ # The valid? method returns true if the message has a valid role, has content, and the content
98
+ # is valid.
99
+ #
100
+ def valid?
101
+ ROLES.include?( @role ) && !@contents.empty? && @contents.all?{ | contents | contents.valid? }
102
+ end
103
+
104
+ ##
105
+ # The text method is a convenience that returns all text content in the message joined with
106
+ # a newline. Any non-text content is skipped. If there is no text content an empty string is
107
+ # returned.
108
+ #
109
+ def text
110
+ result = []
111
+ each_content do | content |
112
+ result << content.text if content.is_a?( MessageContent::Text )
113
+ end
114
+ result.join( "\n" )
29
115
  end
30
116
 
31
- alias :<< :append_content
32
-
33
117
  def each_content( &block )
34
118
  @contents.each( &block )
35
119
  end
36
120
 
37
- def empty?
38
- @contents.empty?
121
+ def append_content( content )
122
+ @contents.push( content ) unless content.nil?
123
+ self
39
124
  end
40
125
 
41
- def valid?
42
- !@role.nil? && @contents.all?{ | contents | contents.valid? }
43
- end
126
+ alias :<< :append_content
44
127
 
45
128
  def to_h
46
129
  {
@@ -2,6 +2,9 @@ module Intelligence
2
2
  module MessageContent
3
3
 
4
4
  class Base
5
+ include DynamicSchema::Definable
6
+ include DynamicSchema::Buildable
7
+
5
8
  def initialize( attributes = {} )
6
9
  attributes.each do | key, value |
7
10
  instance_variable_set( "@#{key}", value.freeze ) if self.respond_to?( "#{key}" )
@@ -2,6 +2,12 @@ module Intelligence
2
2
  module MessageContent
3
3
 
4
4
  class Binary < Base
5
+
6
+ schema do
7
+ content_type String, required: true
8
+ bytes String, required: true
9
+ end
10
+
5
11
  attr_reader :content_type
6
12
  attr_reader :bytes
7
13
 
@@ -0,0 +1,35 @@
1
+ module Intelligence
2
+ module MessageContent
3
+
4
+ class File < Base
5
+
6
+ schema do
7
+ content_type String
8
+ uri URI, required: true
9
+ end
10
+
11
+ def initialize( attributes )
12
+ @uri = URI( attributes[ :uri ] ) if attributes[ :uri ]
13
+ @content_type = attributes[ :content_type ]
14
+ end
15
+
16
+ def content_type
17
+ @content_type ||= valid_uri? ? MIME::Types.type_for( @uri.path )&.first&.content_type : nil
18
+ end
19
+
20
+ def valid_uri?( schemes = [ 'http', 'https' ] )
21
+ !!( @uri && schemes.include?( @uri.scheme ) && @uri.path && !@uri.path.empty? )
22
+ end
23
+
24
+ def valid?
25
+ valid_uri? && !MIME::Types[ content_type ].empty?
26
+ end
27
+
28
+ def to_h
29
+ { type: :file, content_type: content_type, uri: @uri.to_s }
30
+ end
31
+
32
+ end
33
+
34
+ end
35
+ end
@@ -2,6 +2,11 @@ module Intelligence
2
2
  module MessageContent
3
3
 
4
4
  class Text < Base
5
+
6
+ schema do
7
+ text String, required: true
8
+ end
9
+
5
10
  attr_reader :text
6
11
 
7
12
  def valid?
@@ -2,10 +2,21 @@ module Intelligence
2
2
  module MessageContent
3
3
 
4
4
  class ToolCall < Base
5
+
6
+ schema do
7
+ tool_call_id String
8
+ tool_name String, required: true
9
+ tool_parameters [ Hash, String ]
10
+ end
11
+
5
12
  attr_reader :tool_call_id
6
13
  attr_reader :tool_name
7
14
  attr_reader :tool_parameters
8
15
 
16
+ def valid?
17
+ tool_name && !tool_name.empty?
18
+ end
19
+
9
20
  def to_h
10
21
  {
11
22
  type: :tool_call,
@@ -17,4 +28,4 @@ module Intelligence
17
28
  end
18
29
 
19
30
  end
20
- end
31
+ end
@@ -2,19 +2,31 @@ module Intelligence
2
2
  module MessageContent
3
3
 
4
4
  class ToolResult < Base
5
+
6
+ schema do
7
+ tool_call_id
8
+ tool_name String, required: true
9
+ tool_result [ Hash, String ]
10
+ end
11
+
5
12
  attr_reader :tool_call_id
6
13
  attr_reader :tool_name
7
14
  attr_reader :tool_result
8
15
 
16
+ def valid?
17
+ tool_call_id && !tool_call_id.empty? &&
18
+ tool_name && !tool_name.empty?
19
+ end
20
+
9
21
  def to_h
10
22
  {
11
- type: :tool_call,
23
+ type: :tool_result,
12
24
  tool_call_id: tool_call_id,
13
25
  tool_name: tool_name,
14
- tool_parameters: tool_result
26
+ tool_result: tool_result
15
27
  }.compact
16
28
  end
17
29
  end
18
30
 
19
31
  end
20
- end
32
+ end
@@ -4,11 +4,20 @@ end
4
4
 
5
5
  module Intelligence
6
6
  module MessageContent
7
-
8
- def self.build( type, attributes )
7
+
8
+ def self.[]( type )
9
9
  type_name = type.to_s.split( '_' ).map { | word | word.capitalize }.join
10
10
  klass = Intelligence.const_get( "MessageContent::#{type_name}" ) rescue nil
11
- klass.nil? ? nil : klass.new( attributes )
11
+ raise TypeError, "An unknown content type '#{type}' was given." unless klass
12
+ klass
13
+ end
14
+
15
+ def self.build!( type, attributes = nil, &block )
16
+ self[ type ].build!( attributes, &block )
17
+ end
18
+
19
+ def self.build( type, attributes = nil, &block )
20
+ self[ type ].build( attributes, &block )
12
21
  end
13
22
 
14
23
  end