ruby-laya 0.1.0
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 +7 -0
- data/CHANGELOG.md +44 -0
- data/LICENSE +176 -0
- data/NOTICE +7 -0
- data/README.md +399 -0
- data/lib/laya/agent.rb +324 -0
- data/lib/laya/ask.rb +73 -0
- data/lib/laya/checkpoints.rb +77 -0
- data/lib/laya/common.rb +175 -0
- data/lib/laya/configuration.rb +72 -0
- data/lib/laya/decision.rb +120 -0
- data/lib/laya/email.rb +187 -0
- data/lib/laya/errors.rb +16 -0
- data/lib/laya/hub.rb +206 -0
- data/lib/laya/lang.rb +301 -0
- data/lib/laya/presets.rb +197 -0
- data/lib/laya/py_json.rb +144 -0
- data/lib/laya/question.rb +171 -0
- data/lib/laya/questions.rb +43 -0
- data/lib/laya/result.rb +263 -0
- data/lib/laya/router.rb +353 -0
- data/lib/laya/runtime.rb +79 -0
- data/lib/laya/shortlist.rb +197 -0
- data/lib/laya/tokenizer.rb +101 -0
- data/lib/laya/training.rb +88 -0
- data/lib/laya/util.rb +57 -0
- data/lib/laya/version.rb +8 -0
- data/lib/laya.rb +129 -0
- metadata +110 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Laya
|
|
4
|
+
# One typed question, validated and rendered the way the checkpoints were trained on.
|
|
5
|
+
#
|
|
6
|
+
# A question definition arrives as a Hash, with either String or Symbol keys:
|
|
7
|
+
#
|
|
8
|
+
# { "type" => "choice", "instructions" => "...", "criteria" => { "billing" => "invoices" } }
|
|
9
|
+
# { type: :score, instructions: "...", criteria: ["not urgent", "soon", "urgent"] }
|
|
10
|
+
# { type: :noul, instructions: "..." }
|
|
11
|
+
#
|
|
12
|
+
# Each type knows how to check itself, how to render its options, and how to turn the model's
|
|
13
|
+
# probabilities into an {Answer}, so nothing downstream switches on the type again.
|
|
14
|
+
class Question
|
|
15
|
+
attr_reader :id, :instructions, :criteria
|
|
16
|
+
|
|
17
|
+
# Build the question `definition` describes, raising ArgumentError when it cannot be answered.
|
|
18
|
+
def self.build(id, definition)
|
|
19
|
+
unless definition.is_a?(Hash)
|
|
20
|
+
raise ArgumentError, "question #{id.inspect}: definition must be a Hash, got #{definition.class}"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
type = Util.get(definition, "type")
|
|
24
|
+
klass = TYPES[type.to_s]
|
|
25
|
+
unless klass
|
|
26
|
+
raise ArgumentError, "question #{id.inspect}: unknown type #{type.inspect}; " \
|
|
27
|
+
"use one of #{TYPES.keys.sort}"
|
|
28
|
+
end
|
|
29
|
+
unless Util.key?(definition, "instructions")
|
|
30
|
+
raise ArgumentError, "question #{id.inspect}: no 'instructions'; " \
|
|
31
|
+
"add the text the model should answer"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
klass.new(id, Util.get(definition, "instructions"), Util.get(definition, "criteria"))
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def initialize(id, instructions, criteria)
|
|
38
|
+
@id = id
|
|
39
|
+
@instructions = instructions.is_a?(String) ? instructions : PyJSON.dumps(instructions, ensure_ascii: true)
|
|
40
|
+
@criteria = normalise_criteria(criteria)
|
|
41
|
+
validate!
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def type
|
|
45
|
+
self.class::TYPE
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def qtype
|
|
49
|
+
QTYPES.fetch(type)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# The option texts the model scores, in label order.
|
|
53
|
+
def options
|
|
54
|
+
Common.render_options(internal)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# The shape `Laya::Common` and upstream's Python both work in.
|
|
58
|
+
def internal
|
|
59
|
+
{ t: type, ins: instructions, crit: criteria }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Build this question's answer from the probabilities over its options.
|
|
63
|
+
def answer(**)
|
|
64
|
+
raise NotImplementedError, "#{self.class} must build its own answer"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Pick one label from a set of options.
|
|
68
|
+
class Choice < Question
|
|
69
|
+
TYPE = "choice"
|
|
70
|
+
|
|
71
|
+
def labels
|
|
72
|
+
criteria.keys
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def answer(probabilities:, confidence:, action_probability:)
|
|
76
|
+
Answer::Choice.new(
|
|
77
|
+
choice: labels[probabilities.each_with_index.max_by { |p, i| [p, -i] }.last],
|
|
78
|
+
probabilities: labels.zip(probabilities.map { |p| p.round(4) }).to_h,
|
|
79
|
+
confidence: confidence, action_probability: action_probability
|
|
80
|
+
)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
# A list of labels is the same question as a Hash of labels with no descriptions.
|
|
86
|
+
def normalise_criteria(criteria)
|
|
87
|
+
criteria.is_a?(Array) ? criteria.to_h { |label| [label, nil] } : criteria
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def validate!
|
|
91
|
+
unless criteria.is_a?(Hash)
|
|
92
|
+
raise ArgumentError, "question #{id.inspect}: a choice question takes 'criteria' as a Hash " \
|
|
93
|
+
"of label => description, or an Array of labels"
|
|
94
|
+
end
|
|
95
|
+
return unless criteria.empty?
|
|
96
|
+
|
|
97
|
+
raise ArgumentError, "question #{id.inspect}: a choice question needs at least one criterion"
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Place the state on an ordinal rubric, level 0 first.
|
|
102
|
+
class Score < Question
|
|
103
|
+
TYPE = "score"
|
|
104
|
+
|
|
105
|
+
def answer(probabilities:, confidence:, action_probability:)
|
|
106
|
+
Answer::Score.new(
|
|
107
|
+
score: probabilities.each_with_index.sum { |p, i| i * p }.round(4),
|
|
108
|
+
legend: criteria.each_with_index.to_h { |level, i| [i.to_s, level] },
|
|
109
|
+
probabilities: probabilities.each_with_index.to_h { |p, i| [i.to_s, p.round(4)] },
|
|
110
|
+
confidence: confidence, action_probability: action_probability
|
|
111
|
+
)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
private
|
|
115
|
+
|
|
116
|
+
def validate!
|
|
117
|
+
unless criteria.is_a?(Array)
|
|
118
|
+
raise ArgumentError, "question #{id.inspect}: a score question takes 'criteria' as an Array " \
|
|
119
|
+
"of level descriptions, index 0 first"
|
|
120
|
+
end
|
|
121
|
+
return unless criteria.empty?
|
|
122
|
+
|
|
123
|
+
raise ArgumentError, "question #{id.inspect}: a score question needs at least one level"
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Answer a yes/no statement with a calibrated probability.
|
|
128
|
+
class Noul < Question
|
|
129
|
+
TYPE = "noul"
|
|
130
|
+
|
|
131
|
+
# A noul question's confidence is how far the probability sits from a coin flip, computed
|
|
132
|
+
# from the unrounded value as upstream does, so the caller's `confidence` is not used.
|
|
133
|
+
def answer(probabilities:, action_probability:, confidence: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
134
|
+
probability = probabilities[1]
|
|
135
|
+
Answer::Noul.new(
|
|
136
|
+
probability: probability.round(4),
|
|
137
|
+
confidence: [probability, 1.0 - probability].max.round(4),
|
|
138
|
+
action_probability: action_probability
|
|
139
|
+
)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
private
|
|
143
|
+
|
|
144
|
+
# `true:` and `false:` may arrive as booleans, symbols or strings; the renderer wants strings.
|
|
145
|
+
def normalise_criteria(criteria)
|
|
146
|
+
return criteria unless criteria.is_a?(Hash)
|
|
147
|
+
|
|
148
|
+
criteria.to_h { |key, value| [key.to_s.downcase, value] }
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def validate!
|
|
152
|
+
return if criteria.nil? || criteria.is_a?(Hash)
|
|
153
|
+
|
|
154
|
+
raise ArgumentError, "question #{id.inspect}: a noul question takes 'criteria' as a Hash with " \
|
|
155
|
+
"optional 'true'/'false' descriptions, or omits it"
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
TYPES = { Choice::TYPE => Choice, Score::TYPE => Score, Noul::TYPE => Noul }.freeze
|
|
160
|
+
|
|
161
|
+
private
|
|
162
|
+
|
|
163
|
+
def normalise_criteria(criteria)
|
|
164
|
+
criteria
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def validate!
|
|
168
|
+
nil
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Laya
|
|
4
|
+
# Builds the question hash the runtime takes, from the three declarations that describe a
|
|
5
|
+
# decision. {Decision} and {Ask} both delegate here, so the two front doors cannot drift.
|
|
6
|
+
module Questions
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
# One label from a set.
|
|
10
|
+
#
|
|
11
|
+
# choice(:department, "Which team?", billing: "invoices", technical: "outages")
|
|
12
|
+
# choice(:intent, "Which intent?", criteria_hash) # labels that are not valid keywords
|
|
13
|
+
# choice(:tone, "Which tone?", %w[formal casual]) # labels with no description
|
|
14
|
+
def choice(instructions, criteria = nil, **labels)
|
|
15
|
+
criteria = labels if criteria.nil? || (criteria.respond_to?(:empty?) && criteria.empty? && labels.any?)
|
|
16
|
+
criteria = criteria.to_h { |label| [label, nil] } if criteria.is_a?(Array)
|
|
17
|
+
{ "type" => "choice", "instructions" => instructions, "criteria" => stringify(criteria) }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# A position on ordered levels, lowest first.
|
|
21
|
+
#
|
|
22
|
+
# score(:urgency, "How urgent?", levels: ["not urgent", "soon", "critical"])
|
|
23
|
+
def score(instructions, levels)
|
|
24
|
+
{ "type" => "score", "instructions" => instructions, "criteria" => Array(levels) }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# The probability that a statement holds. `yes` and `no` describe the two ends when the
|
|
28
|
+
# statement alone is ambiguous.
|
|
29
|
+
def noul(instructions, yes: nil, no: nil)
|
|
30
|
+
question = { "type" => "noul", "instructions" => instructions }
|
|
31
|
+
criteria = { "true" => yes, "false" => no }.compact
|
|
32
|
+
question["criteria"] = criteria unless criteria.empty?
|
|
33
|
+
question
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Criteria keys reach the model as text, so a symbol label is sent as its name.
|
|
37
|
+
def stringify(criteria)
|
|
38
|
+
raise ArgumentError, "criteria must be a Hash or an Array of labels" unless criteria.is_a?(Hash)
|
|
39
|
+
|
|
40
|
+
criteria.to_h { |label, description| [label.to_s, description] }
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
data/lib/laya/result.rb
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Laya
|
|
4
|
+
# One question's answer.
|
|
5
|
+
#
|
|
6
|
+
# Subclasses carry what their question type produces, and every one of them renders the payload
|
|
7
|
+
# upstream's Python returns through {#to_h}, so a Ruby result can be logged, compared or served
|
|
8
|
+
# exactly as the Python one.
|
|
9
|
+
class Answer
|
|
10
|
+
attr_reader :type, :confidence, :action_probability
|
|
11
|
+
|
|
12
|
+
def initialize(type:, confidence:, action_probability:)
|
|
13
|
+
@type = type
|
|
14
|
+
@confidence = confidence
|
|
15
|
+
@action_probability = action_probability
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def to_h
|
|
19
|
+
payload.merge("confidence" => confidence, "action" => { "act_probability" => action_probability })
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def inspect
|
|
23
|
+
"#<#{self.class.name} #{summary}>"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def to_s
|
|
27
|
+
summary
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def payload
|
|
33
|
+
raise NotImplementedError
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def summary
|
|
37
|
+
raise NotImplementedError
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# The top label of a `choice` question, with a probability per option.
|
|
41
|
+
class Choice < Answer
|
|
42
|
+
attr_reader :choice, :probabilities
|
|
43
|
+
|
|
44
|
+
def initialize(choice:, probabilities:, **rest)
|
|
45
|
+
super(type: "choice", **rest)
|
|
46
|
+
@choice = choice
|
|
47
|
+
@probabilities = probabilities
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# The probability of the chosen label, or of `label` when one is given.
|
|
51
|
+
def probability(label = choice)
|
|
52
|
+
probabilities.fetch(label) do
|
|
53
|
+
probabilities.fetch(label.to_s) do
|
|
54
|
+
raise KeyError, "no option #{label.inspect}; this question offered #{probabilities.keys.inspect}"
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# The chosen label stands in for itself, so a call site reads as the decision it is:
|
|
60
|
+
#
|
|
61
|
+
# answer == :billing # => true
|
|
62
|
+
# answer.billing? # => true
|
|
63
|
+
# case answer.to_sym ... # Ruby asks the `when` value, so compare the symbol there
|
|
64
|
+
def ==(other)
|
|
65
|
+
case other
|
|
66
|
+
when Symbol, String then choice.to_s == other.to_s
|
|
67
|
+
when Answer::Choice then choice == other.choice
|
|
68
|
+
else super
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
alias eql? ==
|
|
72
|
+
|
|
73
|
+
def hash = [self.class, choice].hash
|
|
74
|
+
def to_sym = choice.to_sym
|
|
75
|
+
def to_s = choice.to_s
|
|
76
|
+
|
|
77
|
+
# `answer.billing?` for any label this question offered.
|
|
78
|
+
def method_missing(name, *args)
|
|
79
|
+
label = name.to_s.delete_suffix("?")
|
|
80
|
+
return super unless name.to_s.end_with?("?") && args.empty? && offered?(label)
|
|
81
|
+
|
|
82
|
+
choice.to_s == label
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def respond_to_missing?(name, include_private = false)
|
|
86
|
+
(name.to_s.end_with?("?") && offered?(name.to_s.delete_suffix("?"))) || super
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def offered?(label)
|
|
90
|
+
probabilities.keys.any? { |option| option.to_s == label }
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def payload
|
|
96
|
+
{ "type" => "choice", "choice" => choice, "probabilities" => probabilities }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def summary
|
|
100
|
+
format("%s %.1f%%", choice, probability * 100)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# The expected level of a `score` question on its ordinal rubric.
|
|
105
|
+
class Score < Answer
|
|
106
|
+
attr_reader :score, :legend, :probabilities
|
|
107
|
+
|
|
108
|
+
def initialize(score:, legend:, probabilities:, **rest)
|
|
109
|
+
super(type: "score", **rest)
|
|
110
|
+
@score = score
|
|
111
|
+
@legend = legend
|
|
112
|
+
@probabilities = probabilities
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# The rubric text nearest the expected level.
|
|
116
|
+
def label
|
|
117
|
+
legend.fetch(score.round.clamp(0, legend.length - 1).to_s)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def to_f = score
|
|
121
|
+
def levels = legend.length
|
|
122
|
+
|
|
123
|
+
# Compare against a level index or its text.
|
|
124
|
+
def ==(other)
|
|
125
|
+
case other
|
|
126
|
+
when Numeric then score == other
|
|
127
|
+
when Symbol, String then label.to_s == other.to_s
|
|
128
|
+
when Answer::Score then score == other.score
|
|
129
|
+
else super
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
private
|
|
134
|
+
|
|
135
|
+
def payload
|
|
136
|
+
{ "type" => "score", "score" => score, "legend" => legend, "probabilities" => probabilities }
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def summary
|
|
140
|
+
format("%.2f of %d (%s)", score, legend.length - 1, label)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# The calibrated probability that a `noul` statement holds.
|
|
145
|
+
class Noul < Answer
|
|
146
|
+
attr_reader :probability
|
|
147
|
+
|
|
148
|
+
def initialize(probability:, **rest)
|
|
149
|
+
super(type: "noul", **rest)
|
|
150
|
+
@probability = probability
|
|
151
|
+
end
|
|
152
|
+
alias noul probability
|
|
153
|
+
|
|
154
|
+
# True when the statement is more likely than `threshold` to hold.
|
|
155
|
+
def true?(threshold = 0.5)
|
|
156
|
+
probability > threshold
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def false?(threshold = 0.5) = !true?(threshold)
|
|
160
|
+
def to_f = probability
|
|
161
|
+
|
|
162
|
+
def ==(other)
|
|
163
|
+
case other
|
|
164
|
+
when true, false then true? == other
|
|
165
|
+
when Numeric then probability == other
|
|
166
|
+
when Answer::Noul then probability == other.probability
|
|
167
|
+
else super
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
private
|
|
172
|
+
|
|
173
|
+
def payload
|
|
174
|
+
{ "type" => "noul", "noul" => probability }
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def summary
|
|
178
|
+
format("%.1f%%", probability * 100)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Everything one `predict` produced: an answer per question, the token usage, and the routing
|
|
184
|
+
# decision when a {Router} made one.
|
|
185
|
+
class Result
|
|
186
|
+
include Enumerable
|
|
187
|
+
|
|
188
|
+
MODEL_NAME = "laya-rl-agent"
|
|
189
|
+
|
|
190
|
+
attr_reader :model, :answers, :usage, :routing, :shortlist
|
|
191
|
+
|
|
192
|
+
def initialize(answers:, usage:, model: MODEL_NAME, routing: nil, shortlist: nil)
|
|
193
|
+
@answers = answers
|
|
194
|
+
@usage = usage
|
|
195
|
+
@model = model
|
|
196
|
+
@routing = routing
|
|
197
|
+
@shortlist = shortlist
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# The answer to `id`. A question asked under a symbol can be read back as a string and the
|
|
201
|
+
# other way around, so a result reads the same whichever form the caller reached for.
|
|
202
|
+
def [](id)
|
|
203
|
+
answers.fetch(id) do
|
|
204
|
+
answers.fetch(id.to_s) do
|
|
205
|
+
answers.fetch(id.to_sym) do
|
|
206
|
+
raise KeyError, "no question #{id.inspect} in this result; asked: #{answers.keys.inspect}"
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
rescue NoMethodError
|
|
211
|
+
raise KeyError, "no question #{id.inspect} in this result; asked: #{answers.keys.inspect}"
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Answers are also readable by name: `result.department` is `result[:department]`.
|
|
215
|
+
def method_missing(name, *args)
|
|
216
|
+
return super unless args.empty? && answered?(name)
|
|
217
|
+
|
|
218
|
+
self[name]
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def respond_to_missing?(name, include_private = false)
|
|
222
|
+
answered?(name) || super
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def answered?(name)
|
|
226
|
+
answers.key?(name) || answers.key?(name.to_s) || answers.key?(name.to_sym)
|
|
227
|
+
rescue NoMethodError
|
|
228
|
+
false
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def each(&)
|
|
232
|
+
answers.each(&)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def input_tokens
|
|
236
|
+
usage.fetch("input_tokens")
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# The payload upstream's Python `predict` returns, ready for JSON.
|
|
240
|
+
def to_h
|
|
241
|
+
payload = { "model" => model, "answers" => answers.transform_values(&:to_h), "usage" => usage }
|
|
242
|
+
payload["routing"] = routing.to_h if routing
|
|
243
|
+
payload["shortlist"] = shortlist if shortlist
|
|
244
|
+
payload
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def to_json(*)
|
|
248
|
+
to_h.to_json(*)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def with_routing(decision)
|
|
252
|
+
Result.new(answers: answers, usage: usage, model: model, routing: decision, shortlist: shortlist)
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def with_shortlist(meta)
|
|
256
|
+
Result.new(answers: answers, usage: usage, model: model, routing: routing, shortlist: meta)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def inspect
|
|
260
|
+
"#<Laya::Result #{answers.keys.inspect}#{" via #{routing.model}" if routing}>"
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
end
|