inquirex-llm 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.
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Inquirex
4
+ module LLM
5
+ # Abstract interface for LLM adapters. Adapters bridge the gap between
6
+ # LLM::Node definitions and actual LLM API calls.
7
+ #
8
+ # Implementations must:
9
+ # 1. Accept an LLM::Node and current answers
10
+ # 2. Construct the appropriate prompt (using node.prompt, node.from_steps, etc.)
11
+ # 3. Call the LLM API
12
+ # 4. Parse and validate the response against node.schema (if present)
13
+ # 5. Return a Hash or String result
14
+ #
15
+ # The adapter is invoked server-side when the engine reaches an LLM step.
16
+ # It is never called on the frontend.
17
+ #
18
+ # @example Implementing a custom adapter
19
+ # class MyLlmAdapter < Inquirex::LLM::Adapter
20
+ # def call(node, answers)
21
+ # prompt_text = build_prompt(node, answers)
22
+ # response = my_llm_client.complete(prompt_text, model: node.model)
23
+ # parse_response(response, node.schema)
24
+ # end
25
+ # end
26
+ class Adapter
27
+ # Processes an LLM step and returns the result.
28
+ #
29
+ # @param node [LLM::Node] the LLM step to process
30
+ # @param answers [Hash] current collected answers
31
+ # @return [Hash, String] structured output (for clarify/detour) or text (for describe/summarize)
32
+ # @raise [Errors::AdapterError] if the LLM call fails
33
+ # @raise [Errors::SchemaViolationError] if output doesn't match schema
34
+ def call(node, answers)
35
+ raise NotImplementedError, "#{self.class}#call must be implemented"
36
+ end
37
+
38
+ # Gathers the source answer data that feeds the LLM prompt.
39
+ #
40
+ # @param node [LLM::Node]
41
+ # @param answers [Hash]
42
+ # @return [Hash] relevant subset of answers
43
+ def source_answers(node, answers)
44
+ if node.from_all
45
+ answers.dup
46
+ else
47
+ node.from_steps.each_with_object({}) do |step_id, acc|
48
+ acc[step_id] = answers[step_id] if answers.key?(step_id)
49
+ end
50
+ end
51
+ end
52
+
53
+ # Validates adapter output against the node's schema.
54
+ #
55
+ # @param node [LLM::Node]
56
+ # @param output [Hash, String]
57
+ # @raise [Errors::SchemaViolationError] if validation fails
58
+ def validate_output!(node, output)
59
+ return unless node.schema
60
+
61
+ missing = node.schema.missing_fields(output)
62
+ return if missing.empty?
63
+
64
+ raise Errors::SchemaViolationError,
65
+ "LLM output for #{node.id.inspect} missing fields: #{missing.join(", ")}"
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Inquirex
4
+ module LLM
5
+ module DSL
6
+ # Mixin that adds LLM verb methods to Inquirex::DSL::FlowBuilder.
7
+ # Included automatically when `require "inquirex-llm"` is called,
8
+ # so that `Inquirex.define` gains clarify/describe/summarize/detour
9
+ # without needing a separate entry point.
10
+ #
11
+ # All core verbs (ask, say, header, btw, warning, confirm) remain
12
+ # unchanged — LLM verbs are purely additive.
13
+ module FlowBuilderExtension
14
+ # Defines an LLM extraction step: takes free-text input and produces
15
+ # structured data matching the declared schema.
16
+ #
17
+ # @param id [Symbol] step id
18
+ def clarify(id, &)
19
+ add_llm_step(id, :clarify, &)
20
+ end
21
+
22
+ # Defines an LLM description step: takes structured data and produces
23
+ # natural-language text.
24
+ #
25
+ # @param id [Symbol] step id
26
+ def describe(id, &)
27
+ add_llm_step(id, :describe, &)
28
+ end
29
+
30
+ # Defines an LLM summarization step: takes all or selected answers and
31
+ # produces a textual summary.
32
+ #
33
+ # @param id [Symbol] step id
34
+ def summarize(id, &)
35
+ add_llm_step(id, :summarize, &)
36
+ end
37
+
38
+ # Defines an LLM detour step: based on an answer, dynamically generates
39
+ # follow-up questions. The server adapter handles presenting the generated
40
+ # questions and collecting responses.
41
+ #
42
+ # @param id [Symbol] step id
43
+ def detour(id, &)
44
+ add_llm_step(id, :detour, &)
45
+ end
46
+
47
+ private
48
+
49
+ # Uses the standard Ruby builder pattern (same as core FlowBuilder#add_step).
50
+ def add_llm_step(id, verb, &block)
51
+ builder = LlmStepBuilder.new(verb)
52
+ builder.instance_eval(&block) if block
53
+ @nodes[id.to_sym] = builder.build(id)
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,178 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Inquirex
4
+ module LLM
5
+ module DSL
6
+ # Builds an LLM::Node from a step DSL block. Handles the LLM-specific
7
+ # methods (from, prompt, schema, model, temperature, max_tokens, fallback)
8
+ # while inheriting transition and skip_if from the core StepBuilder.
9
+ #
10
+ # @example
11
+ # clarify :business_extracted do
12
+ # from :business_description
13
+ # prompt "Extract structured business info."
14
+ # schema industry: :string, employee_count: :integer
15
+ # model :claude_sonnet
16
+ # temperature 0.2
17
+ # transition to: :next_step
18
+ # end
19
+ class LlmStepBuilder
20
+ include Inquirex::DSL::RuleHelpers
21
+
22
+ def initialize(verb)
23
+ @verb = verb.to_sym
24
+ @prompt = nil
25
+ @schema_fields = {}
26
+ @from_steps = []
27
+ @from_all = false
28
+ @model = nil
29
+ @temperature = nil
30
+ @max_tokens = nil
31
+ @fallback = nil
32
+ @transitions = []
33
+ @skip_if = nil
34
+ @question = nil
35
+ @text = nil
36
+ end
37
+
38
+ # Sets the LLM prompt template. Use {{field_name}} for interpolation
39
+ # placeholders that the adapter resolves at runtime.
40
+ #
41
+ # @param text [String]
42
+ def prompt(text)
43
+ @prompt = text
44
+ end
45
+
46
+ # Declares the expected output schema. Each key is a field name,
47
+ # each value is an Inquirex data type symbol.
48
+ #
49
+ # @param fields [Hash{Symbol => Symbol}]
50
+ def schema(**fields)
51
+ @schema_fields.merge!(fields)
52
+ end
53
+
54
+ # Adds source step id(s) whose answers feed the LLM prompt.
55
+ #
56
+ # @param step_ids [Symbol, Array<Symbol>] one or more step ids
57
+ def from(*step_ids)
58
+ @from_steps.concat(step_ids.flatten)
59
+ end
60
+
61
+ # Passes all collected answers to the LLM prompt.
62
+ #
63
+ # @param value [Boolean]
64
+ def from_all(value = true)
65
+ @from_all = !!value
66
+ end
67
+
68
+ # Optional model hint for the adapter.
69
+ #
70
+ # @param name [Symbol] e.g. :claude_sonnet, :claude_haiku
71
+ def model(name)
72
+ @model = name.to_sym
73
+ end
74
+
75
+ # Optional sampling temperature.
76
+ #
77
+ # @param value [Float]
78
+ def temperature(value)
79
+ @temperature = value.to_f
80
+ end
81
+
82
+ # Optional maximum output tokens.
83
+ #
84
+ # @param value [Integer]
85
+ def max_tokens(value)
86
+ @max_tokens = value.to_i
87
+ end
88
+
89
+ # Server-side fallback block, invoked when the LLM call fails.
90
+ # Stripped from JSON serialization.
91
+ #
92
+ # @yield [Hash] answers collected so far
93
+ # @return [Object] fallback value to store as the answer
94
+ def fallback(&block)
95
+ @fallback = block
96
+ end
97
+
98
+ # Adds a conditional transition. Inherited concept from core DSL.
99
+ # All LLM transitions are implicitly requires_server: true.
100
+ #
101
+ # @param to [Symbol] target step id
102
+ # @param if_rule [Rules::Base, nil]
103
+ # @param requires_server [Boolean]
104
+ def transition(to:, if_rule: nil, requires_server: true)
105
+ @transitions << Inquirex::Transition.new(target: to, rule: if_rule, requires_server:)
106
+ end
107
+
108
+ # Sets a rule that skips this step entirely when true.
109
+ #
110
+ # @param rule [Rules::Base]
111
+ def skip_if(rule)
112
+ @skip_if = rule
113
+ end
114
+
115
+ # Optional display text (used by describe/summarize for user-visible labels).
116
+ #
117
+ # @param content [String]
118
+ def question(content)
119
+ @question = content
120
+ end
121
+
122
+ # Optional display text for context.
123
+ #
124
+ # @param content [String]
125
+ def text(content)
126
+ @text = content
127
+ end
128
+
129
+ # Builds the LLM::Node.
130
+ #
131
+ # @param id [Symbol]
132
+ # @return [LLM::Node]
133
+ # @raise [Errors::DefinitionError] if prompt is missing
134
+ def build(id)
135
+ validate!(id)
136
+
137
+ schema_obj = @schema_fields.empty? ? nil : Schema.new(**@schema_fields)
138
+
139
+ LLM::Node.new(
140
+ id:,
141
+ verb: @verb,
142
+ prompt: @prompt,
143
+ schema: schema_obj,
144
+ from_steps: @from_steps,
145
+ from_all: @from_all,
146
+ model: @model,
147
+ temperature: @temperature,
148
+ max_tokens: @max_tokens,
149
+ fallback: @fallback,
150
+ question: @question,
151
+ text: @text,
152
+ transitions: @transitions,
153
+ skip_if: @skip_if
154
+ )
155
+ end
156
+
157
+ private
158
+
159
+ def validate!(id)
160
+ raise Errors::DefinitionError, "LLM step #{id.inspect} requires a prompt" if @prompt.nil?
161
+
162
+ if %i[clarify detour].include?(@verb) && @schema_fields.empty?
163
+ raise Errors::DefinitionError,
164
+ "LLM step #{id.inspect} (#{@verb}) requires a schema"
165
+ end
166
+
167
+ return unless @from_steps.empty? && !@from_all && @verb != :summarize
168
+
169
+ # clarify/describe/detour should reference source steps or from_all
170
+ return unless %i[clarify describe detour].include?(@verb)
171
+
172
+ raise Errors::DefinitionError,
173
+ "LLM step #{id.inspect} (#{@verb}) requires `from` or `from_all`"
174
+ end
175
+ end
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Inquirex
4
+ module LLM
5
+ module Errors
6
+ # Base exception for all LLM-related errors.
7
+ class Error < Inquirex::Errors::Error; end
8
+
9
+ # Raised when an LLM step definition is invalid (e.g. missing prompt, bad schema).
10
+ class DefinitionError < Error; end
11
+
12
+ # Raised when the LLM adapter returns output that doesn't match the declared schema.
13
+ class SchemaViolationError < Error; end
14
+
15
+ # Raised when the LLM adapter call fails after exhausting retries.
16
+ class AdapterError < Error; end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Inquirex
4
+ module LLM
5
+ # Enriched node for LLM-powered steps. Extends Inquirex::Node with attributes
6
+ # needed by the server-side LLM adapter: prompt template, output schema, source
7
+ # step references, and model configuration.
8
+ #
9
+ # LLM verbs:
10
+ # :clarify — extract structured data from a free-text answer
11
+ # :describe — generate natural-language text from structured data
12
+ # :summarize — produce a summary of all or selected answers
13
+ # :detour — dynamically generate follow-up questions based on an answer
14
+ #
15
+ # All LLM nodes are collecting (they produce answers) and require server
16
+ # round-trips. The frontend shows a "thinking" state while the server processes.
17
+ #
18
+ # @attr_reader prompt [String] LLM prompt template
19
+ # @attr_reader schema [Schema, nil] expected output structure (required for clarify/detour)
20
+ # @attr_reader from_steps [Array<Symbol>] source step ids whose answers feed the LLM
21
+ # @attr_reader from_all [Boolean] whether to pass all collected answers to the LLM
22
+ # @attr_reader model [Symbol, nil] optional model hint (e.g. :claude_sonnet)
23
+ # @attr_reader temperature [Float, nil] optional sampling temperature
24
+ # @attr_reader max_tokens [Integer, nil] optional max output tokens
25
+ # @attr_reader fallback [Proc, nil] server-side fallback (stripped from JSON)
26
+ class Node < Inquirex::Node
27
+ LLM_VERBS = %i[clarify describe summarize detour].freeze
28
+
29
+ attr_reader :prompt,
30
+ :schema,
31
+ :from_steps,
32
+ :from_all,
33
+ :model,
34
+ :temperature,
35
+ :max_tokens,
36
+ :fallback
37
+
38
+ def initialize(prompt:, schema: nil, from_steps: [], from_all: false,
39
+ model: nil, temperature: nil, max_tokens: nil, fallback: nil, **)
40
+ @prompt = prompt
41
+ @schema = schema
42
+ @from_steps = Array(from_steps).map(&:to_sym).freeze
43
+ @from_all = !!from_all
44
+ @model = model&.to_sym
45
+ @temperature = temperature&.to_f
46
+ @max_tokens = max_tokens&.to_i
47
+ @fallback = fallback
48
+ super(**)
49
+ end
50
+ # rubocop:enable Metrics/ParameterLists
51
+
52
+ # LLM verbs always collect output (the LLM provides the "answer").
53
+ def collecting? = true
54
+
55
+ # LLM verbs are never display-only.
56
+ def display? = false
57
+
58
+ # Whether this is an LLM-powered step requiring server processing.
59
+ def llm_verb? = true
60
+
61
+ # Serializes to a plain Hash. LLM metadata is nested under "llm".
62
+ # Fallback procs are stripped (server-side only).
63
+ # All transitions are marked requires_server: true.
64
+ #
65
+ # @return [Hash]
66
+ def to_h
67
+ hash = { "verb" => @verb.to_s }
68
+ hash["question"] = @question if @question
69
+ hash["text"] = @text if @text
70
+ hash["transitions"] = @transitions.map(&:to_h) unless @transitions.empty?
71
+ hash["skip_if"] = @skip_if.to_h if @skip_if
72
+ hash["requires_server"] = true
73
+
74
+ llm_hash = { "prompt" => @prompt }
75
+ llm_hash["schema"] = @schema.to_h if @schema
76
+ llm_hash["from_steps"] = @from_steps.map(&:to_s) unless @from_steps.empty?
77
+ llm_hash["from_all"] = true if @from_all
78
+ llm_hash["model"] = @model.to_s if @model
79
+ llm_hash["temperature"] = @temperature if @temperature
80
+ llm_hash["max_tokens"] = @max_tokens if @max_tokens
81
+ hash["llm"] = llm_hash
82
+
83
+ hash
84
+ end
85
+
86
+ # Deserializes from a plain Hash (string or symbol keys).
87
+ #
88
+ # @param id [Symbol, String]
89
+ # @param hash [Hash]
90
+ # @return [LLM::Node]
91
+ def self.from_h(id, hash)
92
+ verb = hash["verb"] || hash[:verb]
93
+ question = hash["question"] || hash[:question]
94
+ text = hash["text"] || hash[:text]
95
+ transitions_data = hash["transitions"] || hash[:transitions] || []
96
+ skip_if_data = hash["skip_if"] || hash[:skip_if]
97
+ llm_data = hash["llm"] || hash[:llm] || {}
98
+
99
+ transitions = transitions_data.map { |t| Inquirex::Transition.from_h(t) }
100
+ skip_if = skip_if_data ? Inquirex::Rules::Base.from_h(skip_if_data) : nil
101
+
102
+ prompt = llm_data["prompt"] || llm_data[:prompt]
103
+ schema_raw = llm_data["schema"] || llm_data[:schema]
104
+ from_raw = llm_data["from_steps"] || llm_data[:from_steps] || []
105
+ from_all = llm_data["from_all"] || llm_data[:from_all] || false
106
+ model = llm_data["model"] || llm_data[:model]
107
+ temp = llm_data["temperature"] || llm_data[:temperature]
108
+ max_tok = llm_data["max_tokens"] || llm_data[:max_tokens]
109
+
110
+ schema = schema_raw ? Schema.from_h(schema_raw) : nil
111
+ from_steps = from_raw.map(&:to_sym)
112
+
113
+ new(
114
+ id:,
115
+ verb:,
116
+ question:,
117
+ text:,
118
+ transitions:,
119
+ skip_if:,
120
+ prompt:,
121
+ schema:,
122
+ from_steps:,
123
+ from_all:,
124
+ model:,
125
+ temperature: temp,
126
+ max_tokens: max_tok
127
+ )
128
+ end
129
+
130
+ # Whether this verb is a recognized LLM verb.
131
+ def self.llm_verb?(verb)
132
+ LLM_VERBS.include?(verb.to_sym)
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Inquirex
4
+ module LLM
5
+ # Test adapter that returns schema-conformant placeholder values without
6
+ # calling any LLM API. Useful for testing flows that include LLM steps.
7
+ #
8
+ # For clarify/detour steps with a schema, returns a hash of default values
9
+ # matching each field's declared type. For describe/summarize steps, returns
10
+ # a placeholder string.
11
+ #
12
+ # @example
13
+ # adapter = Inquirex::LLM::NullAdapter.new
14
+ # result = adapter.call(clarify_node, answers)
15
+ # # => { industry: "", employee_count: 0, ... }
16
+ class NullAdapter < Adapter
17
+ TYPE_DEFAULTS = {
18
+ string: "",
19
+ text: "",
20
+ integer: 0,
21
+ decimal: 0.0,
22
+ currency: 0.0,
23
+ boolean: false,
24
+ enum: "",
25
+ multi_enum: [],
26
+ date: "",
27
+ email: "",
28
+ phone: "",
29
+ array: [],
30
+ hash: {}
31
+ }.freeze
32
+
33
+ # Returns placeholder output matching the node's schema or verb.
34
+ #
35
+ # @param node [LLM::Node]
36
+ # @param _answers [Hash] ignored
37
+ # @return [Hash, String]
38
+ def call(node, _answers = {})
39
+ if node.schema
40
+ node.schema.fields.each_with_object({}) do |(name, type), acc|
41
+ acc[name] = TYPE_DEFAULTS.fetch(type, "")
42
+ end
43
+ else
44
+ "(placeholder #{node.verb} output for #{node.id})"
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Inquirex
4
+ module LLM
5
+ # Immutable definition of expected LLM output structure.
6
+ # Each field maps a name to an Inquirex data type, forming the contract
7
+ # between the LLM prompt and the structured data it must return.
8
+ #
9
+ # @example
10
+ # schema = Schema.new(
11
+ # industry: :string,
12
+ # entity_type: :enum,
13
+ # employee_count: :integer,
14
+ # estimated_revenue: :currency
15
+ # )
16
+ # schema.fields # => { industry: :string, ... }
17
+ # schema.field_names # => [:industry, :entity_type, ...]
18
+ # schema.valid_output?({ industry: "Tech", ... }) # => true
19
+ #
20
+ # @attr_reader fields [Hash{Symbol => Symbol}] field_name => type mapping
21
+ class Schema
22
+ VALID_TYPES = %i[
23
+ string text integer decimal currency boolean
24
+ enum multi_enum date email phone
25
+ array hash
26
+ ].freeze
27
+
28
+ attr_reader :fields
29
+
30
+ # @param field_map [Hash{Symbol => Symbol}] field_name => type
31
+ # @raise [Errors::DefinitionError] if any type is unrecognized
32
+ def initialize(**field_map)
33
+ raise Errors::DefinitionError, "Schema must have at least one field" if field_map.empty?
34
+
35
+ validate_types!(field_map)
36
+ @fields = field_map.transform_keys(&:to_sym)
37
+ .transform_values(&:to_sym)
38
+ .freeze
39
+ freeze
40
+ end
41
+
42
+ # @return [Array<Symbol>] ordered list of field names
43
+ def field_names = @fields.keys
44
+
45
+ # @return [Integer] number of fields
46
+ def size = @fields.size
47
+
48
+ # Checks whether a Hash output conforms to the schema (all declared fields present).
49
+ #
50
+ # @param output [Hash] LLM output to validate
51
+ # @return [Boolean]
52
+ def valid_output?(output)
53
+ return false unless output.is_a?(Hash)
54
+
55
+ symbolized = output.transform_keys(&:to_sym)
56
+ @fields.keys.all? { |key| symbolized.key?(key) }
57
+ end
58
+
59
+ # Returns the list of fields missing from the given output.
60
+ #
61
+ # @param output [Hash]
62
+ # @return [Array<Symbol>]
63
+ def missing_fields(output)
64
+ return field_names unless output.is_a?(Hash)
65
+
66
+ symbolized = output.transform_keys(&:to_sym)
67
+ @fields.keys.reject { |key| symbolized.key?(key) }
68
+ end
69
+
70
+ # @return [Hash]
71
+ def to_h
72
+ @fields.transform_keys(&:to_s).transform_values(&:to_s)
73
+ end
74
+
75
+ # @return [String] JSON representation
76
+ def to_json(*)
77
+ JSON.generate(to_h)
78
+ end
79
+
80
+ # @param hash [Hash] string or symbol keys, values are type names
81
+ # @return [Schema]
82
+ def self.from_h(hash)
83
+ field_map = hash.each_with_object({}) do |(k, v), acc|
84
+ acc[k.to_sym] = v.to_sym
85
+ end
86
+ new(**field_map)
87
+ end
88
+
89
+ def ==(other)
90
+ other.is_a?(Schema) && @fields == other.fields
91
+ end
92
+
93
+ def inspect
94
+ "#<Inquirex::LLM::Schema #{@fields.map { |k, v| "#{k}:#{v}" }.join(", ")}>"
95
+ end
96
+
97
+ private
98
+
99
+ def validate_types!(field_map)
100
+ field_map.each do |name, type|
101
+ next if VALID_TYPES.include?(type.to_sym)
102
+
103
+ raise Errors::DefinitionError,
104
+ "Unknown type #{type.inspect} for field #{name.inspect}. " \
105
+ "Valid types: #{VALID_TYPES.join(", ")}"
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Inquirex
4
+ module LLM
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "inquirex"
4
+ require "json"
5
+
6
+ require_relative "llm/version"
7
+ require_relative "llm/errors"
8
+ require_relative "llm/schema"
9
+ require_relative "llm/node"
10
+ require_relative "llm/adapter"
11
+ require_relative "llm/null_adapter"
12
+ require_relative "llm/dsl/llm_step_builder"
13
+ require_relative "llm/dsl/flow_builder"
14
+
15
+ module Inquirex
16
+ # LLM integration layer for Inquirex flows.
17
+ #
18
+ # Extends the core DSL with four LLM-powered verbs that run server-side:
19
+ # - clarify — extract structured data from free-text answers
20
+ # - describe — generate natural-language text from structured data
21
+ # - summarize — produce a summary of all or selected answers
22
+ # - detour — dynamically generate follow-up questions
23
+ #
24
+ # LLM calls never happen on the frontend. Steps are marked `requires_server: true`
25
+ # in the JSON wire format so the JS widget knows to round-trip to the server.
26
+ #
27
+ # Usage:
28
+ # require "inquirex"
29
+ # require "inquirex-llm"
30
+ #
31
+ # Inquirex.define id: "intake" do
32
+ # start :description
33
+ # ask(:description) { type :text; question "Describe your business."; transition to: :extracted }
34
+ # clarify(:extracted) { from :description; prompt "Extract info."; schema name: :string; transition to: :done }
35
+ # say(:done) { text "Done!" }
36
+ # end
37
+ module LLM
38
+ end
39
+ end
40
+
41
+ # Inject LLM verbs into the core FlowBuilder so that Inquirex.define
42
+ # gains clarify/describe/summarize/detour when this gem is loaded.
43
+ Inquirex::DSL::FlowBuilder.include(Inquirex::LLM::DSL::FlowBuilderExtension)
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "inquirex/llm"
@@ -0,0 +1,6 @@
1
+ module Inquirex
2
+ module Llm
3
+ VERSION: String
4
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
5
+ end
6
+ end