inquirex-llm 0.5.0 → 0.6.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: db7e0d183259c896f2b1b5d9bd6ce1808f3a0c89de415a7129d2da2133a1ea49
4
- data.tar.gz: dc3a1659f93182d83972dfccaaacbec820509823c293a97de4ac6df596409701
3
+ metadata.gz: 00b1d649377bb6ff1281a8fd3af730995b86531e44c930cb3c9a267b73e5efe0
4
+ data.tar.gz: '0478c2970b4d501ee3b9b2ed4dcdd2d620fbc99c522ac863e3ac772a4f75dce6'
5
5
  SHA512:
6
- metadata.gz: 6b8e8a8c3069ae95d904e3635d3b4947997e812d678dbf562040ae0647563692edef7364977e20ac2eb972d18d3aaeed88f606a1b05fc2d842ba201ff487b748
7
- data.tar.gz: a3d8a7a89d4f22b7c3420ae1b22114895bee402314b6e4d303dfd3702f8afae1aeb87eac9a35e53090279a188b2d838c56759a8d2cecfc31179006f64d0d8fe7
6
+ metadata.gz: a89a5e3ec65fab4be526a2d8bc730c8bb89bfe9b7945ef900fb987546765c29600219e1959dfb9011a43916658feb56f1f5b70625d3645a26aa75bbbe290ebdb
7
+ data.tar.gz: b9c05000c0b31721f3c00d425b0e525ef309540335d08c10c5c69ba2a6089fab2bc2d10ce53da141f62f1735a631c993b798ed8a7353c50cfb225b4635d59697
data/README.md CHANGED
@@ -17,7 +17,7 @@ Extends the core DSL with a server-side `extract` verb (alias: `clarify`) that t
17
17
  > - [`inquirex-tty`](https://github.com/inquirex/inquirex-)
18
18
  > - [`inquirex-js`](https://github.com/inquirex/inquirex-js) (`npmjs` module [`@kigster/inquirex-js`](https://www.npmjs.com/package/@kigster/inquirex-js))
19
19
  >
20
- > For a presentation about these gems and what they do please watch the [RubySF presentation](https://www.youtube.com/watch?v=iaoKW7Ap3_M&t=1s) and you can also [view the slides form the presentation](https://reinvent.one/images/talks/pdfs/2026.inquirex.pdf).
20
+ > For a presentation about these gems and what they do please watch the [RubySF presentation](https://www.youtube.com/watch?v=iaoKW7Ap3_M&t=1s) and you can also [view the slides from the presentation](https://reinvent.one/images/talks/pdfs/2026.inquirex.pdf).
21
21
  >
22
22
  > Finally, the SaaS application [qualified.at](https://qualified.at) allows users to leverage the ecosystem by creating their own custom lead intake flows and integrating them on their own sites.
23
23
 
@@ -78,20 +78,89 @@ extract :business_extracted do
78
78
  end
79
79
  ```
80
80
 
81
+ ## Schema: Question References (preferred)
82
+
83
+ Most extract schemas exist to pre-fill questions asked later in the same flow. Declaring
84
+ those fields twice — once in the schema, once in the question — invites drift, and worse:
85
+ a hand-typed `income_types: :multi_enum` gives the LLM no idea which values are legal, so
86
+ its answers won't match the question's options.
87
+
88
+ Instead, pass the schema as a list of question ids:
89
+
90
+ ```ruby
91
+ extract :extracted do
92
+ from :description
93
+ prompt "Extract the client's filing status, dependents, and income types."
94
+ schema :filing_status, :dependents, :income_types
95
+ transition to: :filing_status
96
+ end
97
+
98
+ ask :filing_status do
99
+ type :enum
100
+ question "Filing status?"
101
+ options({ "single" => "Single", "mfj" => "Married Filing Jointly" })
102
+ transition to: :dependents
103
+ end
104
+ # ...
105
+ ```
106
+
107
+ Each symbol is resolved against the flow at definition time — references may point
108
+ **forward** to questions defined after the extract step. The gem looks up the question's
109
+ declared type, and for `:enum` / `:multi_enum` questions folds the exhaustive list of
110
+ allowed option values into the JSON schema sent to the LLM. The adapters then instruct
111
+ the model to answer using only those values, so extracted answers always match the
112
+ downstream question's options (and `Engine#prefill!` can skip the question).
113
+
114
+ A symbol that matches no `ask`/`confirm` step in the flow fails validation with
115
+ `Inquirex::LLM::Errors::DefinitionError` — as do references to display-only steps and
116
+ other LLM steps.
117
+
118
+ Both forms compose. Use keywords for output fields that have no corresponding question:
119
+
120
+ ```ruby
121
+ schema :filing_status, :income_types, confidence: :decimal
122
+ ```
123
+
124
+ ### `prompt :auto`
125
+
126
+ When the schema is built from question references, the schema already tells the LLM the
127
+ field names, types, and allowed values — the main thing a hand-written prompt still adds
128
+ is the questions' own wording. `prompt :auto` generates exactly that at definition time:
129
+
130
+ ```ruby
131
+ extract :extracted do
132
+ from :description
133
+ prompt :auto
134
+ schema :filing_status, :dependents, :income_types
135
+ transition to: :filing_status
136
+ end
137
+ ```
138
+
139
+ The generated prompt enumerates each referenced question's text ("- filing_status: What
140
+ is your filing status for 2025?" …), lists explicit keyword fields by name and type, and
141
+ instructs the model to leave unsupported fields empty. Generation happens at build time,
142
+ so the wire format and adapters always see a concrete prompt string — `:auto` never
143
+ leaves the DSL. It requires at least one question reference; with only explicit
144
+ `key: :type` fields there is no question wording to generate from, and validation fails.
145
+
146
+ Write the prompt by hand when you need domain framing the questions don't carry
147
+ ("for tax filing purposes", "map S-Corp to s_corp") — an explicit prompt always wins.
148
+
81
149
  ## DSL Methods (inside LLM verb blocks)
82
150
 
83
- | Method | Purpose | Required |
84
- | ------------------------------ | ----------------------------------------- | ----------------------------- |
85
- | `prompt "..."` | LLM prompt template | Always |
86
- | `schema key: :type, ...` | Expected output structure | `extract` |
87
- | `from :step_id` | Source step(s) whose answers feed the LLM | `extract` (or use `from_all`) |
88
- | `from_all` | Pass all collected answers to the LLM | Alternative to `from` |
89
- | `model :claude_sonnet` | Optional model hint for the adapter | No |
90
- | `temperature 0.3` | Optional sampling temperature | No |
91
- | `max_tokens 1024` | Optional max output tokens | No |
92
- | `fallback { \|answers\| ... }` | Server-side fallback (stripped from JSON) | No |
93
- | `transition to: :step` | Conditional transition (same as core) | No |
94
- | `skip_if rule` | Skip step when condition is true | No |
151
+ | Method | Purpose | Required |
152
+ | ------------------------------- | ---------------------------------------------------- | ----------------------------- |
153
+ | `prompt "..."` / `prompt :auto` | LLM prompt template, or generated from question refs | Always |
154
+ | `schema :question_id, ...` | Fields resolved from questions (types + options) | `extract` (this or keywords) |
155
+ | `schema key: :type, ...` | Explicit field => type pairs | `extract` (this or refs) |
156
+ | `from :step_id` | Source step(s) whose answers feed the LLM | `extract` (or use `from_all`) |
157
+ | `from_all` | Pass all collected answers to the LLM | Alternative to `from` |
158
+ | `model :claude_sonnet` | Optional model hint for the adapter | No |
159
+ | `temperature 0.3` | Optional sampling temperature | No |
160
+ | `max_tokens 1024` | Optional max output tokens | No |
161
+ | `fallback { \|answers\| ... }` | Server-side fallback (stripped from JSON) | No |
162
+ | `transition to: :step` | Conditional transition (same as core) | No |
163
+ | `skip_if rule` | Skip step when condition is true | No |
95
164
 
96
165
  ## Engine Integration
97
166
 
@@ -224,7 +293,11 @@ LLM steps serialize with `"requires_server": true` so the JS widget knows to rou
224
293
  "schema": {
225
294
  "industry": "string",
226
295
  "employee_count": "integer",
227
- "revenue": "currency"
296
+ "revenue": "currency",
297
+ "income_types": {
298
+ "type": "multi_enum",
299
+ "values": ["W2", "business", "crypto"]
300
+ }
228
301
  },
229
302
  "from_steps": ["business_description"],
230
303
  "model": "claude_sonnet",
@@ -234,7 +307,10 @@ LLM steps serialize with `"requires_server": true` so the JS widget knows to rou
234
307
  }
235
308
  ```
236
309
 
237
- Fallback procs are stripped from JSON (server-side only).
310
+ Unconstrained fields serialize as a plain type string; fields resolved from `:enum` /
311
+ `:multi_enum` questions serialize as `{ "type": ..., "values": [...] }` so any consumer
312
+ (the JS widget, a server adapter) sees the full contract. Fallback procs are stripped
313
+ from JSON (server-side only).
238
314
 
239
315
  ## Custom Adapter
240
316
 
@@ -64,6 +64,45 @@ module Inquirex
64
64
  raise Errors::SchemaViolationError,
65
65
  "LLM output for #{node.id.inspect} missing fields: #{missing.join(", ")}"
66
66
  end
67
+
68
+ protected
69
+
70
+ # The schema as a JSON contract for the system prompt: enum-constrained
71
+ # fields render as { "type": ..., "values": [...] } so the model knows
72
+ # the exhaustive list of allowed answers.
73
+ #
74
+ # @param schema [Schema]
75
+ # @return [String] pretty-printed JSON
76
+ def schema_contract_json(schema)
77
+ JSON.pretty_generate(schema.to_h)
78
+ end
79
+
80
+ # Prompt instruction spelling out how value-constrained fields must be
81
+ # answered. Empty string when the schema has no constrained fields.
82
+ #
83
+ # @param schema [Schema]
84
+ # @return [String]
85
+ def values_instruction(schema)
86
+ constrained = schema.field_names.select { |name| schema.values_for(name) }
87
+ return "" if constrained.empty?
88
+
89
+ "\nFor fields that declare \"values\", you MUST answer using ONLY values from that list — " \
90
+ "return a single value for \"enum\" fields and an array of selected values for " \
91
+ "\"multi_enum\" fields. Never invent a value outside the list."
92
+ end
93
+
94
+ # One human-readable line per schema field, used in user prompts:
95
+ # income_types (multi_enum: W2 | business | crypto)
96
+ # dependents (integer)
97
+ #
98
+ # @param schema [Schema]
99
+ # @return [Array<String>]
100
+ def field_descriptions(schema)
101
+ schema.fields.map do |field, type|
102
+ values = schema.values_for(field)
103
+ values ? " #{field} (#{type}: #{values.join(" | ")})" : " #{field} (#{type})"
104
+ end
105
+ end
67
106
  end
68
107
  end
69
108
  end
@@ -86,9 +86,9 @@ module Inquirex
86
86
 
87
87
  def schema_instruction(node)
88
88
  if node.respond_to?(:schema) && node.schema
89
- schema_json = node.schema.fields.transform_values(&:to_s)
90
89
  "\n\nYou MUST respond with ONLY a valid JSON object matching this schema:\n" \
91
- "#{JSON.pretty_generate(schema_json)}\n\n" \
90
+ "#{schema_contract_json(node.schema)}\n" \
91
+ "#{values_instruction(node.schema)}\n\n" \
92
92
  "Do not include any text before or after the JSON. No markdown fences. Just the raw JSON object."
93
93
  else
94
94
  "\n\nRespond with a valid JSON object containing your analysis. " \
@@ -107,7 +107,7 @@ module Inquirex
107
107
 
108
108
  if node.respond_to?(:schema) && node.schema
109
109
  parts << "\nExtract these fields from the source data:"
110
- node.schema.fields.each { |field, type| parts << " #{field} (#{type})" }
110
+ parts.concat(field_descriptions(node.schema))
111
111
  end
112
112
 
113
113
  if node.respond_to?(:from_all) && node.from_all && all_answers.any?
@@ -4,12 +4,16 @@ module Inquirex
4
4
  module LLM
5
5
  module DSL
6
6
  # Mixin that adds LLM verb methods to Inquirex::DSL::FlowBuilder.
7
- # Included automatically when `require "inquirex-llm"` is called,
7
+ # Prepended automatically when `require "inquirex-llm"` is called,
8
8
  # so that `Inquirex.define` gains `extract` (and its `clarify` alias)
9
9
  # without needing a separate entry point.
10
10
  #
11
11
  # All core verbs (ask, say, header, btw, warning, confirm) remain
12
- # unchanged — LLM verbs are purely additive.
12
+ # unchanged — LLM verbs are purely additive. The mixin must be
13
+ # prepended (not included) because it overrides #build: LLM steps are
14
+ # built lazily at #build time, once every step in the flow is known,
15
+ # so that schema question references can resolve forward to questions
16
+ # defined after the LLM step.
13
17
  module FlowBuilderExtension
14
18
  # Defines an LLM extraction step: takes free-text input and produces
15
19
  # structured data matching the declared schema.
@@ -46,13 +50,28 @@ module Inquirex
46
50
  # add_llm_step(id, :detour, &)
47
51
  # end
48
52
 
53
+ # Builds any deferred LLM steps (now that the full node map exists),
54
+ # then produces the frozen Definition via the core builder.
55
+ def build
56
+ resolve_llm_steps!
57
+ super
58
+ end
59
+
49
60
  private
50
61
 
51
- # Uses the standard Ruby builder pattern (same as core FlowBuilder#add_step).
62
+ # Evaluates the step block immediately (same as core FlowBuilder#add_step)
63
+ # but parks the builder in the node map instead of building the node.
64
+ # The builder placeholder holds this step's position; #build replaces it.
52
65
  def add_llm_step(id, verb, &block)
53
66
  builder = LlmStepBuilder.new(verb)
54
67
  builder.instance_eval(&block) if block
55
- @nodes[id.to_sym] = builder.build(id)
68
+ @nodes[id.to_sym] = builder
69
+ end
70
+
71
+ def resolve_llm_steps!
72
+ @nodes.each do |id, entry|
73
+ @nodes[id] = entry.build(id, nodes: @nodes) if entry.is_a?(LlmStepBuilder)
74
+ end
56
75
  end
57
76
  end
58
77
  end
@@ -7,21 +7,30 @@ module Inquirex
7
7
  # methods (from, prompt, schema, model, temperature, max_tokens, fallback)
8
8
  # while inheriting transition and skip_if from the core StepBuilder.
9
9
  #
10
- # @example
10
+ # @example Referencing downstream questions (preferred)
11
11
  # extract :business_extracted do
12
12
  # from :business_description
13
13
  # prompt "Extract structured business info."
14
- # schema industry: :string, employee_count: :integer
14
+ # schema :entity_type, :employee_band # resolved from the ask steps
15
15
  # model :claude_sonnet
16
16
  # temperature 0.2
17
17
  # transition to: :next_step
18
18
  # end
19
+ #
20
+ # @example Explicit field types (for fields with no matching question)
21
+ # extract :business_extracted do
22
+ # from :business_description
23
+ # prompt "Extract structured business info."
24
+ # schema industry: :string, employee_count: :integer
25
+ # transition to: :next_step
26
+ # end
19
27
  class LlmStepBuilder
20
28
  include Inquirex::DSL::RuleHelpers
21
29
 
22
30
  def initialize(verb)
23
31
  @verb = verb.to_sym
24
32
  @prompt = nil
33
+ @schema_refs = []
25
34
  @schema_fields = {}
26
35
  @from_steps = []
27
36
  @from_all = false
@@ -38,16 +47,33 @@ module Inquirex
38
47
  # Sets the LLM prompt template. Use {{field_name}} for interpolation
39
48
  # placeholders that the adapter resolves at runtime.
40
49
  #
41
- # @param text [String]
50
+ # Pass `:auto` to generate the prompt from the schema's question
51
+ # references at build time: each referenced question's own wording is
52
+ # enumerated, so the LLM sees what was actually asked. Requires at
53
+ # least one question reference in the schema.
54
+ #
55
+ # @param text [String, :auto]
42
56
  def prompt(text)
43
57
  @prompt = text
44
58
  end
45
59
 
46
- # Declares the expected output schema. Each key is a field name,
47
- # each value is an Inquirex data type symbol.
60
+ # Declares the expected output schema.
61
+ #
62
+ # Positional symbols name questions defined elsewhere in the flow;
63
+ # each is resolved against that step's declared type, and for
64
+ # :enum / :multi_enum questions the allowed option values are folded
65
+ # into the schema sent to the LLM. A symbol that matches no ask/confirm
66
+ # step in the flow fails validation as invalid DSL.
67
+ #
68
+ # Keyword pairs declare explicit field => type mappings, for output
69
+ # fields that have no corresponding question. Both forms compose:
48
70
  #
49
- # @param fields [Hash{Symbol => Symbol}]
50
- def schema(**fields)
71
+ # schema :filing_status, :income_types, confidence: :decimal
72
+ #
73
+ # @param question_ids [Array<Symbol>] ids of questions to fill
74
+ # @param fields [Hash{Symbol => Symbol}] explicit field => type pairs
75
+ def schema(*question_ids, **fields)
76
+ @schema_refs.concat(question_ids.flatten.map(&:to_sym))
51
77
  @schema_fields.merge!(fields)
52
78
  end
53
79
 
@@ -126,20 +152,27 @@ module Inquirex
126
152
  @text = content
127
153
  end
128
154
 
129
- # Builds the LLM::Node.
155
+ # Builds the LLM::Node. Question references in the schema are resolved
156
+ # against the full node map, so the flow builder defers this call until
157
+ # every step — including ones defined after this one — is known.
130
158
  #
131
159
  # @param id [Symbol]
160
+ # @param nodes [Hash{Symbol => Inquirex::Node}, nil] all flow nodes,
161
+ # required when the schema references question ids
132
162
  # @return [LLM::Node]
133
- # @raise [Errors::DefinitionError] if prompt is missing
134
- def build(id)
163
+ # @raise [Errors::DefinitionError] if prompt is missing or a schema
164
+ # reference does not resolve to an ask/confirm question
165
+ def build(id, nodes: nil)
135
166
  validate!(id)
136
167
 
137
- schema_obj = @schema_fields.empty? ? nil : Schema.new(**@schema_fields)
168
+ field_map = resolve_schema_refs(id, nodes).merge(@schema_fields)
169
+ schema_obj = field_map.empty? ? nil : Schema.new(**field_map)
170
+ prompt_text = @prompt == :auto ? auto_prompt(nodes) : @prompt
138
171
 
139
172
  LLM::Node.new(
140
173
  id:,
141
174
  verb: @verb,
142
- prompt: @prompt,
175
+ prompt: prompt_text,
143
176
  schema: schema_obj,
144
177
  from_steps: @from_steps,
145
178
  from_all: @from_all,
@@ -156,11 +189,98 @@ module Inquirex
156
189
 
157
190
  private
158
191
 
192
+ # Turns question references into full field specs by looking up each
193
+ # referenced step in the flow: its declared type, and for enum-like
194
+ # types the exhaustive list of allowed option values.
195
+ #
196
+ # @param id [Symbol] this LLM step's id (for error messages)
197
+ # @param nodes [Hash{Symbol => Inquirex::Node}, nil]
198
+ # @return [Hash{Symbol => Hash}] field => { type:, values: } specs
199
+ def resolve_schema_refs(id, nodes)
200
+ return {} if @schema_refs.empty?
201
+
202
+ overlap = @schema_refs & @schema_fields.keys
203
+ unless overlap.empty?
204
+ raise Errors::DefinitionError,
205
+ "LLM step #{id.inspect} schema declares #{overlap.map(&:inspect).join(", ")} " \
206
+ "both as a question reference and an explicit field"
207
+ end
208
+
209
+ @schema_refs.uniq.to_h do |ref|
210
+ [ref, field_spec_for(id, ref, resolve_question!(id, ref, nodes))]
211
+ end
212
+ end
213
+
214
+ # Generates a prompt from the referenced questions' own wording.
215
+ # Runs at build time, after resolution, so the serialized node (and
216
+ # every adapter) sees a concrete prompt string — `:auto` never
217
+ # reaches the wire format.
218
+ #
219
+ # @param nodes [Hash{Symbol => Inquirex::Node}]
220
+ # @return [String]
221
+ def auto_prompt(nodes)
222
+ lines = @schema_refs.uniq.map do |ref|
223
+ question = nodes[ref].question
224
+ question ? "- #{ref}: #{question}" : "- #{ref}"
225
+ end
226
+ lines += @schema_fields.map { |name, type| "- #{name} (#{type})" }
227
+
228
+ "Extract structured answers from the user's text for the following questions:\n" \
229
+ "#{lines.join("\n")}\n" \
230
+ "Only extract what the text supports; leave a field empty or null when unsure."
231
+ end
232
+
233
+ # @return [Inquirex::Node] the ask/confirm step the reference names
234
+ def resolve_question!(id, ref, nodes)
235
+ node = nodes&.fetch(ref, nil)
236
+
237
+ case node
238
+ when nil
239
+ known = (nodes || {}).filter_map { |sid, n| sid if question_node?(n) }
240
+ raise Errors::DefinitionError,
241
+ "LLM step #{id.inspect} schema references unknown question #{ref.inspect}. " \
242
+ "Known questions: #{known.empty? ? "(none)" : known.map(&:inspect).join(", ")}"
243
+ when LLM::Node, LlmStepBuilder
244
+ raise Errors::DefinitionError,
245
+ "LLM step #{id.inspect} schema references #{ref.inspect}, which is an LLM step — " \
246
+ "schema references must name ask/confirm questions"
247
+ else
248
+ unless node.collecting?
249
+ raise Errors::DefinitionError,
250
+ "LLM step #{id.inspect} schema references #{ref.inspect}, which is a display-only " \
251
+ "#{node.verb} step — schema references must name ask/confirm questions"
252
+ end
253
+ node
254
+ end
255
+ end
256
+
257
+ def question_node?(node)
258
+ node.is_a?(Inquirex::Node) && !node.is_a?(LLM::Node) && node.collecting?
259
+ end
260
+
261
+ # @return [Hash] a Schema field spec derived from the question's definition
262
+ def field_spec_for(id, ref, node)
263
+ if node.type.nil?
264
+ raise Errors::DefinitionError,
265
+ "LLM step #{id.inspect} schema references #{ref.inspect}, which declares no type"
266
+ end
267
+
268
+ spec = { type: node.type }
269
+ spec[:values] = node.options if %i[enum multi_enum].include?(node.type) && node.options
270
+ spec
271
+ end
272
+
159
273
  def validate!(id)
160
274
  raise Errors::DefinitionError, "LLM step #{id.inspect} requires a prompt" if @prompt.nil?
161
275
 
276
+ if @prompt == :auto && @schema_refs.empty?
277
+ raise Errors::DefinitionError,
278
+ "LLM step #{id.inspect} declares `prompt :auto`, which requires schema question " \
279
+ "references to generate from — declare the schema as question ids (e.g. schema :filing_status)"
280
+ end
281
+
162
282
  # Schema required for extract (and formerly detour).
163
- if %i[extract].include?(@verb) && @schema_fields.empty?
283
+ if %i[extract].include?(@verb) && @schema_fields.empty? && @schema_refs.empty?
164
284
  # if %i[extract detour].include?(@verb) && @schema_fields.empty?
165
285
  raise Errors::DefinitionError,
166
286
  "LLM step #{id.inspect} (#{@verb}) requires a schema"
@@ -6,13 +6,15 @@ module Inquirex
6
6
  # calling any LLM API. Useful for testing flows that include LLM steps.
7
7
  #
8
8
  # For extract steps with a schema, returns a hash of default values
9
- # matching each field's declared type. Without a schema, returns
10
- # a placeholder string.
9
+ # matching each field's declared type. Fields constrained to a list of
10
+ # allowed values (enum/multi_enum resolved from questions) return the
11
+ # first allowed value, so placeholders stay valid answers for the
12
+ # downstream question. Without a schema, returns a placeholder string.
11
13
  #
12
14
  # @example
13
15
  # adapter = Inquirex::LLM::NullAdapter.new
14
16
  # result = adapter.call(extract_node, answers)
15
- # # => { industry: "", employee_count: 0, ... }
17
+ # # => { industry: "", employee_count: 0, entity_type: "llc", ... }
16
18
  class NullAdapter < Adapter
17
19
  TYPE_DEFAULTS = {
18
20
  string: "",
@@ -38,12 +40,21 @@ module Inquirex
38
40
  def call(node, _answers = {})
39
41
  if node.schema
40
42
  node.schema.fields.each_with_object({}) do |(name, type), acc|
41
- acc[name] = TYPE_DEFAULTS.fetch(type, "")
43
+ acc[name] = placeholder_for(node.schema, name, type)
42
44
  end
43
45
  else
44
46
  "(placeholder #{node.verb} output for #{node.id})"
45
47
  end
46
48
  end
49
+
50
+ private
51
+
52
+ def placeholder_for(schema, name, type)
53
+ values = schema.values_for(name)
54
+ return TYPE_DEFAULTS.fetch(type, "") if values.nil? || values.empty?
55
+
56
+ type == :multi_enum ? [values.first] : values.first
57
+ end
47
58
  end
48
59
  end
49
60
  end
@@ -88,9 +88,9 @@ module Inquirex
88
88
 
89
89
  def schema_instruction(node)
90
90
  if node.respond_to?(:schema) && node.schema
91
- schema_json = node.schema.fields.transform_values(&:to_s)
92
91
  "\n\nThe JSON object MUST match this schema exactly (same keys, appropriate types):\n" \
93
- "#{JSON.pretty_generate(schema_json)}\n\n" \
92
+ "#{schema_contract_json(node.schema)}\n" \
93
+ "#{values_instruction(node.schema)}\n\n" \
94
94
  "Every key in the schema must be present in your output. Use null, \"\", 0, or [] " \
95
95
  "for values the source text does not provide."
96
96
  else
@@ -109,7 +109,7 @@ module Inquirex
109
109
 
110
110
  if node.respond_to?(:schema) && node.schema
111
111
  parts << "\nExtract these fields as a JSON object:"
112
- node.schema.fields.each { |field, type| parts << " #{field} (#{type})" }
112
+ parts.concat(field_descriptions(node.schema))
113
113
  end
114
114
 
115
115
  if node.respond_to?(:from_all) && node.from_all && all_answers.any?
@@ -3,21 +3,25 @@
3
3
  module Inquirex
4
4
  module LLM
5
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.
6
+ # Each field maps a name to a spec: an Inquirex data type, plus for
7
+ # :enum / :multi_enum fields the exhaustive list of allowed values.
8
+ # Together they form the contract between the LLM prompt and the
9
+ # structured data it must return.
10
+ #
11
+ # A field spec is given either as a bare type symbol or as a Hash with
12
+ # :type and optional :values:
8
13
  #
9
14
  # @example
10
15
  # schema = Schema.new(
11
- # industry: :string,
12
- # entity_type: :enum,
13
- # employee_count: :integer,
14
- # estimated_revenue: :currency
16
+ # industry: :string,
17
+ # employee_count: :integer,
18
+ # entity_type: { type: :enum, values: %w[llc s_corp c_corp] }
15
19
  # )
16
- # schema.fields # => { industry: :string, ... }
17
- # schema.field_names # => [:industry, :entity_type, ...]
20
+ # schema.fields # => { industry: :string, ... }
21
+ # schema.values_for(:entity_type) # => ["llc", "s_corp", "c_corp"]
18
22
  # schema.valid_output?({ industry: "Tech", ... }) # => true
19
23
  #
20
- # @attr_reader fields [Hash{Symbol => Symbol}] field_name => type mapping
24
+ # @attr_reader field_specs [Hash{Symbol => Field}] field_name => spec mapping
21
25
  class Schema
22
26
  VALID_TYPES = %i[
23
27
  string text integer decimal currency boolean
@@ -25,25 +29,39 @@ module Inquirex
25
29
  array hash
26
30
  ].freeze
27
31
 
28
- attr_reader :fields
32
+ # A single field's contract: its data type and, for enum-like types,
33
+ # the allowed values the LLM must choose from.
34
+ Field = Data.define(:type, :values)
29
35
 
30
- # @param field_map [Hash{Symbol => Symbol}] field_name => type
36
+ attr_reader :field_specs
37
+
38
+ # @param field_map [Hash{Symbol => Symbol, Hash}] field_name => type,
39
+ # or field_name => { type: Symbol, values: Array }
31
40
  # @raise [Errors::DefinitionError] if any type is unrecognized
32
41
  def initialize(**field_map)
33
42
  raise Errors::DefinitionError, "Schema must have at least one field" if field_map.empty?
34
43
 
35
- validate_types!(field_map)
36
- @fields = field_map.transform_keys(&:to_sym)
37
- .transform_values(&:to_sym)
38
- .freeze
44
+ @field_specs = field_map.to_h { |name, spec| [name.to_sym, build_field(name, spec)] }.freeze
45
+ @fields = @field_specs.transform_values(&:type).freeze
39
46
  freeze
40
47
  end
41
48
 
49
+ # @return [Hash{Symbol => Symbol}] field_name => type mapping
50
+ attr_reader :fields
51
+
42
52
  # @return [Array<Symbol>] ordered list of field names
43
- def field_names = @fields.keys
53
+ def field_names = @field_specs.keys
44
54
 
45
55
  # @return [Integer] number of fields
46
- def size = @fields.size
56
+ def size = @field_specs.size
57
+
58
+ # Allowed values for an enum/multi_enum field, or nil when unconstrained.
59
+ #
60
+ # @param name [Symbol, String]
61
+ # @return [Array<String>, nil]
62
+ def values_for(name)
63
+ @field_specs[name.to_sym]&.values
64
+ end
47
65
 
48
66
  # Checks whether a Hash output conforms to the schema (all declared fields present).
49
67
  #
@@ -53,7 +71,7 @@ module Inquirex
53
71
  return false unless output.is_a?(Hash)
54
72
 
55
73
  symbolized = output.transform_keys(&:to_sym)
56
- @fields.keys.all? { |key| symbolized.key?(key) }
74
+ @field_specs.keys.all? { |key| symbolized.key?(key) }
57
75
  end
58
76
 
59
77
  # Returns the list of fields missing from the given output.
@@ -64,12 +82,23 @@ module Inquirex
64
82
  return field_names unless output.is_a?(Hash)
65
83
 
66
84
  symbolized = output.transform_keys(&:to_sym)
67
- @fields.keys.reject { |key| symbolized.key?(key) }
85
+ @field_specs.keys.reject { |key| symbolized.key?(key) }
68
86
  end
69
87
 
88
+ # Wire format. Fields without value constraints serialize as a plain
89
+ # type string (the pre-0.6 shape); constrained fields serialize as
90
+ # { "type" => ..., "values" => [...] }.
91
+ #
70
92
  # @return [Hash]
71
93
  def to_h
72
- @fields.transform_keys(&:to_s).transform_values(&:to_s)
94
+ @field_specs.each_with_object({}) do |(name, field), acc|
95
+ acc[name.to_s] =
96
+ if field.values
97
+ { "type" => field.type.to_s, "values" => field.values }
98
+ else
99
+ field.type.to_s
100
+ end
101
+ end
73
102
  end
74
103
 
75
104
  # @return [String] JSON representation
@@ -77,34 +106,52 @@ module Inquirex
77
106
  JSON.generate(to_h)
78
107
  end
79
108
 
80
- # @param hash [Hash] string or symbol keys, values are type names
109
+ # Accepts both wire shapes: plain type strings and rich
110
+ # { "type" => ..., "values" => [...] } specs.
111
+ #
112
+ # @param hash [Hash] string or symbol keys
81
113
  # @return [Schema]
82
114
  def self.from_h(hash)
83
115
  field_map = hash.each_with_object({}) do |(k, v), acc|
84
- acc[k.to_sym] = v.to_sym
116
+ acc[k.to_sym] = v
85
117
  end
86
118
  new(**field_map)
87
119
  end
88
120
 
89
121
  def ==(other)
90
- other.is_a?(Schema) && @fields == other.fields
122
+ other.is_a?(Schema) && @field_specs == other.field_specs
91
123
  end
92
124
 
93
125
  def inspect
94
- "#<Inquirex::LLM::Schema #{@fields.map { |k, v| "#{k}:#{v}" }.join(", ")}>"
126
+ parts = @field_specs.map do |name, field|
127
+ field.values ? "#{name}:#{field.type}(#{field.values.join("|")})" : "#{name}:#{field.type}"
128
+ end
129
+ "#<Inquirex::LLM::Schema #{parts.join(", ")}>"
95
130
  end
96
131
 
97
132
  private
98
133
 
99
- def validate_types!(field_map)
100
- field_map.each do |name, type|
101
- next if VALID_TYPES.include?(type.to_sym)
134
+ def build_field(name, spec)
135
+ case spec
136
+ when Hash
137
+ type = spec[:type] || spec["type"]
138
+ values = spec[:values] || spec["values"]
139
+ raise Errors::DefinitionError, "Field #{name.inspect} spec is missing :type" if type.nil?
102
140
 
103
- raise Errors::DefinitionError,
104
- "Unknown type #{type.inspect} for field #{name.inspect}. " \
105
- "Valid types: #{VALID_TYPES.join(", ")}"
141
+ Field.new(type: validated_type(name, type), values: values&.map(&:to_s)&.freeze)
142
+ else
143
+ Field.new(type: validated_type(name, spec), values: nil)
106
144
  end
107
145
  end
146
+
147
+ def validated_type(name, type)
148
+ sym = type.to_sym
149
+ return sym if VALID_TYPES.include?(sym)
150
+
151
+ raise Errors::DefinitionError,
152
+ "Unknown type #{type.inspect} for field #{name.inspect}. " \
153
+ "Valid types: #{VALID_TYPES.join(", ")}"
154
+ end
108
155
  end
109
156
  end
110
157
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Inquirex
4
4
  module LLM
5
- VERSION = "0.5.0"
5
+ VERSION = "0.6.0"
6
6
  end
7
7
  end
data/lib/inquirex/llm.rb CHANGED
@@ -42,4 +42,7 @@ end
42
42
 
43
43
  # Inject LLM verbs into the core FlowBuilder so that Inquirex.define
44
44
  # gains extract (and clarify as an alias) when this gem is loaded.
45
- Inquirex::DSL::FlowBuilder.include(Inquirex::LLM::DSL::FlowBuilderExtension)
45
+ # Prepend (not include): the extension overrides #build to defer LLM node
46
+ # construction until the whole flow is known, so schema question references
47
+ # can resolve forward to questions defined after the LLM step.
48
+ Inquirex::DSL::FlowBuilder.prepend(Inquirex::LLM::DSL::FlowBuilderExtension)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: inquirex-llm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Konstantin Gredeskoul
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: '0.4'
18
+ version: '0.6'
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
- version: '0.4'
25
+ version: '0.6'
26
26
  description: 'Extends the Inquirex DSL with an LLM-powered `extract` verb (alias:
27
27
  `clarify`) that runs server-side to turn free-text answers into structured data.
28
28
  Pluggable adapter interface keeps the gem LLM-agnostic; a NullAdapter ships for
@@ -34,17 +34,10 @@ executables:
34
34
  extensions: []
35
35
  extra_rdoc_files: []
36
36
  files:
37
- - ".relaxed_rubocop.yml"
38
- - ".rubocop_todo.yml"
39
- - ".secrets.baseline"
40
37
  - CHANGELOG.md
41
38
  - LICENSE.txt
42
39
  - README.md
43
- - Rakefile
44
- - docs/badges/coverage_badge.svg
45
40
  - exe/inquirex-llm
46
- - justfile
47
- - lefthook.yml
48
41
  - lib/inquirex-llm.rb
49
42
  - lib/inquirex/llm.rb
50
43
  - lib/inquirex/llm/adapter.rb
@@ -57,7 +50,6 @@ files:
57
50
  - lib/inquirex/llm/openai_adapter.rb
58
51
  - lib/inquirex/llm/schema.rb
59
52
  - lib/inquirex/llm/version.rb
60
- - sig/inquirex/llm.rbs
61
53
  homepage: https://github.com/inquirex/inquirex-llm
62
54
  licenses:
63
55
  - MIT
data/.relaxed_rubocop.yml DELETED
@@ -1,153 +0,0 @@
1
- # Relaxed.Ruby.Style
2
- ## Version 2.5
3
-
4
- Style/Alias:
5
- Enabled: false
6
- StyleGuide: https://relaxed.ruby.style/#stylealias
7
-
8
- Style/AsciiComments:
9
- Enabled: false
10
- StyleGuide: https://relaxed.ruby.style/#styleasciicomments
11
-
12
- Style/BeginBlock:
13
- Enabled: false
14
- StyleGuide: https://relaxed.ruby.style/#stylebeginblock
15
-
16
- Style/BlockDelimiters:
17
- Enabled: false
18
- StyleGuide: https://relaxed.ruby.style/#styleblockdelimiters
19
-
20
- Style/CommentAnnotation:
21
- Enabled: false
22
- StyleGuide: https://relaxed.ruby.style/#stylecommentannotation
23
-
24
- Style/Documentation:
25
- Enabled: false
26
- StyleGuide: https://relaxed.ruby.style/#styledocumentation
27
-
28
- Layout/DotPosition:
29
- Enabled: false
30
- StyleGuide: https://relaxed.ruby.style/#layoutdotposition
31
-
32
- Style/DoubleNegation:
33
- Enabled: false
34
- StyleGuide: https://relaxed.ruby.style/#styledoublenegation
35
-
36
- Style/EndBlock:
37
- Enabled: false
38
- StyleGuide: https://relaxed.ruby.style/#styleendblock
39
-
40
- Style/FormatString:
41
- Enabled: false
42
- StyleGuide: https://relaxed.ruby.style/#styleformatstring
43
-
44
- Style/IfUnlessModifier:
45
- Enabled: false
46
- StyleGuide: https://relaxed.ruby.style/#styleifunlessmodifier
47
-
48
- Style/Lambda:
49
- Enabled: false
50
- StyleGuide: https://relaxed.ruby.style/#stylelambda
51
-
52
- Style/ModuleFunction:
53
- Enabled: false
54
- StyleGuide: https://relaxed.ruby.style/#stylemodulefunction
55
-
56
- Style/MultilineBlockChain:
57
- Enabled: false
58
- StyleGuide: https://relaxed.ruby.style/#stylemultilineblockchain
59
-
60
- Style/NegatedIf:
61
- Enabled: false
62
- StyleGuide: https://relaxed.ruby.style/#stylenegatedif
63
-
64
- Style/NegatedWhile:
65
- Enabled: false
66
- StyleGuide: https://relaxed.ruby.style/#stylenegatedwhile
67
-
68
- Style/NumericPredicate:
69
- Enabled: false
70
- StyleGuide: https://relaxed.ruby.style/#stylenumericpredicate
71
-
72
- Style/ParallelAssignment:
73
- Enabled: false
74
- StyleGuide: https://relaxed.ruby.style/#styleparallelassignment
75
-
76
- Style/PercentLiteralDelimiters:
77
- Enabled: false
78
- StyleGuide: https://relaxed.ruby.style/#stylepercentliteraldelimiters
79
-
80
- Style/PerlBackrefs:
81
- Enabled: false
82
- StyleGuide: https://relaxed.ruby.style/#styleperlbackrefs
83
-
84
- Style/Semicolon:
85
- Enabled: false
86
- StyleGuide: https://relaxed.ruby.style/#stylesemicolon
87
-
88
- Style/SignalException:
89
- Enabled: false
90
- StyleGuide: https://relaxed.ruby.style/#stylesignalexception
91
-
92
- Style/SingleLineBlockParams:
93
- Enabled: false
94
- StyleGuide: https://relaxed.ruby.style/#stylesinglelineblockparams
95
-
96
- Style/SingleLineMethods:
97
- Enabled: false
98
- StyleGuide: https://relaxed.ruby.style/#stylesinglelinemethods
99
-
100
- Layout/SpaceBeforeBlockBraces:
101
- Enabled: false
102
- StyleGuide: https://relaxed.ruby.style/#layoutspacebeforeblockbraces
103
-
104
- Layout/SpaceInsideParens:
105
- Enabled: false
106
- StyleGuide: https://relaxed.ruby.style/#layoutspaceinsideparens
107
-
108
- Style/SpecialGlobalVars:
109
- Enabled: false
110
- StyleGuide: https://relaxed.ruby.style/#stylespecialglobalvars
111
-
112
- Style/StringLiterals:
113
- Enabled: false
114
- StyleGuide: https://relaxed.ruby.style/#stylestringliterals
115
-
116
- Style/TrailingCommaInArguments:
117
- Enabled: false
118
- StyleGuide: https://relaxed.ruby.style/#styletrailingcommainarguments
119
-
120
- Style/TrailingCommaInArrayLiteral:
121
- Enabled: false
122
- StyleGuide: https://relaxed.ruby.style/#styletrailingcommainarrayliteral
123
-
124
- Style/TrailingCommaInHashLiteral:
125
- Enabled: false
126
- StyleGuide: https://relaxed.ruby.style/#styletrailingcommainhashliteral
127
-
128
- Style/SymbolArray:
129
- Enabled: false
130
- StyleGuide: http://relaxed.ruby.style/#stylesymbolarray
131
-
132
- Style/WhileUntilModifier:
133
- Enabled: false
134
- StyleGuide: https://relaxed.ruby.style/#stylewhileuntilmodifier
135
-
136
- Style/WordArray:
137
- Enabled: false
138
- StyleGuide: https://relaxed.ruby.style/#stylewordarray
139
-
140
- Lint/AmbiguousRegexpLiteral:
141
- Enabled: false
142
- StyleGuide: https://relaxed.ruby.style/#lintambiguousregexpliteral
143
-
144
- Lint/AssignmentInCondition:
145
- Enabled: false
146
- StyleGuide: https://relaxed.ruby.style/#lintassignmentincondition
147
-
148
- Layout/LineLength:
149
- Enabled: false
150
-
151
- Metrics:
152
- Enabled: false
153
-
data/.rubocop_todo.yml DELETED
@@ -1,28 +0,0 @@
1
- # This configuration was generated by
2
- # `rubocop --auto-gen-config`
3
- # on 2026-04-14 23:51:42 UTC using RuboCop version 1.86.1.
4
- # The point is for the user to remove these configuration records
5
- # one by one as the offenses are removed from the code base.
6
- # Note that changes in the inspected code, or installation of new
7
- # versions of RuboCop, may require this file to be generated again.
8
-
9
- # Offense count: 1
10
- # Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers, AllowedPatterns.
11
- # SupportedStyles: snake_case, normalcase, non_integer
12
- # AllowedIdentifiers: TLS1_1, TLS1_2, capture3, iso8601, rfc1123_date, rfc822, rfc2822, rfc3339, x86_64
13
- Naming/VariableNumber:
14
- Exclude:
15
- - 'lib/inquirex/llm/openai_adapter.rb'
16
-
17
- # Offense count: 2
18
- # Configuration parameters: AllowSubject.
19
- RSpec/MultipleMemoizedHelpers:
20
- Max: 6
21
-
22
- # Offense count: 1
23
- # Configuration parameters: CustomTransform, IgnoreMethods, IgnoreMetadata, InflectorPath, EnforcedInflector.
24
- # SupportedInflectors: default, active_support
25
- RSpec/SpecFilePathFormat:
26
- Exclude:
27
- - '**/spec/routing/**/*'
28
- - 'spec/inquirex/llm/openai_adapter_spec.rb'
data/.secrets.baseline DELETED
@@ -1,127 +0,0 @@
1
- {
2
- "version": "1.5.0",
3
- "plugins_used": [
4
- {
5
- "name": "ArtifactoryDetector"
6
- },
7
- {
8
- "name": "AWSKeyDetector"
9
- },
10
- {
11
- "name": "AzureStorageKeyDetector"
12
- },
13
- {
14
- "name": "Base64HighEntropyString",
15
- "limit": 4.5
16
- },
17
- {
18
- "name": "BasicAuthDetector"
19
- },
20
- {
21
- "name": "CloudantDetector"
22
- },
23
- {
24
- "name": "DiscordBotTokenDetector"
25
- },
26
- {
27
- "name": "GitHubTokenDetector"
28
- },
29
- {
30
- "name": "GitLabTokenDetector"
31
- },
32
- {
33
- "name": "HexHighEntropyString",
34
- "limit": 3.0
35
- },
36
- {
37
- "name": "IbmCloudIamDetector"
38
- },
39
- {
40
- "name": "IbmCosHmacDetector"
41
- },
42
- {
43
- "name": "IPPublicDetector"
44
- },
45
- {
46
- "name": "JwtTokenDetector"
47
- },
48
- {
49
- "name": "KeywordDetector",
50
- "keyword_exclude": ""
51
- },
52
- {
53
- "name": "MailchimpDetector"
54
- },
55
- {
56
- "name": "NpmDetector"
57
- },
58
- {
59
- "name": "OpenAIDetector"
60
- },
61
- {
62
- "name": "PrivateKeyDetector"
63
- },
64
- {
65
- "name": "PypiTokenDetector"
66
- },
67
- {
68
- "name": "SendGridDetector"
69
- },
70
- {
71
- "name": "SlackDetector"
72
- },
73
- {
74
- "name": "SoftlayerDetector"
75
- },
76
- {
77
- "name": "SquareOAuthDetector"
78
- },
79
- {
80
- "name": "StripeDetector"
81
- },
82
- {
83
- "name": "TelegramBotTokenDetector"
84
- },
85
- {
86
- "name": "TwilioKeyDetector"
87
- }
88
- ],
89
- "filters_used": [
90
- {
91
- "path": "detect_secrets.filters.allowlist.is_line_allowlisted"
92
- },
93
- {
94
- "path": "detect_secrets.filters.common.is_ignored_due_to_verification_policies",
95
- "min_level": 2
96
- },
97
- {
98
- "path": "detect_secrets.filters.heuristic.is_indirect_reference"
99
- },
100
- {
101
- "path": "detect_secrets.filters.heuristic.is_likely_id_string"
102
- },
103
- {
104
- "path": "detect_secrets.filters.heuristic.is_lock_file"
105
- },
106
- {
107
- "path": "detect_secrets.filters.heuristic.is_not_alphanumeric_string"
108
- },
109
- {
110
- "path": "detect_secrets.filters.heuristic.is_potential_uuid"
111
- },
112
- {
113
- "path": "detect_secrets.filters.heuristic.is_prefixed_with_dollar_sign"
114
- },
115
- {
116
- "path": "detect_secrets.filters.heuristic.is_sequential_string"
117
- },
118
- {
119
- "path": "detect_secrets.filters.heuristic.is_swagger_file"
120
- },
121
- {
122
- "path": "detect_secrets.filters.heuristic.is_templated_secret"
123
- }
124
- ],
125
- "results": {},
126
- "generated_at": "2026-04-13T21:29:03Z"
127
- }
data/Rakefile DELETED
@@ -1,36 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "bundler/gem_tasks"
4
- require "rspec/core/rake_task"
5
- require "timeout"
6
- require "yard"
7
-
8
- def shell(*args)
9
- puts "running: #{args.join(" ")}"
10
- system(args.join(" "))
11
- end
12
-
13
- task :clean do
14
- shell("rm -rf pkg/ tmp/ coverage/ doc/ ")
15
- end
16
-
17
- task gem: [:build] do
18
- shell("gem install pkg/*")
19
- end
20
-
21
- task permissions: [:clean] do
22
- shell("chmod -v o+r,g+r * */* */*/* */*/*/* */*/*/*/* */*/*/*/*/*")
23
- shell("find . -type d -exec chmod o+x,g+x {} \\;")
24
- end
25
-
26
- task build: :permissions
27
-
28
- YARD::Rake::YardocTask.new(:doc) do |t|
29
- t.files = %w[lib/**/*.rb exe/*.rb - README.md LICENSE.txt CHANGELOG.md]
30
- t.options.unshift("--title", '"FlowEngine — DSL + AST for buildiong complex flows in Ruby."')
31
- t.after = -> { exec("open doc/index.html") } if RUBY_PLATFORM =~ /darwin/
32
- end
33
-
34
- RSpec::Core::RakeTask.new(:spec)
35
-
36
- task default: :spec
@@ -1,21 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <svg xmlns="http://www.w3.org/2000/svg" width="99" height="20">
3
- <linearGradient id="b" x2="0" y2="100%">
4
- <stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
5
- <stop offset="1" stop-opacity=".1"/>
6
- </linearGradient>
7
- <mask id="a">
8
- <rect width="99" height="20" rx="3" fill="#fff"/>
9
- </mask>
10
- <g mask="url(#a)">
11
- <path fill="#555" d="M0 0h63v20H0z"/>
12
- <path fill="#4c1" d="M63 0h36v20H63z"/>
13
- <path fill="url(#b)" d="M0 0h99v20H0z"/>
14
- </g>
15
- <g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">
16
- <text x="31.5" y="15" fill="#010101" fill-opacity=".3">coverage</text>
17
- <text x="31.5" y="14">coverage</text>
18
- <text x="80" y="15" fill="#010101" fill-opacity=".3">98%</text>
19
- <text x="80" y="14">98%</text>
20
- </g>
21
- </svg>
data/justfile DELETED
@@ -1,69 +0,0 @@
1
- # Tell 'just' to run bash, source our setup script, then execute the recipe
2
- set shell := ["bash", "-c"]
3
-
4
- version := `grep VERSION lib/inquirex/llm/version.rb | awk '{print $3}' | tr -d '"' | tr -d '\n'`
5
- rbenv := 'eval "$(rbenv init bash)"; bundle exec '
6
- repo := 'git@github.com:inquirex/inquirex.git'
7
-
8
- [no-exit-message]
9
- recipes:
10
- just --choose
11
-
12
- # Sync all dependencies
13
- install:
14
- bin/setup
15
-
16
- build: install
17
-
18
- # Lint and reformat files
19
- lint:
20
- {{ rbenv }} rubocop
21
-
22
- # Lint and reformat files (-a) — pass -A as an argument
23
- format *args:
24
- {{ rbenv }} rubocop -a {{ args }}
25
-
26
- # Run all the tests
27
- test *args:
28
- export ENVIRONMENT=test; {{ rbenv }} rspec {{args}}
29
-
30
- # Run tests with coverage
31
- test-coverage *args:
32
- export ENVIRONMENT=test; export COVERAGE=true; {{ rbenv }} rspec {{ args }}
33
-
34
- check-all: install lint test-coverage
35
-
36
- clean:
37
- #!/usr/bin/env bash
38
- @find . -name .DS_Store -delete -print || true
39
- @rm -rf tmp/*
40
-
41
- # Run all lefthook pre-commit hooks
42
- lefthook:
43
- {{ rbenv }} lefthook run pre-commit --all-files
44
-
45
- # Print current gem version
46
- version:
47
- @echo "{{ version }}"
48
-
49
- # Clobber
50
- clobber:
51
- {{ rbenv }} rake clobber
52
-
53
- # Generate documentation
54
- doc:
55
- #!/usr/bin/env bash
56
- {{ rbenv }} rake doc
57
-
58
- # Create
59
- publish: build
60
- {{ rbenv }} rake release[remote]
61
-
62
-
63
- # Tag v{{ version }}, publish the GH release, & refresh the Homebrew tap.
64
- release:
65
- git fetch --tags
66
- git tag -f "v{{ version }}"
67
- git push -f --tags
68
- gh release delete -y "v{{ version }}" --repo {{ repo }} 2>/dev/null || true
69
- gh release create "v{{ version }}" --generate-notes --repo {{ repo }}
data/lefthook.yml DELETED
@@ -1,35 +0,0 @@
1
- output:
2
- - summary
3
- - failure
4
-
5
- pre-commit:
6
- parallel: true
7
- jobs:
8
- - name: lint
9
- run: bundle exec rubocop -c .rubocop.yml {staged_files}
10
- glob: "*.{rb,Gemfile}"
11
- stage_fixed: true
12
-
13
- - name: check for conflict markers and whitespace issues
14
- run: git --no-pager diff --check
15
-
16
- # If tests take >1 second, move this (or just the long-running tests) to pre-push.
17
- - name: run tests
18
- run: just test
19
-
20
- - name: fix rubocop formatting issues
21
- run: bundle exec rubocop -a {staged_files}
22
- glob: "*.{rb,Gemfile,gemspec}"
23
- stage_fixed: true
24
-
25
- - name: spell check
26
- run: codespell {staged_files}
27
- glob: "*.{rb,md,gemspec}"
28
-
29
- - name: format markdown
30
- run: mdformat {staged_files}
31
- glob: "*.md"
32
- stage_fixed: true
33
-
34
- - name: scan for secrets
35
- run: detect-secrets-hook --baseline .secrets.baseline
data/sig/inquirex/llm.rbs DELETED
@@ -1,6 +0,0 @@
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