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 +4 -4
- data/README.md +91 -15
- data/lib/inquirex/llm/adapter.rb +39 -0
- data/lib/inquirex/llm/anthropic_adapter.rb +3 -3
- data/lib/inquirex/llm/dsl/flow_builder.rb +23 -4
- data/lib/inquirex/llm/dsl/llm_step_builder.rb +133 -13
- data/lib/inquirex/llm/null_adapter.rb +15 -4
- data/lib/inquirex/llm/openai_adapter.rb +3 -3
- data/lib/inquirex/llm/schema.rb +77 -30
- data/lib/inquirex/llm/version.rb +1 -1
- data/lib/inquirex/llm.rb +4 -1
- metadata +3 -11
- data/.relaxed_rubocop.yml +0 -153
- data/.rubocop_todo.yml +0 -28
- data/.secrets.baseline +0 -127
- data/Rakefile +0 -36
- data/docs/badges/coverage_badge.svg +0 -21
- data/justfile +0 -69
- data/lefthook.yml +0 -35
- data/sig/inquirex/llm.rbs +0 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 00b1d649377bb6ff1281a8fd3af730995b86531e44c930cb3c9a267b73e5efe0
|
|
4
|
+
data.tar.gz: '0478c2970b4d501ee3b9b2ed4dcdd2d620fbc99c522ac863e3ac772a4f75dce6'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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
|
|
84
|
-
|
|
|
85
|
-
| `prompt "..."`
|
|
86
|
-
| `schema
|
|
87
|
-
| `
|
|
88
|
-
| `
|
|
89
|
-
| `
|
|
90
|
-
| `
|
|
91
|
-
| `
|
|
92
|
-
| `
|
|
93
|
-
| `
|
|
94
|
-
| `
|
|
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
|
-
|
|
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
|
|
data/lib/inquirex/llm/adapter.rb
CHANGED
|
@@ -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
|
-
"#{
|
|
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
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
|
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
|
|
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
|
-
#
|
|
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.
|
|
47
|
-
#
|
|
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
|
-
#
|
|
50
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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.
|
|
10
|
-
#
|
|
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] =
|
|
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
|
-
"#{
|
|
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
|
|
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?
|
data/lib/inquirex/llm/schema.rb
CHANGED
|
@@ -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,
|
|
7
|
-
#
|
|
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:
|
|
12
|
-
#
|
|
13
|
-
#
|
|
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
|
|
17
|
-
# schema.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
36
|
-
@fields =
|
|
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 = @
|
|
53
|
+
def field_names = @field_specs.keys
|
|
44
54
|
|
|
45
55
|
# @return [Integer] number of fields
|
|
46
|
-
def 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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
#
|
|
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
|
|
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) && @
|
|
122
|
+
other.is_a?(Schema) && @field_specs == other.field_specs
|
|
91
123
|
end
|
|
92
124
|
|
|
93
125
|
def inspect
|
|
94
|
-
|
|
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
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
data/lib/inquirex/llm/version.rb
CHANGED
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|