braintrust 0.1.1 → 0.1.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 40d5c41ef999d495f9eb7b3a4cd41f1bb9ee7ba831de2074195a7d9e18da2e3a
4
- data.tar.gz: 8e48aee25cd02a4b936da1264a9964097c6e07744faf0564d81885daa72b87da
3
+ metadata.gz: 626876b443795d28b4ba5d12f8bf10381c3052d5d196adb01207d545303f3d1e
4
+ data.tar.gz: 347ca89ea9f485ca6521a38c067bdd15074db4e6a4757523901888a8d4cc3e9c
5
5
  SHA512:
6
- metadata.gz: 9391f2dcec3c92e032d3e7009ec4bf1d26f6d2a606f0edad78acf78078f116393d95a4a3d70a08010b89df214f92498905c67cd02b3bd6327b5aa2cbf1dda872
7
- data.tar.gz: 5665f9bcb49a8b5ca90f5d58d9c279120a053756c6ec03ba17557b6064c7a7142ac9674789fe4c89df70e2ac61d1d1ce9318a33b897b55da1d45b9bc9df1bf15
6
+ metadata.gz: 7b827a4f92e2bc4b39e41174e62dacdc431fdc0b6c8d13882bdcaaa369af9621174fc01bafa3d0d594b71464ea374c6904e9834631957951dad87d6583a58dc9
7
+ data.tar.gz: bb6f2d3807765ef4ad591849e0972379fc3f97ef8d90bda0785e1d4dab87ce5e91d954d9d3c8fc7eff6c9295d120d6cbe07acb5bb348873c842d791a3fbdce84
@@ -85,6 +85,17 @@ module Braintrust
85
85
  http_post_json("/v1/function/#{id}/invoke", payload)
86
86
  end
87
87
 
88
+ # Get a function by ID (includes full prompt_data)
89
+ # GET /v1/function/{id}
90
+ # @param id [String] Function UUID
91
+ # @param version [String, nil] Retrieve prompt at a specific version (transaction ID or version identifier)
92
+ # @return [Hash] Full function data including prompt_data
93
+ def get(id:, version: nil)
94
+ params = {}
95
+ params["version"] = version if version
96
+ http_get("/v1/function/#{id}", params)
97
+ end
98
+
88
99
  # Delete a function by ID
89
100
  # DELETE /v1/function/{id}
