intelligence 0.5.0 → 0.7.1

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