Class: Inquirex::Engine
- Inherits:
-
Object
- Object
- Inquirex::Engine
- Defined in:
- lib/inquirex/engine.rb,
lib/inquirex/engine/state_serializer.rb
Overview
Runtime session that drives flow navigation. Holds the definition, collected answers, and current position in the flow graph.
Collecting steps (ask, confirm): call engine.answer(value)
Display steps (say, header, btw, warning): call engine.advance
Optional steps (declared required false): engine.skip is also allowed
Validates each answer via an optional Validation::Adapter, then advances using node transitions. Skips steps whose skip_if rule evaluates to true.
Defined Under Namespace
Modules: StateSerializer
Instance Attribute Summary collapse
-
#answers ⇒ Object
readonly
Returns the value of attribute answers.
-
#completion_hook_errors ⇒ Array<StandardError>
readonly
Exceptions raised by after_completion hooks, in the order they were raised.
-
#completion_metadata ⇒ CompletionMetadata?
Metadata describing how and where the flow was completed.
-
#current_step_id ⇒ Object
readonly
Returns the value of attribute current_step_id.
-
#definition ⇒ Object
readonly
Returns the value of attribute definition.
-
#history ⇒ Object
readonly
Returns the value of attribute history.
-
#skipped ⇒ Array<Symbol>
readonly
Step ids the user explicitly skipped via #skip, in the order they were skipped.
-
#suggestions ⇒ Hash{Symbol => Array}
readonly
Answer suggestions produced by prefill! for multi-select steps, keyed by step id.
-
#totals ⇒ Object
readonly
Returns the value of attribute totals.
Class Method Summary collapse
-
.from_state(definition, state_hash, validator: Validation::NullAdapter.new) ⇒ Engine
Rebuilds an Engine from a previously saved state.
Instance Method Summary collapse
-
#advance ⇒ Object
Advances past the current non-collecting step (say/header/btw/warning).
-
#after_completion {|Engine| ... } ⇒ Engine
Registers a hook to run when the flow finishes.
-
#answer(value) ⇒ Object
Submits an answer for the current collecting step (ask/confirm).
-
#answers_with_metadata ⇒ Hash
The collected answers with the completion metadata (when a renderer attached one) merged in under the :completion_metadata key, and the user-skipped step ids (when any) under the :skipped key — so post-completion actions and API consumers can tell default-by-skip values apart from answers the user actually provided.
-
#current_step ⇒ Node?
Current step node, or nil if flow is finished.
-
#finished? ⇒ Boolean
True when there is no current step (flow ended).
-
#initialize(definition, validator: Validation::NullAdapter.new) ⇒ Engine
constructor
A new instance of Engine.
-
#prefill!(hash) ⇒ Hash
Merges a hash of { step_id => value } into the top-level answers without clobbering answers the user has already provided.
-
#skip ⇒ void
Skips the current optional collecting step at the user's request — the engine-side handler for a widget's Skip button.
-
#skipped?(step_id) ⇒ Boolean
Whether the user explicitly skipped the given step via #skip.
-
#suggestion_for(step_id) ⇒ Array?
The prefill suggestion for a step, or nil when none was recorded.
-
#text(name) ⇒ String
The running narrative of a
:textaccumulator — everything the user was shown and everything they answered, in the order it happened. -
#texts ⇒ Hash{Symbol => String}
Every text accumulator's running narrative, keyed by name.
-
#to_state ⇒ Hash
Serializable state snapshot for persistence or resumption.
-
#total(name) ⇒ Numeric
Convenience accessor for a single accumulator's running total.
Constructor Details
#initialize(definition, validator: Validation::NullAdapter.new) ⇒ Engine
Returns a new instance of Engine.
53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
# File 'lib/inquirex/engine.rb', line 53 def initialize(definition, validator: Validation::NullAdapter.new) @definition = definition @answers = {} @history = [] @current_step_id = definition.start_step_id @validator = validator @totals = init_totals @completion_metadata = nil @after_completion_hooks = [] @completion_hook_errors = [] @suggestions = {} @skipped = [] @history << @current_step_id skip_display_steps_if_needed end |
Instance Attribute Details
#answers ⇒ Object (readonly)
Returns the value of attribute answers.
14 15 16 |
# File 'lib/inquirex/engine.rb', line 14 def answers @answers end |
#completion_hook_errors ⇒ Array<StandardError> (readonly)
Exceptions raised by after_completion hooks, in the order they were raised. Hooks are isolated from one another, so a raising hook is recorded here rather than propagated — callers that care can inspect this after the flow finishes. Empty when every hook succeeded.
49 50 51 |
# File 'lib/inquirex/engine.rb', line 49 def completion_hook_errors @completion_hook_errors end |
#completion_metadata ⇒ CompletionMetadata?
Metadata describing how and where the flow was completed. Rendering front-ends (TTY, web, chat widget) attach a rich version from an after_completion hook; when no hook provides one, the engine stamps a minimal core version the moment the flow finishes. Only :engine and :engine_version are required members; everything else is free-form.
23 24 25 |
# File 'lib/inquirex/engine.rb', line 23 def @completion_metadata end |
#current_step_id ⇒ Object (readonly)
Returns the value of attribute current_step_id.
14 15 16 |
# File 'lib/inquirex/engine.rb', line 14 def current_step_id @current_step_id end |
#definition ⇒ Object (readonly)
Returns the value of attribute definition.
14 15 16 |
# File 'lib/inquirex/engine.rb', line 14 def definition @definition end |
#history ⇒ Object (readonly)
Returns the value of attribute history.
14 15 16 |
# File 'lib/inquirex/engine.rb', line 14 def history @history end |
#skipped ⇒ Array<Symbol> (readonly)
Step ids the user explicitly skipped via #skip, in the order they were skipped. Distinguishes default-by-skip values in answers from values the user actually provided. Steps elided automatically by their skip_if rule are NOT listed here — they were never presented, so the user cannot have declined them.
41 42 43 |
# File 'lib/inquirex/engine.rb', line 41 def skipped @skipped end |
#suggestions ⇒ Hash{Symbol => Array} (readonly)
Answer suggestions produced by prefill! for multi-select steps, keyed by step id. A suggestion pre-populates the step's choices in a renderer but — unlike an answer — never satisfies skip_if rules: multi-select extraction is treated as a hint the user confirms and may extend, not a deterministic fact. Cleared per step once the user answers it.
32 33 34 |
# File 'lib/inquirex/engine.rb', line 32 def suggestions @suggestions end |
#totals ⇒ Object (readonly)
Returns the value of attribute totals.
14 15 16 |
# File 'lib/inquirex/engine.rb', line 14 def totals @totals end |
Class Method Details
.from_state(definition, state_hash, validator: Validation::NullAdapter.new) ⇒ Engine
Rebuilds an Engine from a previously saved state.
302 303 304 305 306 307 |
# File 'lib/inquirex/engine.rb', line 302 def self.from_state(definition, state_hash, validator: Validation::NullAdapter.new) state = StateSerializer.symbolize_state(state_hash) engine = allocate engine.send(:restore_state, definition, state, validator) engine end |
Instance Method Details
#advance ⇒ Object
Advances past the current non-collecting step (say/header/btw/warning).
132 133 134 135 136 137 138 |
# File 'lib/inquirex/engine.rb', line 132 def advance raise Errors::AlreadyFinishedError, "Flow is already finished" if finished? node = current_step capture_transcript(Transcript.display_entry(node)) if node.display? advance_step end |
#after_completion {|Engine| ... } ⇒ Engine
Registers a hook to run when the flow finishes. The block receives the engine; front-ends typically use it to attach a rich completion_metadata (host, user, ips, terminal, ...). Optional — after all hooks run, the engine fills in a minimal CompletionMetadata when none of them provided one. Registering on an already-finished engine invokes the block immediately.
Any number of hooks may be registered; they run in registration order. Each is isolated from the others — a hook that raises a StandardError has it recorded in #completion_hook_errors, and the remaining hooks still run. Non-StandardError exceptions (Interrupt, SignalException) propagate, as they should.
256 257 258 259 260 261 262 263 264 265 |
# File 'lib/inquirex/engine.rb', line 256 def after_completion(&block) raise ArgumentError, "after_completion requires a block" unless block @after_completion_hooks << block if finished? invoke_completion_hook(block) end self end |
#answer(value) ⇒ Object
Submits an answer for the current collecting step (ask/confirm). Validates, stores, and advances to the next step.
113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 |
# File 'lib/inquirex/engine.rb', line 113 def answer(value) raise Errors::AlreadyFinishedError, "Flow is already finished" if finished? raise Errors::NonCollectingStepError, "Step #{@current_step_id} is a display step; use #advance instead" \ unless current_step.collecting? result = @validator.validate(current_step, value) raise Errors::ValidationError, "Validation failed: #{result.errors.join(", ")}" unless result.valid? node = current_step @answers[@current_step_id] = value @suggestions.delete(@current_step_id) apply_accumulations(node, value) capture_transcript(Transcript.answer_entry(node, value)) advance_step end |
#answers_with_metadata ⇒ Hash
The collected answers with the completion metadata (when a renderer attached one) merged in under the :completion_metadata key, and the user-skipped step ids (when any) under the :skipped key — so post-completion actions and API consumers can tell default-by-skip values apart from answers the user actually provided.
289 290 291 292 293 294 |
# File 'lib/inquirex/engine.rb', line 289 def extra = {} extra[:completion_metadata] = @completion_metadata.to_h if @completion_metadata extra[:skipped] = @skipped.dup unless @skipped.empty? extra.empty? ? @answers : @answers.merge(extra) end |
#current_step ⇒ Node?
Returns current step node, or nil if flow is finished.
95 96 97 98 99 |
# File 'lib/inquirex/engine.rb', line 95 def current_step return nil if finished? @definition.step(@current_step_id) end |
#finished? ⇒ Boolean
Returns true when there is no current step (flow ended).
102 103 104 |
# File 'lib/inquirex/engine.rb', line 102 def finished? @current_step_id.nil? end |
#prefill!(hash) ⇒ Hash
Merges a hash of { step_id => value } into the top-level answers without clobbering answers the user has already provided. Used by LLM clarify steps to populate downstream answers from free-text extraction; a prefilled question is treated as answered and is never asked again.
Nil/empty values in the hash are ignored so that "unknown" LLM outputs
don't spuriously satisfy not_empty rules.
Values for steps with options (enum / multi_enum) are canonicalized via Node#resolve_option — matching is against the option's form VALUE, with a case-insensitive fallback and a label fallback ("US citizen or permanent resident" resolves to "us_person"). A value that matches neither value nor label is dropped, so junk never enters the answers.
Prefilled answers contribute to accumulators exactly like typed ones, and if the engine's current step becomes skippable as a result of the prefill, it auto-advances past it.
208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 |
# File 'lib/inquirex/engine.rb', line 208 def prefill!(hash) return @answers unless hash.is_a?(Hash) hash.each do |key, value| next if value.nil? next if value.respond_to?(:empty?) && value.empty? sym = key.to_sym if multi_select_step?(sym) prefill_suggestion(sym, value) else prefill_answer(sym, value) end end skip_if_needed unless finished? @answers end |
#skip ⇒ void
This method returns an undefined value.
Skips the current optional collecting step at the user's request — the
engine-side handler for a widget's Skip button. Only steps declared with
required false may be skipped.
When the step has a default, the default is recorded into the answers and contributes to accumulators exactly as if the user had submitted it; the step id lands in #skipped so consumers can tell the value apart from one the user actually provided. Without a default, no answers entry is written (rules read a missing key as nil, so branching is unaffected). Advances through transitions exactly like #answer.
161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 |
# File 'lib/inquirex/engine.rb', line 161 def skip raise Errors::AlreadyFinishedError, "Flow is already finished" if finished? raise Errors::NonCollectingStepError, "Step #{@current_step_id} is a display step; use #advance instead" \ unless current_step.collecting? raise Errors::RequiredStepError, "Step #{@current_step_id} is required and cannot be skipped" \ if current_step.required? node = current_step default = resolve_default(node) unless default.nil? @answers[@current_step_id] = default apply_accumulations(node, default) end @skipped << @current_step_id unless @skipped.include?(@current_step_id) @suggestions.delete(@current_step_id) capture_transcript(Transcript.skipped_entry(node)) advance_step end |
#skipped?(step_id) ⇒ Boolean
Whether the user explicitly skipped the given step via #skip.
184 185 186 |
# File 'lib/inquirex/engine.rb', line 184 def skipped?(step_id) @skipped.include?(step_id.to_sym) end |
#suggestion_for(step_id) ⇒ Array?
The prefill suggestion for a step, or nil when none was recorded.
230 231 232 |
# File 'lib/inquirex/engine.rb', line 230 def suggestion_for(step_id) @suggestions[step_id.to_sym] end |
#text(name) ⇒ String
The running narrative of a :text accumulator — everything the user was
shown and everything they answered, in the order it happened. This is
what an LLM summarize step reads.
83 84 85 |
# File 'lib/inquirex/engine.rb', line 83 def text(name) @totals[name.to_sym].to_s end |
#texts ⇒ Hash{Symbol => String}
Every text accumulator's running narrative, keyed by name.
90 91 92 |
# File 'lib/inquirex/engine.rb', line 90 def texts text_accumulator_names.to_h { |name| [name, text(name)] } end |
#to_state ⇒ Hash
Serializable state snapshot for persistence or resumption.
270 271 272 273 274 275 276 277 278 279 280 |
# File 'lib/inquirex/engine.rb', line 270 def to_state { current_step_id: @current_step_id, answers: @answers, history: @history, totals: @totals, suggestions: @suggestions, skipped: @skipped, completion_metadata: @completion_metadata&.to_h } end |
#total(name) ⇒ Numeric
Convenience accessor for a single accumulator's running total.
73 74 75 |
# File 'lib/inquirex/engine.rb', line 73 def total(name) @totals[name.to_sym] || 0 end |