90
101
  # @param id [String] Function UUID
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../vendor/mustache"
4
+
5
+ module Braintrust
6
+ module Internal
7
+ # Template rendering utilities for Mustache templates
8
+ module Template
9
+ module_function
10
+
11
+ # Render a template string with variable substitution
12
+ #
13
+ # @param text [String] Template text to render
14
+ # @param variables [Hash] Variables to substitute
15
+ # @param format [String] Template format: "mustache", "none", or "nunjucks"
16
+ # @param strict [Boolean] Raise error on missing variables (default: false)
17
+ # @return [String] Rendered text
18
+ def render(text, variables, format:, strict: false)
19
+ return text unless text.is_a?(String)
20
+
21
+ case format
22
+ when "none"
23
+ # No templating - return text unchanged
24
+ text
25
+ when "nunjucks"
26
+ # Nunjucks is a UI-only feature in Braintrust
27
+ raise Error, "Nunjucks templates are not supported in the Ruby SDK. " \
28
+ "Nunjucks only works in Braintrust playgrounds. " \
29
+ "Please use 'mustache' or 'none' template format, or invoke the prompt via the API proxy."
30
+ when "mustache", "", nil
31
+ # Default: Mustache templating
32
+ if strict
33
+ missing = find_missing_variables(text, variables)
34
+ if missing.any?
35
+ raise Error, "Missing required variables: #{missing.join(", ")}"
36
+ end
37
+ end
38
+
39
+ Vendor::Mustache.render(text, variables)
40
+ else
41
+ raise Error, "Unknown template format: #{format.inspect}. " \
42
+ "Supported formats are 'mustache' and 'none'."
43
+ end
44
+ end
45
+
46
+ # Find Mustache variables in template that are not provided
47
+ #
48
+ # @param text [String] Template text
49
+ # @param variables [Hash] Available variables
50
+ # @return [Array<String>] List of missing variable names
51
+ def find_missing_variables(text, variables)
52
+ # Extract {{variable}} and {{variable.path}} patterns
53
+ # Mustache uses {{name}} syntax
54
+ text.scan(/\{\{([^}#^\/!>]+)\}\}/).flatten.map(&:strip).uniq.reject do |var|
55
+ resolve_variable(var, variables)
56
+ end
57
+ end
58
+
59
+ # Check if a variable path exists in a hash
60
+ #
61
+ # @param path [String] Dot-separated variable path (e.g., "user.name")
62
+ # @param variables [Hash] Variables to search
63
+ # @return [Object, nil] The value if found, nil otherwise
64
+ def resolve_variable(path, variables)
65
+ parts = path.split(".")
66
+ value = variables
67
+
68
+ parts.each do |part|
69
+ return nil unless value.is_a?(Hash)
70
+ # Try both string and symbol keys
71
+ value = value[part] || value[part.to_sym]
72
+ return nil if value.nil?
73
+ end
74
+
75
+ value
76
+ end
77
+
78
+ # Convert hash keys to strings recursively
79
+ #
80
+ # @param hash [Hash] Hash with symbol or string keys
81
+ # @return [Hash] Hash with all string keys
82
+ def stringify_keys(hash)
83
+ return {} unless hash.is_a?(Hash)
84
+
85
+ hash.transform_keys(&:to_s).transform_values do |v|
86
+ v.is_a?(Hash) ? stringify_keys(v) : v
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require_relative "internal/template"
5
+
6
+ module Braintrust
7
+ # Prompt class for loading and building prompts from Braintrust
8
+ #
9
+ # @example Load and use a prompt
10
+ # prompt = Braintrust::Prompt.load(project: "my-project", slug: "summarizer")
11
+ # params = prompt.build(text: "Article to summarize...")
12
+ # client.messages.create(**params)
13
+ class Prompt
14
+ attr_reader :id, :name, :slug, :project_id
15
+
16
+ # Load a prompt from Braintrust
17
+ #
18
+ # @param project [String] Project name
19
+ # @param slug [String] Prompt slug
20
+ # @param version [String, nil] Specific version (default: latest)
21
+ # @param defaults [Hash] Default variable values for build()
22
+ # @param api [API, nil] Braintrust API client (default: creates one using global state)
23
+ # @return [Prompt]
24
+ def self.load(project:, slug:, version: nil, defaults: {}, api: nil)
25
+ api ||= API.new
26
+
27
+ # Find the function by project + slug
28
+ result = api.functions.list(project_name: project, slug: slug)
29
+ function = result.dig("objects")&.first
30
+ raise Error, "Prompt '#{slug}' not found in project '#{project}'" unless function
31
+
32
+ # Fetch full function data including prompt_data
33
+ full_data = api.functions.get(id: function["id"], version: version)
34
+
35
+ new(full_data, defaults: defaults)
36
+ end
37
+
38
+ # Initialize a Prompt from function data
39
+ #
40
+ # @param data [Hash] Function data from API
41
+ # @param defaults [Hash] Default variable values for build()
42
+ def initialize(data, defaults: {})
43
+ @data = data
44
+ @defaults = Internal::Template.stringify_keys(defaults)
45
+
46
+ @id = data["id"]
47
+ @name = data["name"]
48
+ @slug = data["slug"]
49
+ @project_id = data["project_id"]
50
+ end
51
+
52
+ # Get the raw prompt definition
53
+ # @return [Hash, nil]
54
+ def prompt
55
+ @data.dig("prompt_data", "prompt")
56
+ end
57
+
58
+ # Get the prompt messages
59
+ # @return [Array<Hash>]
60
+ def messages
61
+ prompt&.dig("messages") || []
62
+ end
63
+
64
+ # Get the tools definition (parsed from JSON string)
65
+ # @return [Array<Hash>, nil]
66
+ def tools
67
+ tools_json = prompt&.dig("tools")
68
+ return nil unless tools_json.is_a?(String) && !tools_json.empty?
69
+
70
+ JSON.parse(tools_json)
71
+ rescue JSON::ParserError
72
+ nil
73
+ end
74
+
75
+ # Get the model name
76
+ # @return [String, nil]
77
+ def model
78
+ @data.dig("prompt_data", "options", "model")
79
+ end
80
+
81
+ # Get model options
82
+ # @return [Hash]
83
+ def options
84
+ @data.dig("prompt_data", "options") || {}
85
+ end
86
+
87
+ # Get the template format
88
+ # @return [String] "mustache" (default), "nunjucks", or "none"
89
+ def template_format
90
+ @data.dig("prompt_data", "template_format") || "mustache"
91
+ end
92
+
93
+ # Build the prompt with variable substitution
94
+ #
95
+ # Returns a hash ready to pass to an LLM client:
96
+ # {model: "...", messages: [...], temperature: ..., ...}
97
+ #
98
+ # @param variables [Hash] Variables to substitute (e.g., {name: "Alice"})
99
+ # @param strict [Boolean] Raise error on missing variables (default: false)
100
+ # @return [Hash] Built prompt ready for LLM client
101
+ #
102
+ # @example With keyword arguments
103
+ # prompt.build(name: "Alice", task: "coding")
104
+ #
105
+ # @example With explicit hash
106
+ # prompt.build({name: "Alice"}, strict: true)
107
+ def build(variables = nil, strict: false, **kwargs)
108
+ # Support both explicit hash and keyword arguments
109
+ variables_hash = variables.is_a?(Hash) ? variables : {}
110
+ vars = @defaults
111
+ .merge(Internal::Template.stringify_keys(variables_hash))
112
+ .merge(Internal::Template.stringify_keys(kwargs))
113
+
114
+ # Render Mustache templates in messages
115
+ built_messages = messages.map do |msg|
116
+ {
117
+ role: msg["role"].to_sym,
118
+ content: Internal::Template.render(msg["content"], vars, format: template_format, strict: strict)
119
+ }
120
+ end
121
+
122
+ # Build result with model and messages
123
+ result = {
124
+ model: model,
125
+ messages: built_messages
126
+ }
127
+
128
+ # Add tools if defined
129
+ parsed_tools = tools
130
+ result[:tools] = parsed_tools if parsed_tools
131
+
132
+ # Add params (temperature, max_tokens, etc.) to top level
133
+ params = options.dig("params")
134
+ if params.is_a?(Hash)
135
+ params.each do |key, value|
136
+ result[key.to_sym] = value
137
+ end
138
+ end
139
+
140
+ result
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,180 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Vendored from mustache gem v1.1.1
4
+ # https://github.com/mustache/mustache
5
+ # License: MIT
6
+ # Modifications: Namespaced under Braintrust::Vendor
7
+
8
+ require_relative "context_miss"
9
+
10
+ module Braintrust
11
+ module Vendor
12
+ class Mustache
13
+ # A Context represents the context which a Mustache template is
14
+ # executed within. All Mustache tags reference keys in the Context.
15
+ class Context
16
+ # Initializes a Mustache::Context.
17
+ #
18
+ # @param [Mustache] mustache A Mustache instance.
19
+ #
20
+ def initialize(mustache)
21
+ @stack = [mustache]
22
+ @partial_template_cache = {}
23
+ end
24
+
25
+ # A {{>partial}} tag translates into a call to the context's
26
+ # `partial` method, which would be this sucker right here.
27
+ #
28
+ # If the Mustache view handling the rendering (e.g. the view
29
+ # representing your profile page or some other template) responds
30
+ # to `partial`, we call it and render the result.
31
+ #
32
+ def partial(name, indentation = "")
33
+ # Look for the first Mustache in the stack.
34
+ mustache = mustache_in_stack
35
+
36
+ # Indent the partial template by the given indentation.
37
+ part = mustache.partial(name).to_s.gsub(/^/, indentation)
38
+
39
+ # Get a template object for the partial and render the result.
40
+ template_for_partial(part).render(self)
41
+ end
42
+
43
+ def template_for_partial(partial)
44
+ @partial_template_cache[partial] ||= Template.new(partial)
45
+ end
46
+
47
+ # Find the first Mustache in the stack.
48
+ #
49
+ # If we're being rendered inside a Mustache object as a context,
50
+ # we'll use that one.
51
+ #
52
+ # @return [Mustache] First Mustache in the stack.
53
+ #
54
+ def mustache_in_stack
55
+ @mustache_in_stack ||= @stack.find { |frame| frame.is_a?(Mustache) }
56
+ end
57
+
58
+ # Allows customization of how Mustache escapes things.
59
+ #
60
+ # @param [Object] value Value to escape.
61
+ #
62
+ # @return [String] Escaped string.
63
+ #
64
+ def escape(value)
65
+ mustache_in_stack.escape(value)
66
+ end
67
+
68
+ # Adds a new object to the context's internal stack.
69
+ #
70
+ # @param [Object] new_obj Object to be added to the internal stack.
71
+ #
72
+ # @return [Context] Returns the Context.
73
+ #
74
+ def push(new_obj)
75
+ @stack.unshift(new_obj)
76
+ @mustache_in_stack = nil
77
+ self
78
+ end
79
+
80
+ # Removes the most recently added object from the context's
81
+ # internal stack.
82
+ #
83
+ # @return [Context] Returns the Context.
84
+ #
85
+ def pop
86
+ @stack.shift
87
+ @mustache_in_stack = nil
88
+ self
89
+ end
90
+
91
+ # Can be used to add a value to the context in a hash-like way.
92
+ #
93
+ # context[:name] = "Chris"
94
+ def []=(name, value)
95
+ push(name => value)
96
+ end
97
+
98
+ # Alias for `fetch`.
99
+ def [](name)
100
+ fetch(name, nil)
101
+ end
102
+
103
+ # Do we know about a particular key? In other words, will calling
104
+ # `context[key]` give us a result that was set. Basically.
105
+ def has_key?(key)
106
+ fetch(key, false)
107
+ rescue ContextMiss
108
+ false
109
+ end
110
+
111
+ # Similar to Hash#fetch, finds a value by `name` in the context's
112
+ # stack. You may specify the default return value by passing a
113
+ # second parameter.
114
+ #
115
+ # If no second parameter is passed (or raise_on_context_miss is
116
+ # set to true), will raise a ContextMiss exception on miss.
117
+ def fetch(name, default = :__raise)
118
+ @stack.each do |frame|
119
+ # Prevent infinite recursion.
120
+ next if frame == self
121
+
122
+ value = find(frame, name, :__missing)
123
+ return value if :__missing != value
124
+ end
125
+
126
+ if default == :__raise || mustache_in_stack.raise_on_context_miss?
127
+ raise ContextMiss.new("Can't find #{name} in #{@stack.inspect}")
128
+ else
129
+ default
130
+ end
131
+ end
132
+
133
+ # Finds a key in an object, using whatever method is most
134
+ # appropriate. If the object is a hash, does a simple hash lookup.
135
+ # If it's an object that responds to the key as a method call,
136
+ # invokes that method. You get the idea.
137
+ #
138
+ # @param [Object] obj The object to perform the lookup on.
139
+ # @param [String,Symbol] key The key whose value you want
140
+ # @param [Object] default An optional default value, to return if the key is not found.
141
+ #
142
+ # @return [Object] The value of key in object if it is found, and default otherwise.
143
+ #
144
+ def find(obj, key, default = nil)
145
+ return find_in_hash(obj.to_hash, key, default) if obj.respond_to?(:to_hash)
146
+
147
+ unless obj.respond_to?(key)
148
+ # no match for the key, but it may include a hyphen, so try again replacing hyphens with underscores.
149
+ key = key.to_s.tr("-", "_")
150
+ return default unless obj.respond_to?(key)
151
+ end
152
+
153
+ meth = obj.method(key) rescue proc { obj.send(key) }
154
+ meth.arity == 1 ? meth.to_proc : meth.call
155
+ end
156
+
157
+ def current
158
+ @stack.first
159
+ end
160
+
161
+ private
162
+
163
+ # Fetches a hash key if it exists, or returns the given default.
164
+ def find_in_hash(obj, key, default)
165
+ return obj[key] if obj.has_key?(key)
166
+ return obj[key.to_s] if obj.has_key?(key.to_s)
167
+ return obj[key] if obj.respond_to?(:default_proc) && obj.default_proc && obj[key]
168
+
169
+ # If default is :__missing then we are from #fetch which is hunting through the stack
170
+ # If default is nil then we are reducing dot notation
171
+ if :__missing != default && mustache_in_stack.raise_on_context_miss?
172
+ raise ContextMiss.new("Can't find #{key} in #{obj}")
173
+ else
174
+ obj.fetch(key, default)
175
+ end
176
+ end
177
+ end
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Vendored from mustache gem v1.1.1
4
+ # https://github.com/mustache/mustache
5
+ # License: MIT
6
+ # Modifications: Namespaced under Braintrust::Vendor
7
+
8
+ module Braintrust
9
+ module Vendor
10
+ class Mustache
11
+ # A ContextMiss is raised whenever a tag's target can not be found
12
+ # in the current context if `Mustache#raise_on_context_miss?` is
13
+ # set to true.
14
+ #
15
+ # For example, if your View class does not respond to `music` but
16
+ # your template contains a `{{music}}` tag this exception will be raised.
17
+ #
18
+ # By default it is not raised. See Mustache.raise_on_context_miss.
19
+ class ContextMiss < RuntimeError; end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Vendored from mustache gem v1.1.1
4
+ # https://github.com/mustache/mustache
5
+ # License: MIT
6
+ # Modifications: Namespaced under Braintrust::Vendor
7
+
8
+ module Braintrust
9
+ module Vendor
10
+ class Mustache
11
+ Enumerable = Module.new
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,188 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Vendored from mustache gem v1.1.1
4
+ # https://github.com/mustache/mustache
5
+ # License: MIT
6
+ # Modifications: Namespaced under Braintrust::Vendor
7
+
8
+ module Braintrust
9
+ module Vendor
10
+ class Mustache
11
+ # The Generator is in charge of taking an array of Mustache tokens,
12
+ # usually assembled by the Parser, and generating an interpolatable
13
+ # Ruby string. This string is considered the "compiled" template
14
+ # because at that point we're relying on Ruby to do the parsing and
15
+ # run our code.
16
+ #
17
+ # For example, let's take this template:
18
+ #
19
+ # Hi {{thing}}!
20
+ #
21
+ # If we run this through the Parser we'll get these tokens:
22
+ #
23
+ # [:multi,
24
+ # [:static, "Hi "],
25
+ # [:mustache, :etag, "thing"],
26
+ # [:static, "!\n"]]
27
+ #
28
+ # Now let's hand that to the Generator:
29
+ #
30
+ # >> puts Braintrust::Vendor::Mustache::Generator.new.compile(tokens)
31
+ # "Hi #{ctx.escape(ctx[:thing])}!\n"
32
+ class Generator
33
+ # Options can be used to manipulate the resulting ruby code string behavior.
34
+ def initialize(options = {})
35
+ @options = options
36
+ @option_static_lambdas = options[:static_lambdas] == true
37
+ end
38
+
39
+ # Given an array of tokens, returns an interpolatable Ruby string.
40
+ def compile(exp)
41
+ "\"#{compile!(exp)}\""
42
+ end
43
+
44
+ private
45
+
46
+ # Given an array of tokens, converts them into Ruby code. In
47
+ # particular there are three types of expressions we are concerned
48
+ # with:
49
+ #
50
+ # :multi
51
+ # Mixed bag of :static, :mustache, and whatever.
52
+ #
53
+ # :static
54
+ # Normal HTML, the stuff outside of {{mustaches}}.
55
+ #
56
+ # :mustache
57
+ # Any Mustache tag, from sections to partials.
58
+ def compile!(exp)
59
+ case exp.first
60
+ when :multi
61
+ exp[1..-1].reduce(+"") { |sum, e| sum << compile!(e) }
62
+ when :static
63
+ str(exp[1])
64
+ when :mustache
65
+ send(:"on_#{exp[1]}", *exp[2..-1])
66
+ else
67
+ raise "Unhandled exp: #{exp.first}"
68
+ end
69
+ end
70
+
71
+ # Callback fired when the compiler finds a section token. We're
72
+ # passed the section name and the array of tokens.
73
+ def on_section(name, offset, content, raw, delims)
74
+ # Convert the tokenized content of this section into a Ruby
75
+ # string we can use.
76
+ code = compile(content)
77
+
78
+ # Lambda handling - default handling is to dynamically interpret
79
+ # the returned lambda result as mustache source
80
+ proc_handling = if @option_static_lambdas
81
+ <<-compiled
82
+ v.call(lambda {|v| #{code}}.call(v)).to_s
83
+ compiled
84
+ else
85
+ <<-compiled
86
+ t = Braintrust::Vendor::Mustache::Template.new(v.call(#{raw.inspect}).to_s)
87
+ def t.tokens(src=@source)
88
+ p = Braintrust::Vendor::Mustache::Parser.new
89
+ p.otag, p.ctag = #{delims.inspect}
90
+ p.compile(src)
91
+ end
92
+ t.render(ctx.dup)
93
+ compiled
94
+ end
95
+
96
+ # Compile the Ruby for this section now that we know what's
97
+ # inside the section.
98
+ ev(<<-compiled)
99
+ case v = #{compile!(name)}
100
+ when NilClass, FalseClass
101
+ when TrueClass
102
+ #{code}
103
+ when Proc
104
+ #{proc_handling}
105
+ when Array, Enumerator, Braintrust::Vendor::Mustache::Enumerable
106
+ v.map { |_| ctx.push(_); r = #{code}; ctx.pop; r }.join
107
+ else
108
+ ctx.push(v); r = #{code}; ctx.pop; r
109
+ end
110
+ compiled
111
+ end
112
+
113
+ # Fired when we find an inverted section. Just like `on_section`,
114
+ # we're passed the inverted section name and the array of tokens.
115
+ def on_inverted_section(name, offset, content, raw, delims)
116
+ # Convert the tokenized content of this section into a Ruby
117
+ # string we can use.
118
+ code = compile(content)
119
+
120
+ # Compile the Ruby for this inverted section now that we know
121
+ # what's inside.
122
+ ev(<<-compiled)
123
+ v = #{compile!(name)}
124
+ if v.nil? || v == false || v.respond_to?(:empty?) && v.empty?
125
+ #{code}
126
+ end
127
+ compiled
128
+ end
129
+
130
+ # Fired when the compiler finds a partial. We want to return code
131
+ # which calls a partial at runtime instead of expanding and
132
+ # including the partial's body to allow for recursive partials.
133
+ def on_partial(name, offset, indentation)
134
+ ev("ctx.partial(#{name.to_sym.inspect}, #{indentation.inspect})")
135
+ end
136
+
137
+ # An unescaped tag.
138
+ def on_utag(name, offset)
139
+ ev(<<-compiled)
140
+ v = #{compile!(name)}
141
+ if v.is_a?(Proc)
142
+ v = #{@option_static_lambdas ? "v.call" : "Braintrust::Vendor::Mustache::Template.new(v.call.to_s).render(ctx.dup)"}
143
+ end
144
+ v.to_s
145
+ compiled
146
+ end
147
+
148
+ # An escaped tag.
149
+ def on_etag(name, offset)
150
+ ev(<<-compiled)
151
+ v = #{compile!(name)}
152
+ if v.is_a?(Proc)
153
+ v = #{@option_static_lambdas ? "v.call" : "Braintrust::Vendor::Mustache::Template.new(v.call.to_s).render(ctx.dup)"}
154
+ end
155
+ ctx.escape(v)
156
+ compiled
157
+ end
158
+
159
+ def on_fetch(names)
160
+ return "ctx.current" if names.empty?
161
+
162
+ names = names.map { |n| n.to_sym }
163
+
164
+ initial, *rest = names
165
+ if rest.any?
166
+ <<-compiled
167
+ #{rest.inspect}.reduce(ctx[#{initial.inspect}]) { |value, key| value && ctx.find(value, key) }
168
+ compiled
169
+ else
170
+ <<-compiled
171
+ ctx[#{initial.inspect}]
172
+ compiled
173
+ end
174
+ end
175
+
176
+ # An interpolation-friendly version of a string, for use within a
177
+ # Ruby string.
178
+ def ev(s)
179
+ "#\{#{s}}"
180
+ end
181
+
182
+ def str(s)
183
+ s.inspect[1..-2]
184
+ end
185
+ end
186
+ end
187
+ end
188
+ end