langsmith-sdk 0.1.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.
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Langsmith
4
+ # Rails integration for automatic configuration and lifecycle management.
5
+ #
6
+ # When Rails is detected, this Railtie will:
7
+ # - Automatically configure Langsmith from Rails credentials or environment
8
+ # - Register shutdown hooks to flush traces before the application exits
9
+ # - Provide a generator for creating an initializer
10
+ #
11
+ # @example Using Rails credentials (config/credentials.yml.enc)
12
+ # langsmith:
13
+ # api_key: ls_...
14
+ # project: my-rails-app
15
+ #
16
+ # @example Using environment variables
17
+ # LANGSMITH_API_KEY=ls_...
18
+ # LANGSMITH_PROJECT=my-rails-app
19
+ # LANGSMITH_TRACING=true
20
+ #
21
+ class Railtie < ::Rails::Railtie
22
+ config.langsmith = ActiveSupport::OrderedOptions.new
23
+
24
+ # Default configuration values
25
+ config.langsmith.auto_configure = true
26
+ config.langsmith.tracing_enabled = nil # nil means defer to env var
27
+
28
+ initializer "langsmith.configure" do |app|
29
+ next unless app.config.langsmith.auto_configure
30
+
31
+ configure_from_rails(app)
32
+ end
33
+
34
+ config.after_initialize do
35
+ # Log configuration status in development
36
+ if Rails.env.development? && Langsmith.tracing_enabled?
37
+ Rails.logger.info "[Langsmith] Tracing enabled for project: #{Langsmith.configuration.project}"
38
+ end
39
+ end
40
+
41
+ # Ensure traces are flushed before the application exits
42
+ config.before_configuration do
43
+ at_exit do
44
+ Langsmith.shutdown if Langsmith.tracing_enabled?
45
+ rescue StandardError => e
46
+ Rails.logger.error "[Langsmith] Error during shutdown: #{e.message}" if defined?(Rails.logger)
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ def configure_from_rails(app) # rubocop:disable Metrics/AbcSize
53
+ Langsmith.configure do |config|
54
+ # Try Rails credentials first, fall back to environment variables
55
+ credentials = app.credentials.langsmith || {}
56
+
57
+ config.api_key = credentials[:api_key] || ENV.fetch("LANGSMITH_API_KEY", nil)
58
+ config.endpoint = credentials[:endpoint] || ENV.fetch("LANGSMITH_ENDPOINT", "https://api.smith.langchain.com")
59
+ config.project = credentials[:project] || ENV.fetch("LANGSMITH_PROJECT",
60
+ Rails.application.class.module_parent_name.underscore)
61
+ config.tenant_id = credentials[:tenant_id] || ENV.fetch("LANGSMITH_TENANT_ID", nil)
62
+
63
+ # Tracing can be set via Rails config, credentials, or environment
64
+ config.tracing_enabled = resolve_tracing_enabled(app, credentials)
65
+
66
+ # Optional settings from credentials
67
+ config.batch_size = credentials[:batch_size] if credentials[:batch_size]
68
+ config.flush_interval = credentials[:flush_interval] if credentials[:flush_interval]
69
+ config.timeout = credentials[:timeout] if credentials[:timeout]
70
+ end
71
+ rescue Langsmith::ConfigurationError => e
72
+ Rails.logger.warn "[Langsmith] Configuration error: #{e.message}" if defined?(Rails.logger)
73
+ end
74
+
75
+ def resolve_tracing_enabled(app, credentials)
76
+ # Priority: Rails config > credentials > environment variable
77
+ return app.config.langsmith.tracing_enabled unless app.config.langsmith.tracing_enabled.nil?
78
+ return credentials[:tracing_enabled] unless credentials[:tracing_enabled].nil?
79
+
80
+ env_value = ENV.fetch("LANGSMITH_TRACING", nil)
81
+ return false if env_value.nil?
82
+
83
+ %w[true 1 yes on].include?(env_value.downcase)
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,320 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+ require "time"
5
+ require "json"
6
+
7
+ module Langsmith
8
+ # Represents a single trace run/span in LangSmith.
9
+ # All run types (chain, llm, tool, etc.) use this same class with different run_type values.
10
+ #
11
+ # @example Creating a run
12
+ # run = Langsmith::Run.new(name: "my_operation", run_type: "chain")
13
+ # run.add_metadata(user_id: "123")
14
+ # run.finish(outputs: { result: "success" })
15
+ class Run
16
+ # Valid run types supported by LangSmith
17
+ VALID_RUN_TYPES = %w[chain llm tool retriever prompt parser].freeze
18
+
19
+ # @return [String] unique identifier for this run
20
+ attr_reader :id
21
+
22
+ # @return [String] name of the operation
23
+ attr_reader :name
24
+
25
+ # @return [String] type of run (chain, llm, tool, etc.)
26
+ attr_reader :run_type
27
+
28
+ # @return [String, nil] parent run ID for nested traces
29
+ attr_reader :parent_run_id
30
+
31
+ # @return [String] project/session name
32
+ attr_reader :session_name
33
+
34
+ # @return [Time] when the run started
35
+ attr_reader :start_time
36
+
37
+ # @return [String, nil] tenant ID for multi-tenant scenarios
38
+ attr_reader :tenant_id
39
+
40
+ # @return [String] trace ID (root run's ID)
41
+ attr_reader :trace_id
42
+
43
+ # @return [String] dotted order for trace tree ordering
44
+ attr_reader :dotted_order
45
+
46
+ # @return [Hash] input data
47
+ attr_accessor :inputs
48
+
49
+ # @return [Hash, nil] output data
50
+ attr_accessor :outputs
51
+
52
+ # @return [String, nil] error message if run failed
53
+ attr_accessor :error
54
+
55
+ # @return [Time, nil] when the run ended
56
+ attr_accessor :end_time
57
+
58
+ # @return [Hash] additional metadata
59
+ attr_accessor :metadata
60
+
61
+ # @return [Hash] extra data (e.g., token usage)
62
+ attr_accessor :extra
63
+
64
+ # @return [Array<Hash>] events that occurred during the run
65
+ attr_accessor :events
66
+
67
+ # @return [Array<String>] tags for filtering
68
+ attr_accessor :tags
69
+
70
+ # Creates a new Run instance.
71
+ #
72
+ # @param name [String] name of the operation
73
+ # @param run_type [String] type of run ("chain", "llm", "tool", etc.)
74
+ # @param inputs [Hash, nil] input data
75
+ # @param parent_run_id [String, nil] parent run ID for nested traces
76
+ # @param session_name [String, nil] project/session name
77
+ # @param metadata [Hash, nil] additional metadata
78
+ # @param tags [Array<String>, nil] tags for filtering
79
+ # @param extra [Hash, nil] extra data
80
+ # @param id [String, nil] custom ID (auto-generated if not provided)
81
+ # @param tenant_id [String, nil] tenant ID for multi-tenant scenarios
82
+ # @param trace_id [String, nil] trace ID (defaults to own ID for root runs)
83
+ # @param parent_dotted_order [String, nil] parent's dotted order for tree ordering
84
+ #
85
+ # @raise [ArgumentError] if run_type is invalid
86
+ def initialize(
87
+ name:,
88
+ run_type: "chain",
89
+ inputs: nil,
90
+ parent_run_id: nil,
91
+ session_name: nil,
92
+ metadata: nil,
93
+ tags: nil,
94
+ extra: nil,
95
+ id: nil,
96
+ tenant_id: nil,
97
+ trace_id: nil,
98
+ parent_dotted_order: nil
99
+ )
100
+ @id = id || SecureRandom.uuid
101
+ @name = name
102
+ @run_type = validate_run_type(run_type)
103
+ @inputs = inputs || {}
104
+ @outputs = nil
105
+ @error = nil
106
+ @parent_run_id = parent_run_id
107
+ @session_name = session_name || Langsmith.configuration.project
108
+ @tenant_id = tenant_id || Langsmith.configuration.tenant_id
109
+ # trace_id is the root run's ID; for root runs it equals the run's own ID
110
+ @trace_id = trace_id || @id
111
+ @start_time = Time.now.utc
112
+ @end_time = nil
113
+ @metadata = metadata || {}
114
+ @tags = tags || []
115
+ @extra = extra || {}
116
+ @events = []
117
+ # dotted_order is used for ordering runs in the trace tree
118
+ @dotted_order = build_dotted_order(parent_dotted_order)
119
+ end
120
+
121
+ # Marks the run as finished.
122
+ #
123
+ # @param outputs [Hash, nil] output data
124
+ # @param error [Exception, String, nil] error if the run failed
125
+ # @return [self]
126
+ def finish(outputs: nil, error: nil)
127
+ @end_time = Time.now.utc
128
+ @outputs = outputs if outputs
129
+ @error = format_error(error) if error
130
+ self
131
+ end
132
+
133
+ # Adds metadata to the run.
134
+ #
135
+ # @param new_metadata [Hash] metadata to merge
136
+ # @return [nil] returns nil to prevent circular reference when used as last line
137
+ def add_metadata(new_metadata)
138
+ @metadata.merge!(new_metadata)
139
+ nil
140
+ end
141
+
142
+ # Adds tags to the run.
143
+ #
144
+ # @param new_tags [Array<String>] tags to add
145
+ # @return [nil] returns nil to prevent circular reference when used as last line
146
+ def add_tags(*new_tags)
147
+ @tags.concat(new_tags.flatten)
148
+ nil
149
+ end
150
+
151
+ # Adds an event to the run.
152
+ #
153
+ # @param name [String] event name
154
+ # @param time [Time, nil] event time (defaults to now)
155
+ # @param kwargs [Hash] additional event data
156
+ # @return [nil] returns nil to prevent circular reference when used as last line
157
+ def add_event(name:, time: nil, **kwargs)
158
+ @events << {
159
+ name: name,
160
+ time: (time || Time.now.utc).iso8601(3),
161
+ **kwargs
162
+ }
163
+ nil
164
+ end
165
+
166
+ # Sets token usage for LLM runs.
167
+ # Follows the Python SDK pattern: tokens are stored in extra.metadata.usage_metadata
168
+ # with keys: input_tokens, output_tokens, total_tokens
169
+ #
170
+ # @param input_tokens [Integer, nil] number of input/prompt tokens
171
+ # @param output_tokens [Integer, nil] number of output/completion tokens
172
+ # @param total_tokens [Integer, nil] total tokens (calculated if not provided)
173
+ # @return [nil] returns nil to prevent circular reference when used as last line
174
+ def set_token_usage(input_tokens: nil, output_tokens: nil, total_tokens: nil)
175
+ calculated_total = total_tokens || ((input_tokens || 0) + (output_tokens || 0))
176
+
177
+ @extra[:metadata] ||= {}
178
+ @extra[:metadata][:usage_metadata] = {
179
+ input_tokens: input_tokens,
180
+ output_tokens: output_tokens,
181
+ total_tokens: calculated_total
182
+ }.compact
183
+
184
+ nil # Return nil to prevent circular reference if used as last line of trace block
185
+ end
186
+
187
+ # Sets LLM model metadata.
188
+ # The model name should be stored in extra.metadata for LangSmith to display it.
189
+ #
190
+ # @param model [String] the model name/identifier
191
+ # @param provider [String, nil] the model provider (e.g., "openai", "anthropic")
192
+ # @return [nil] returns nil to prevent circular reference when used as last line
193
+ def set_model(model:, provider: nil)
194
+ @extra[:metadata] ||= {}
195
+ @extra[:metadata][:ls_model_name] = model
196
+ @extra[:metadata][:ls_provider] = provider if provider
197
+ nil
198
+ end
199
+
200
+ # Sets streaming metrics for LLM runs.
201
+ # Useful for tracking performance of streaming responses.
202
+ #
203
+ # @param time_to_first_token [Float, nil] time in seconds until first token received
204
+ # @param chunk_count [Integer, nil] total number of chunks received
205
+ # @param tokens_per_second [Float, nil] throughput in tokens per second
206
+ # @return [nil] returns nil to prevent circular reference when used as last line
207
+ def set_streaming_metrics(time_to_first_token: nil, chunk_count: nil, tokens_per_second: nil)
208
+ @extra[:metadata] ||= {}
209
+ @extra[:metadata][:streaming_metrics] = {
210
+ time_to_first_token_s: time_to_first_token,
211
+ chunk_count: chunk_count,
212
+ tokens_per_second: tokens_per_second
213
+ }.compact
214
+
215
+ nil
216
+ end
217
+
218
+ # Returns whether the run has finished.
219
+ # @return [Boolean]
220
+ def finished?
221
+ !end_time.nil?
222
+ end
223
+
224
+ # Returns the duration in milliseconds.
225
+ # @return [Float, nil] duration in ms, or nil if not finished
226
+ def duration_ms
227
+ return nil unless end_time
228
+
229
+ ((end_time - start_time) * 1000).round(2)
230
+ end
231
+
232
+ # Convert to hash for JSON serialization to LangSmith API (full run for POST).
233
+ # Token usage is stored in extra.metadata.usage_metadata following Python SDK pattern.
234
+ #
235
+ # @return [Hash]
236
+ def to_h
237
+ {
238
+ id:,
239
+ name:,
240
+ run_type:,
241
+ inputs:,
242
+ outputs:,
243
+ error:,
244
+ parent_run_id:,
245
+ trace_id:,
246
+ dotted_order:,
247
+ session_name:,
248
+ start_time: start_time.iso8601(3),
249
+ end_time: end_time&.iso8601(3),
250
+ extra: extra.empty? ? nil : extra,
251
+ events: events.empty? ? nil : events,
252
+ tags: tags.empty? ? nil : tags,
253
+ serialized: { name: },
254
+ **(metadata.empty? ? {} : { metadata: })
255
+ }.compact
256
+ end
257
+
258
+ # Convert to hash for PATCH requests (only fields that change on completion).
259
+ # Note: parent_run_id is required for LangSmith to validate dotted_order correctly.
260
+ # Token usage is included in extra.metadata.usage_metadata.
261
+ # Metadata and tags are included as they may be added during execution.
262
+ #
263
+ # @return [Hash]
264
+ def to_update_h
265
+ {
266
+ id:,
267
+ trace_id:,
268
+ parent_run_id:,
269
+ dotted_order:,
270
+ end_time: end_time&.iso8601(3),
271
+ outputs:,
272
+ error:,
273
+ events: events.empty? ? nil : events,
274
+ extra: extra.empty? ? nil : extra,
275
+ tags: tags.empty? ? nil : tags,
276
+ **(metadata.empty? ? {} : { metadata: })
277
+ }.compact
278
+ end
279
+
280
+ # Convert to JSON string.
281
+ #
282
+ # @return [String]
283
+ def to_json(*args)
284
+ to_h.to_json(*args)
285
+ end
286
+
287
+ private
288
+
289
+ def validate_run_type(run_type)
290
+ return run_type if VALID_RUN_TYPES.include?(run_type)
291
+
292
+ raise ArgumentError, "Invalid run_type '#{run_type}'. Must be one of: #{VALID_RUN_TYPES.join(", ")}"
293
+ end
294
+
295
+ def format_error(error)
296
+ case error
297
+ when Exception
298
+ "#{error.class}: #{error.message}\n#{error.backtrace&.first(10)&.join("\n")}"
299
+ when String
300
+ error
301
+ else
302
+ error.to_s
303
+ end
304
+ end
305
+
306
+ # Build the dotted_order string for trace ordering
307
+ # Format: {timestamp}{id} for root, {parent_dotted_order}.{timestamp}{id} for children
308
+ def build_dotted_order(parent_dotted_order)
309
+ # Format timestamp as YYYYMMDDTHHMMSSffffffZ (compact ISO8601 with microseconds)
310
+ timestamp = @start_time.strftime("%Y%m%dT%H%M%S%6NZ")
311
+ order_part = "#{timestamp}#{@id}"
312
+
313
+ if parent_dotted_order
314
+ "#{parent_dotted_order}.#{order_part}"
315
+ else
316
+ order_part
317
+ end
318
+ end
319
+ end
320
+ end
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "run"
4
+ require_relative "context"
5
+
6
+ module Langsmith
7
+ # RunTree manages the creation and lifecycle of trace runs.
8
+ # It handles parent-child relationships and coordinates with the batch processor.
9
+ class RunTree
10
+ attr_reader :run
11
+
12
+ def initialize(
13
+ name:,
14
+ run_type: "chain",
15
+ inputs: nil,
16
+ metadata: nil,
17
+ tags: nil,
18
+ extra: nil,
19
+ parent_run_id: nil,
20
+ tenant_id: nil,
21
+ project: nil
22
+ )
23
+ # If no explicit parent, check context for current parent
24
+ effective_parent_id = parent_run_id || Context.current_parent_run_id
25
+
26
+ # Inherit tenant_id from parent run if not explicitly set
27
+ effective_tenant_id = tenant_id || Context.current_run&.tenant_id
28
+
29
+ # Child traces must use the same project as their parent to keep the trace tree together.
30
+ # Only root traces can set the project; children always inherit from parent.
31
+ effective_project = Context.current_run&.session_name || project
32
+
33
+ # Inherit trace_id from root run (parent's trace_id)
34
+ # For root runs, trace_id will default to the run's own ID
35
+ effective_trace_id = Context.current_run&.trace_id
36
+
37
+ # Inherit dotted_order from parent for proper trace ordering
38
+ parent_dotted_order = Context.current_run&.dotted_order
39
+
40
+ @run = Run.new(
41
+ name: name,
42
+ run_type: run_type,
43
+ inputs: inputs,
44
+ parent_run_id: effective_parent_id,
45
+ metadata: metadata,
46
+ tags: tags,
47
+ extra: extra,
48
+ tenant_id: effective_tenant_id,
49
+ session_name: effective_project,
50
+ trace_id: effective_trace_id,
51
+ parent_dotted_order: parent_dotted_order
52
+ )
53
+
54
+ @posted_start = false
55
+ @posted_end = false
56
+ end
57
+
58
+ # Post the run start to LangSmith
59
+ def post_start
60
+ return if @posted_start || !Langsmith.tracing_enabled?
61
+
62
+ Langsmith.batch_processor.enqueue_create(@run)
63
+ @posted_start = true
64
+ end
65
+
66
+ # Post the run end to LangSmith
67
+ def post_end
68
+ return if @posted_end || !Langsmith.tracing_enabled?
69
+
70
+ Langsmith.batch_processor.enqueue_update(@run)
71
+ @posted_end = true
72
+ end
73
+
74
+ # Execute a block within this run's context
75
+ def execute
76
+ return yield(@run) unless Langsmith.tracing_enabled?
77
+
78
+ post_start
79
+
80
+ Context.with_run(@run) do
81
+ result = yield(@run)
82
+ @run.finish(outputs: sanitize_outputs(result))
83
+ result
84
+ end
85
+ rescue StandardError => e
86
+ @run.finish(error: e)
87
+ raise
88
+ ensure
89
+ post_end if Langsmith.tracing_enabled?
90
+ end
91
+
92
+ # Convenience methods that delegate to the run
93
+ def add_metadata(...)
94
+ @run.add_metadata(...)
95
+ end
96
+
97
+ def add_tags(...)
98
+ @run.add_tags(...)
99
+ end
100
+
101
+ def set_inputs(inputs)
102
+ @run.inputs = inputs
103
+ end
104
+
105
+ def set_outputs(outputs)
106
+ @run.outputs = outputs
107
+ end
108
+
109
+ def set_token_usage(...)
110
+ @run.set_token_usage(...)
111
+ end
112
+
113
+ def set_model(...)
114
+ @run.set_model(...)
115
+ end
116
+
117
+ def set_streaming_metrics(...)
118
+ @run.set_streaming_metrics(...)
119
+ end
120
+
121
+ def id
122
+ @run.id
123
+ end
124
+
125
+ def parent_run_id
126
+ @run.parent_run_id
127
+ end
128
+
129
+ # Create a child run tree
130
+ def create_child(name:, run_type: "chain", **kwargs)
131
+ RunTree.new(
132
+ name: name,
133
+ run_type: run_type,
134
+ parent_run_id: @run.id,
135
+ **kwargs
136
+ )
137
+ end
138
+
139
+ private
140
+
141
+ # Sanitize block results to prevent circular references.
142
+ # When users call methods like `run.add_metadata(...)` as the last line,
143
+ # the Run object itself becomes the result, creating a circular reference.
144
+ def sanitize_outputs(result)
145
+ case result
146
+ when Run, RunTree, nil
147
+ # Run/RunTree objects would create circular reference, nil means no output
148
+ nil
149
+ else
150
+ { result: result }
151
+ end
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Langsmith
4
+ # Module that provides method decoration for automatic tracing.
5
+ # Include this module in your class and use the `traceable` class method
6
+ # to mark methods for tracing.
7
+ #
8
+ # @example
9
+ # class MyService
10
+ # include Langsmith::Traceable
11
+ #
12
+ # traceable run_type: "llm"
13
+ # def call_llm(prompt)
14
+ # # automatically traced
15
+ # end
16
+ #
17
+ # traceable run_type: "tool", name: "search"
18
+ # def search(query)
19
+ # # traced with custom name
20
+ # end
21
+ #
22
+ # traceable run_type: "chain", tenant_id: "tenant-123"
23
+ # def process_for_tenant(data)
24
+ # # traced to specific tenant
25
+ # end
26
+ # end
27
+ module Traceable
28
+ def self.included(base)
29
+ base.extend(ClassMethods)
30
+ end
31
+
32
+ module ClassMethods
33
+ # Marks the next defined method as traceable
34
+ def traceable(run_type: "chain", name: nil, metadata: nil, tags: nil, tenant_id: nil)
35
+ @pending_traceable_options = {
36
+ run_type: run_type,
37
+ name: name,
38
+ metadata: metadata,
39
+ tags: tags,
40
+ tenant_id: tenant_id
41
+ }
42
+ end
43
+
44
+ def method_added(method_name)
45
+ super
46
+
47
+ return unless @pending_traceable_options
48
+
49
+ options = @pending_traceable_options
50
+ @pending_traceable_options = nil
51
+
52
+ # Don't wrap private/protected methods that start with underscore
53
+ return if method_name.to_s.start_with?("_langsmith_")
54
+
55
+ wrap_method(method_name, options)
56
+ end
57
+
58
+ private
59
+
60
+ def wrap_method(method_name, options)
61
+ original_method = instance_method(method_name)
62
+ trace_name = options[:name] || "#{name}##{method_name}"
63
+
64
+ # Remove original method to avoid "method redefined" warning
65
+ remove_method(method_name)
66
+
67
+ define_method(method_name) do |*args, **kwargs, &block|
68
+ Langsmith.trace(
69
+ trace_name,
70
+ run_type: options[:run_type],
71
+ inputs: build_trace_inputs(args, kwargs, original_method),
72
+ metadata: options[:metadata],
73
+ tags: options[:tags],
74
+ tenant_id: options[:tenant_id]
75
+ ) do |_run|
76
+ if kwargs.empty?
77
+ original_method.bind(self).call(*args, &block)
78
+ else
79
+ original_method.bind(self).call(*args, **kwargs, &block)
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
85
+
86
+ private
87
+
88
+ def build_trace_inputs(args, kwargs, method)
89
+ params = method.parameters
90
+ inputs = {}
91
+
92
+ # Map positional arguments
93
+ args.each_with_index do |arg, index|
94
+ param = params[index]
95
+ param_name = param ? param[1] : "arg#{index}"
96
+ inputs[param_name] = serialize_input(arg)
97
+ end
98
+
99
+ # Map keyword arguments
100
+ kwargs.each do |key, value|
101
+ inputs[key] = serialize_input(value)
102
+ end
103
+
104
+ inputs
105
+ end
106
+
107
+ def serialize_input(value)
108
+ case value
109
+ when String, Numeric, TrueClass, FalseClass, NilClass
110
+ value
111
+ when Array
112
+ value.map { |v| serialize_input(v) }
113
+ when Hash
114
+ value.transform_values { |v| serialize_input(v) }
115
+ else
116
+ value.to_s
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Langsmith
4
+ VERSION = "0.1.1"
5
+ end