inquirex-tty 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.envrc +3 -0
- data/.relaxed_rubocop.yml +153 -0
- data/.secrets.baseline +127 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +530 -0
- data/Rakefile +36 -0
- data/docs/badges/coverage_badge.svg +21 -0
- data/examples/01_hello_world.rb +32 -0
- data/examples/02_yes_or_no.rb +33 -0
- data/examples/03_food_preferences.png +0 -0
- data/examples/03_food_preferences.rb +65 -0
- data/examples/04_event_registration.rb +86 -0
- data/examples/05_job_application.rb +122 -0
- data/examples/06_health_assessment.rb +173 -0
- data/examples/07_loan_application.rb +217 -0
- data/examples/08_tax_preparer.rb +195 -0
- data/examples/09_tax_preparer_llm.rb +151 -0
- data/examples/10_answers.json +53 -0
- data/examples/10_real_tax_preparer.rb +604 -0
- data/examples/README.md +50 -0
- data/exe/inquirex +24 -0
- data/justfile +56 -0
- data/lefthook.yml +36 -0
- data/lib/inquirex/tty/commands/export.rb +81 -0
- data/lib/inquirex/tty/commands/graph.rb +116 -0
- data/lib/inquirex/tty/commands/run.rb +193 -0
- data/lib/inquirex/tty/commands/validate.rb +108 -0
- data/lib/inquirex/tty/commands/version.rb +19 -0
- data/lib/inquirex/tty/commands.rb +19 -0
- data/lib/inquirex/tty/flow_loader.rb +41 -0
- data/lib/inquirex/tty/output_path.rb +66 -0
- data/lib/inquirex/tty/renderer.rb +245 -0
- data/lib/inquirex/tty/ui_helper.rb +58 -0
- data/lib/inquirex/tty/version.rb +7 -0
- data/lib/inquirex/tty.rb +41 -0
- data/sig/inquirex/tty.rbs +6 -0
- metadata +197 -0
data/README.md
ADDED
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
[](https://github.com/inquirex/inquirex-tty/actions/workflows/main.yml) 
|
|
2
|
+
|
|
3
|
+
# inquirex-tty
|
|
4
|
+
|
|
5
|
+
Terminal adapter for the [Inquirex](https://github.com/inquirex/inquirex) questionnaire engine. Renders flow definitions as interactive ANSI terminal wizards using [tty-prompt](https://github.com/piotrmurach/tty-prompt), with ASCII-art headers, styled boxes, and automatic widget selection based on data types.
|
|
6
|
+
|
|
7
|
+
Ships as a CLI (`inquirex`) with commands to run flows interactively, validate definitions, and export Mermaid diagrams.
|
|
8
|
+
|
|
9
|
+
## Status
|
|
10
|
+
|
|
11
|
+
- Version: `0.2.1`
|
|
12
|
+
- Ruby: `>= 4.0.0`
|
|
13
|
+
- Depends on: `inquirex`, `tty-prompt`, `tty-box`, `tty-font`, `pastel`, `dry-cli`
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
gem "inquirex-tty"
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The gem installs an `inquirex` executable.
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
Write a flow definition in Ruby:
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
# my_flow.rb
|
|
29
|
+
require "inquirex"
|
|
30
|
+
|
|
31
|
+
Inquirex.define id: "hello", version: "1.0.0" do
|
|
32
|
+
meta title: "Hello World"
|
|
33
|
+
start :name
|
|
34
|
+
|
|
35
|
+
ask :name do
|
|
36
|
+
type :string
|
|
37
|
+
question "What is your name?"
|
|
38
|
+
transition to: :age
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
ask :age do
|
|
42
|
+
type :integer
|
|
43
|
+
question "How old are you?"
|
|
44
|
+
transition to: :farewell
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
say :farewell do
|
|
48
|
+
text "Thanks for chatting!"
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Run it:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
inquirex run my_flow.rb
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The CLI walks the user through each step, selecting the appropriate TTY widget for each data type, and prints collected answers as JSON when the flow completes.
|
|
60
|
+
|
|
61
|
+
## CLI Commands
|
|
62
|
+
|
|
63
|
+
### `inquirex run <flow_file>`
|
|
64
|
+
|
|
65
|
+
Execute a flow interactively. Each step is rendered with the appropriate tty-prompt widget based on the node's data type and widget hints.
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
# run it and then dump answers as json to stdout
|
|
69
|
+
inquirex run examples/08_tax_preparer.rb
|
|
70
|
+
|
|
71
|
+
# run it and save the anawers to a json file
|
|
72
|
+
inquirex run examples/08_tax_preparer.rb \
|
|
73
|
+
--output answers.json
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Options:
|
|
77
|
+
|
|
78
|
+
| Flag | Description |
|
|
79
|
+
|------|-------------|
|
|
80
|
+
| `--output`, `-o` | Write JSON results to a file instead of stderr |
|
|
81
|
+
|
|
82
|
+
On completion, outputs a JSON summary:
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
{
|
|
86
|
+
"flow_file": "examples/08_tax_preparer.rb",
|
|
87
|
+
"path_taken": [
|
|
88
|
+
"intro", "filing_status", "dependents", "income_types",
|
|
89
|
+
"state_filing", "foreign_accounts", "deduction_types",
|
|
90
|
+
"client_name", "client_email", "thanks"
|
|
91
|
+
],
|
|
92
|
+
"answers": {
|
|
93
|
+
"filing_status": "single",
|
|
94
|
+
"dependents": 2,
|
|
95
|
+
"income_types": ["W2", "1099"],
|
|
96
|
+
"state_filing": ["California"],
|
|
97
|
+
"foreign_accounts": "no",
|
|
98
|
+
"deduction_types": ["Medical"],
|
|
99
|
+
"client_name": "Konstantin Gredeskoul",
|
|
100
|
+
"client_email": "kigster@gmail.com"
|
|
101
|
+
},
|
|
102
|
+
"steps_completed": 10,
|
|
103
|
+
"completed_at": "2026-04-13T23:51:33-07:00"
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### `inquirex validate <flow_file>`
|
|
108
|
+
|
|
109
|
+
Check that a flow definition is well-formed without running it. Validates:
|
|
110
|
+
|
|
111
|
+
- Start step exists in the step list
|
|
112
|
+
- All transition targets reference known steps
|
|
113
|
+
- All steps are reachable from the start step (detects orphans)
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
inquirex validate examples/08_tax_preparer.rb
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### `inquirex graph <flow_file>`
|
|
120
|
+
|
|
121
|
+
Export the flow as a [Mermaid](https://mermaid.js.org/) diagram source, an image, or both.
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
inquirex graph examples/08_tax_preparer.rb # Mermaid source to stdout
|
|
125
|
+
inquirex graph examples/08_tax_preparer.rb --output flow.mmd # write source to a file
|
|
126
|
+
inquirex graph examples/08_tax_preparer.rb --format image -o flow.svg # SVG via mmdc
|
|
127
|
+
inquirex graph examples/08_tax_preparer.rb --format both --output ~/Desktop # source + image into a directory
|
|
128
|
+
inquirex graph examples/08_tax_preparer.rb --format image --open # SVG + open in viewer
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Options:
|
|
132
|
+
|
|
133
|
+
| Flag | Description |
|
|
134
|
+
|------|-------------|
|
|
135
|
+
| `--output`, `-o` | Output file or directory (default: stdout) |
|
|
136
|
+
| `--format`, `-f` | `source` (default), `image` (SVG via `mmdc`), or `both` |
|
|
137
|
+
| `--open`, `-p` | Open the generated image in the system viewer (default: false) |
|
|
138
|
+
|
|
139
|
+
Image generation requires [mermaid-cli](https://github.com/mermaid-js/mermaid-cli) (`npm install -g @mermaid-js/mermaid-cli`). The command attempts to install it automatically if `mmdc` is not on your `PATH`.
|
|
140
|
+
|
|
141
|
+
### `inquirex export <flow_file>`
|
|
142
|
+
|
|
143
|
+
Export the flow definition as JSON or YAML. Useful for serving flows to frontend adapters (the JS widget, Rails API, etc.) or for inspecting the wire format.
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
inquirex export examples/08_tax_preparer.rb # pretty JSON to stdout
|
|
147
|
+
inquirex export examples/08_tax_preparer.rb -f yml # YAML to stdout
|
|
148
|
+
inquirex export examples/08_tax_preparer.rb -o . # write 08_tax_preparer.json to cwd
|
|
149
|
+
inquirex export examples/08_tax_preparer.rb -f yml -o ~/flows # write 08_tax_preparer.yml to ~/flows
|
|
150
|
+
inquirex export examples/08_tax_preparer.rb -o out.json # write to named file
|
|
151
|
+
inquirex export examples/08_tax_preparer.rb -f yml -o out # appends .yml → out.yml
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Options:
|
|
155
|
+
|
|
156
|
+
| Flag | Description |
|
|
157
|
+
|------|-------------|
|
|
158
|
+
| `--format`, `-f` | `json` (default), `yaml`, or `yml` |
|
|
159
|
+
| `--output`, `-o` | Output file or directory (default: stdout) |
|
|
160
|
+
|
|
161
|
+
Output path rules:
|
|
162
|
+
|
|
163
|
+
- No `--output` → print to stdout
|
|
164
|
+
- `--output <dir>` (existing directory) → write `<flow-basename>.<ext>` inside it
|
|
165
|
+
- `--output <file>` → use that filename; if the extension is missing or mismatched, the appropriate one (`.json`/`.yml`) is substituted
|
|
166
|
+
|
|
167
|
+
### `inquirex version`
|
|
168
|
+
|
|
169
|
+
Print version information for the TTY adapter and its dependencies.
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
inquirex version
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Example Session
|
|
176
|
+
|
|
177
|
+
Running the tax preparation intake example:
|
|
178
|
+
|
|
179
|
+
```
|
|
180
|
+
$ inquirex run examples/08_tax_preparer.rb
|
|
181
|
+
|
|
182
|
+
_____ _ __ __ ____ ____ _____ ____ _ ____ _ _____ ___ ___ _ _ ___ _ _ _____ _ _ __ _____
|
|
183
|
+
|_ _| / \ \ \/ / | _ \ | _ \ | ____| | _ \ / \ | _ \ / \ |_ _| |_ _| / _ \ | \ | | |_ _| | \ | | |_ _| / \ | |/ / | ____|
|
|
184
|
+
| | / _ \ \ / | |_) | | |_) | | _| | |_) | / _ \ | |_) | / _ \ | | | | | | | | | \| | | | | \| | | | / _ \ | ' / | _|
|
|
185
|
+
| | / ___ \ / \ | __/ | _ < | |___ | __/ / ___ \ | _ < / ___ \ | | | | | |_| | | |\ | | | | |\ | | | / ___ \ | . \ | |___
|
|
186
|
+
|_| /_/ \_\ /_/\_\ |_| |_| \_\ |_____| |_| /_/ \_\ |_| \_\ /_/ \_\ |_| |___| \___/ |_| \_| |___| |_| \_| |_| /_/ \_\ |_|\_\ |_____|
|
|
187
|
+
|
|
188
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
189
|
+
Step 2: intro
|
|
190
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
191
|
+
|
|
192
|
+
Please describe your tax situation in a few sentences.
|
|
193
|
+
Do not under any circumstances provide personal information,
|
|
194
|
+
such as your address or social security number.
|
|
195
|
+
|
|
196
|
+
Example: I have two W-2s from my two jobs, a rental property, and a side business.
|
|
197
|
+
Press any key to continue...
|
|
198
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
199
|
+
Step 3: filing_status
|
|
200
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
201
|
+
What is your filing status for 2025? single
|
|
202
|
+
Step 4: dependents
|
|
203
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
204
|
+
How many dependents do you have? 2
|
|
205
|
+
Step 5: income_types
|
|
206
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
207
|
+
Select all income types that apply to you in 2025. W2, 1099
|
|
208
|
+
Step 6: state_filing
|
|
209
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
210
|
+
Which states do you need to file in? California
|
|
211
|
+
Step 7: foreign_accounts
|
|
212
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
213
|
+
Do you have any foreign financial accounts? no
|
|
214
|
+
Step 8: deduction_types
|
|
215
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
216
|
+
Which additional deductions apply to you? Medical
|
|
217
|
+
Step 9: client_name
|
|
218
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
219
|
+
Your name, please: Konstantin Gredeskoul
|
|
220
|
+
Step 10: client_email
|
|
221
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
222
|
+
Your email address: kigster@gmail.com
|
|
223
|
+
Step 11: thanks
|
|
224
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
225
|
+
|
|
226
|
+
Thank you! We will review your information and send you a
|
|
227
|
+
tax preparation estimate within 1-2 business days.
|
|
228
|
+
Press any key to continue...
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## Widget Mapping
|
|
232
|
+
|
|
233
|
+
The renderer selects a tty-prompt method for each node based on the `:tty` widget hint from `WidgetRegistry`:
|
|
234
|
+
|
|
235
|
+
| Widget Hint | tty-prompt Method | Used For |
|
|
236
|
+
|-------------|-------------------|----------|
|
|
237
|
+
| `text_input` | `prompt.ask` | `:string`, `:date`, `:email`, `:phone` |
|
|
238
|
+
| `multiline` | `prompt.multiline` | `:text` |
|
|
239
|
+
| `number_input` | `prompt.ask` (with `convert:`) | `:integer`, `:decimal`, `:currency` |
|
|
240
|
+
| `yes_no` | `prompt.yes?` | `:boolean` / `confirm` |
|
|
241
|
+
| `select` | `prompt.select` | `:enum` |
|
|
242
|
+
| `multi_select` | `prompt.multi_select` | `:multi_enum` |
|
|
243
|
+
| `enum_select` | `prompt.enum_select` | Numbered menu variant |
|
|
244
|
+
| `mask` | `prompt.mask` | Password/hidden input |
|
|
245
|
+
| `slider` | `prompt.slider` | Numeric range |
|
|
246
|
+
|
|
247
|
+
You can override the default by setting an explicit `:tty` widget hint in the DSL:
|
|
248
|
+
|
|
249
|
+
```ruby
|
|
250
|
+
ask :priority do
|
|
251
|
+
type :enum
|
|
252
|
+
question "How urgent?"
|
|
253
|
+
options low: "Low", medium: "Medium", high: "High"
|
|
254
|
+
widget target: :tty, type: :enum_select # numbered menu instead of arrow-key select
|
|
255
|
+
transition to: :next_step
|
|
256
|
+
end
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
## Display Verbs
|
|
260
|
+
|
|
261
|
+
| Verb | Rendering |
|
|
262
|
+
|------|-----------|
|
|
263
|
+
| `header` | Large ASCII-art text via TTY::Font (falls back to TTY::Box) |
|
|
264
|
+
| `say` | Plain text with "Press any key to continue..." |
|
|
265
|
+
| `btw` | Info-styled box (blue border) |
|
|
266
|
+
| `warning` | Warning-styled box (yellow/red) |
|
|
267
|
+
|
|
268
|
+
## LLM Integration
|
|
269
|
+
|
|
270
|
+
When a flow contains [inquirex-llm](../inquirex-llm) verbs (`clarify`,
|
|
271
|
+
`describe`, `summarize`, `detour`), the `inquirex run` command automatically:
|
|
272
|
+
|
|
273
|
+
1. Loads `.env` files, walking up from the current directory and the flow
|
|
274
|
+
file's directory. Shell-set values take precedence; empty-string keys are
|
|
275
|
+
treated as unset.
|
|
276
|
+
1. Picks an adapter based on available credentials (see "Adapter selection"
|
|
277
|
+
below).
|
|
278
|
+
1. Shows a `🧠 Thinking — asking <provider> to extract structured data…`
|
|
279
|
+
banner while the LLM call is in flight.
|
|
280
|
+
1. For `clarify` steps, splats the extracted fields into the engine's
|
|
281
|
+
top-level answers via `Engine#prefill!`, so any downstream step with
|
|
282
|
+
`skip_if not_empty(:field)` is auto-skipped.
|
|
283
|
+
1. Prints a `✅` / `❓` extraction table showing which fields the LLM filled
|
|
284
|
+
in vs. which ones will still be asked.
|
|
285
|
+
|
|
286
|
+
### Adapter selection (first match wins)
|
|
287
|
+
|
|
288
|
+
| Condition | Adapter used |
|
|
289
|
+
|---------------------------------------------|--------------------------------------|
|
|
290
|
+
| `INQUIREX_LLM_ADAPTER=null` | `Inquirex::LLM::NullAdapter` |
|
|
291
|
+
| `INQUIREX_LLM_ADAPTER=anthropic` | `Inquirex::LLM::AnthropicAdapter` |
|
|
292
|
+
| `INQUIREX_LLM_ADAPTER=openai` | `Inquirex::LLM::OpenAIAdapter` |
|
|
293
|
+
| `ANTHROPIC_API_KEY` is set | `Inquirex::LLM::AnthropicAdapter` |
|
|
294
|
+
| `OPENAI_API_KEY` is set | `Inquirex::LLM::OpenAIAdapter` |
|
|
295
|
+
| nothing set | `Inquirex::LLM::NullAdapter` (demo) |
|
|
296
|
+
|
|
297
|
+
### End-to-end example
|
|
298
|
+
|
|
299
|
+
`examples/09_tax_preparer_llm.rb` is a complete LLM-assisted tax intake. The
|
|
300
|
+
user types one free-text description of their tax situation; the LLM extracts
|
|
301
|
+
`filing_status`, `dependents`, `income_types`, `state_filing`; the wizard only
|
|
302
|
+
asks for whatever the LLM couldn't determine, then runs a final `summarize`
|
|
303
|
+
for a complexity / fee-estimate write-up.
|
|
304
|
+
|
|
305
|
+
```ruby
|
|
306
|
+
# examples/09_tax_preparer_llm.rb (excerpt)
|
|
307
|
+
require "inquirex"
|
|
308
|
+
require "inquirex/llm"
|
|
309
|
+
|
|
310
|
+
Inquirex.define id: "tax-preparer-llm-2025", version: "1.0.0" do
|
|
311
|
+
meta title: "Tax Prep Intake (LLM-assisted)"
|
|
312
|
+
start :describe
|
|
313
|
+
|
|
314
|
+
ask :describe do
|
|
315
|
+
type :text
|
|
316
|
+
question "Describe your 2025 tax situation in your own words…"
|
|
317
|
+
widget target: :tty, type: :multiline
|
|
318
|
+
transition to: :extracted
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
clarify :extracted do
|
|
322
|
+
from :describe
|
|
323
|
+
prompt <<~PROMPT
|
|
324
|
+
Extract tax intake fields. Use these EXACT value conventions:
|
|
325
|
+
filing_status: "single" | "married_filing_jointly" | …
|
|
326
|
+
income_types: array of "W2" | "1099" | "Business" | "Investment" | "Rental" | "Retirement"
|
|
327
|
+
Use "" / 0 / [] for anything the client did not mention.
|
|
328
|
+
PROMPT
|
|
329
|
+
schema filing_status: :string,
|
|
330
|
+
dependents: :integer,
|
|
331
|
+
income_types: :multi_enum,
|
|
332
|
+
state_filing: :string
|
|
333
|
+
model :claude_sonnet
|
|
334
|
+
temperature 0.0
|
|
335
|
+
transition to: :filing_status
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
ask :filing_status do
|
|
339
|
+
type :enum
|
|
340
|
+
question "What is your filing status?"
|
|
341
|
+
options(%w[single married_filing_jointly married_filing_separately head_of_household widowed])
|
|
342
|
+
skip_if not_empty(:filing_status)
|
|
343
|
+
transition to: :dependents
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
# …same pattern for :dependents, :income_types, :state_filing…
|
|
347
|
+
|
|
348
|
+
ask :client_contact do
|
|
349
|
+
type :string
|
|
350
|
+
question "Your name and email?"
|
|
351
|
+
transition to: :summary
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
summarize :summary do
|
|
355
|
+
from_all
|
|
356
|
+
prompt "Return JSON: { complexity, fee_estimate_low, fee_estimate_high, red_flags, notes }"
|
|
357
|
+
transition to: :done
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
say :done do
|
|
361
|
+
text "Thank you! A tax professional will review your intake and reach out."
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
Run it:
|
|
367
|
+
|
|
368
|
+
```bash
|
|
369
|
+
# Put your key in any .env up the tree — OPENAI_API_KEY or ANTHROPIC_API_KEY
|
|
370
|
+
echo 'OPENAI_API_KEY=sk-…' >> ../.env
|
|
371
|
+
|
|
372
|
+
inquirex run examples/09_tax_preparer_llm.rb
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
A typical session (input shortened):
|
|
376
|
+
|
|
377
|
+
```
|
|
378
|
+
> I'm MFJ with two kids. I'm W-2 at Google, my wife runs a consulting LLC,
|
|
379
|
+
we have a rental in Oakland and some Coinbase crypto. We live in California.
|
|
380
|
+
|
|
381
|
+
🧠 Thinking — asking GPT to extract structured data…
|
|
382
|
+
📋 LLM extracted:
|
|
383
|
+
✅ filing_status: "married_filing_jointly"
|
|
384
|
+
✅ dependents: 2
|
|
385
|
+
✅ income_types: ["W2", "Business", "Rental", "Investment"]
|
|
386
|
+
✅ state_filing: "California"
|
|
387
|
+
# :filing_status, :dependents, :income_types, :state_filing all auto-skipped.
|
|
388
|
+
# User only gets asked for :client_contact before the summary.
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
### Troubleshooting
|
|
392
|
+
|
|
393
|
+
- **"the null adapter"** shown in the thinking banner → no API key was found.
|
|
394
|
+
Check `OPENAI_API_KEY` / `ANTHROPIC_API_KEY` in your shell or in a `.env`
|
|
395
|
+
up the directory tree.
|
|
396
|
+
- **`NoMethodError: undefined method 'prefill!'`** → you're running the
|
|
397
|
+
`inquirex` binary against an older rubygems-installed copy of the core gem.
|
|
398
|
+
`exe/inquirex` now bootstraps Bundler against the repo's Gemfile
|
|
399
|
+
automatically, but a global `rake install`'d copy will still be
|
|
400
|
+
stale — run `rake install` in `../inquirex` to update, or run from a
|
|
401
|
+
checkout so the shim's Bundler bootstrap takes over.
|
|
402
|
+
|
|
403
|
+
## Examples
|
|
404
|
+
|
|
405
|
+
The gem ships with 10 examples of increasing complexity:
|
|
406
|
+
|
|
407
|
+
| Example | Description | Steps | Features |
|
|
408
|
+
|---------|-------------|-------|----------|
|
|
409
|
+
| `01_hello_world.rb` | Minimal flow | 3 | String and integer input |
|
|
410
|
+
| `02_yes_or_no.rb` | Boolean branching | 3 | `confirm`, `equals` rule |
|
|
411
|
+
| `03_food_preferences.rb` | Multi-select branching | 6 | `multi_enum`, `contains` rule |
|
|
412
|
+
| `04_event_registration.rb` | Two-level branching | 9 | Nested conditionals |
|
|
413
|
+
| `05_job_application.rb` | Composed rules | 13 | `all()`, `any()`, `greater_than` |
|
|
414
|
+
| `06_health_assessment.rb` | Three-level branching | 18 | Complex composed rules |
|
|
415
|
+
| `07_loan_application.rb` | Real-world loan intake | 20+ | Currency, 3-level branching |
|
|
416
|
+
| `08_tax_preparer.rb` | Full tax preparation wizard | 18+ | All data types, deep branching |
|
|
417
|
+
| `09_tax_preparer_llm.rb` | **LLM-assisted tax intake** | 9 | `clarify` + `summarize`, `skip_if not_empty`, auto-prefill |
|
|
418
|
+
| `10_real_tax_preparer.rb` | Realistic tax preparer flow | 20+ | Full intake variant |
|
|
419
|
+
|
|
420
|
+
Run any example:
|
|
421
|
+
|
|
422
|
+
```bash
|
|
423
|
+
inquirex run examples/01_hello_world.rb
|
|
424
|
+
inquirex run examples/08_tax_preparer.rb
|
|
425
|
+
inquirex run examples/09_tax_preparer_llm.rb
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
Validate all examples:
|
|
429
|
+
|
|
430
|
+
```bash
|
|
431
|
+
for f in examples/*.rb; do inquirex validate "$f"; done
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
## Architecture
|
|
435
|
+
|
|
436
|
+
```
|
|
437
|
+
inquirex-tty/
|
|
438
|
+
├── exe/inquirex # CLI entry point (dry-cli)
|
|
439
|
+
└── lib/inquirex/tty/
|
|
440
|
+
├── commands/
|
|
441
|
+
│ ├── run.rb # Interactive flow execution
|
|
442
|
+
│ ├── validate.rb # Definition validation
|
|
443
|
+
│ ├── graph.rb # Mermaid diagram export
|
|
444
|
+
│ ├── export.rb # JSON / YAML serialization
|
|
445
|
+
│ └── version.rb # Print version info
|
|
446
|
+
├── renderer.rb # Node → tty-prompt widget dispatcher
|
|
447
|
+
├── flow_loader.rb # Load .rb flow definitions
|
|
448
|
+
├── output_path.rb # Shared -o/--output path resolution
|
|
449
|
+
├── ui_helper.rb # TTY::Box/Pastel/Screen helpers
|
|
450
|
+
└── commands.rb # dry-cli command registry
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
### Renderer
|
|
454
|
+
|
|
455
|
+
`Inquirex::TTY::Renderer` is the core class that maps nodes to tty-prompt calls. It reads the `:tty` widget hint from each node (via `WidgetRegistry` defaults or explicit DSL hints) and dispatches to the matching `render_*` method.
|
|
456
|
+
|
|
457
|
+
The `TTY::Prompt` instance is injectable for testing:
|
|
458
|
+
|
|
459
|
+
```ruby
|
|
460
|
+
prompt = TTY::Prompt::Test.new
|
|
461
|
+
renderer = Inquirex::TTY::Renderer.new(prompt:)
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
### FlowLoader
|
|
465
|
+
|
|
466
|
+
Loads a `.rb` file and evaluates it to produce an `Inquirex::Definition`. The file must contain an `Inquirex.define` call that returns the definition.
|
|
467
|
+
|
|
468
|
+
### UIHelper
|
|
469
|
+
|
|
470
|
+
A mixin module providing styled output helpers (`box`, `info`, `success`, `error`, `warning`, `sep`, `next_step`) built on TTY::Box, Pastel, and TTY::Screen. Included automatically in all CLI commands and the Renderer.
|
|
471
|
+
|
|
472
|
+
## Development
|
|
473
|
+
|
|
474
|
+
```bash
|
|
475
|
+
bin/setup # install dependencies
|
|
476
|
+
bundle exec rspec # run tests
|
|
477
|
+
bundle exec rspec --format doc # verbose test output
|
|
478
|
+
bundle exec rubocop # lint
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
### Useful `just` tasks
|
|
482
|
+
|
|
483
|
+
```bash
|
|
484
|
+
just test # run full test suite with coverage
|
|
485
|
+
just lint # rubocop
|
|
486
|
+
just format # rubocop --auto-correct
|
|
487
|
+
just run examples/01_hello_world.rb # run a flow
|
|
488
|
+
just validate examples/01_hello_world.rb
|
|
489
|
+
just graph examples/01_hello_world.rb
|
|
490
|
+
just examples # validate all examples
|
|
491
|
+
just ci # tests + lint
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
### Writing a Custom Flow
|
|
495
|
+
|
|
496
|
+
Any `.rb` file that calls `Inquirex.define` and returns a `Definition` works:
|
|
497
|
+
|
|
498
|
+
```ruby
|
|
499
|
+
require "inquirex"
|
|
500
|
+
|
|
501
|
+
Inquirex.define id: "my-flow", version: "1.0.0" do
|
|
502
|
+
meta title: "My Flow"
|
|
503
|
+
start :first_question
|
|
504
|
+
|
|
505
|
+
ask :first_question do
|
|
506
|
+
type :enum
|
|
507
|
+
question "Pick one:"
|
|
508
|
+
options a: "Option A", b: "Option B", c: "Option C"
|
|
509
|
+
widget target: :tty, type: :select
|
|
510
|
+
transition to: :detail, if_rule: equals(:first_question, "a")
|
|
511
|
+
transition to: :done
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
ask :detail do
|
|
515
|
+
type :text
|
|
516
|
+
question "Tell me more about A:"
|
|
517
|
+
transition to: :done
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
say :done do
|
|
521
|
+
text "All done!"
|
|
522
|
+
end
|
|
523
|
+
end
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
## License
|
|
527
|
+
|
|
528
|
+
MIT. See [LICENSE.txt](LICENSE.txt).
|
|
529
|
+
|
|
530
|
+
Copyright 2026 Konstantin Gredeskoul & Inquirex.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
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
|
|
@@ -0,0 +1,21 @@
|
|
|
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">99%</text>
|
|
19
|
+
<text x="80" y="14">99%</text>
|
|
20
|
+
</g>
|
|
21
|
+
</svg>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Example 1: Hello World
|
|
4
|
+
#
|
|
5
|
+
# The simplest possible flow — three linear steps, no branching.
|
|
6
|
+
# Demonstrates: string input, integer input, say display step.
|
|
7
|
+
#
|
|
8
|
+
# Run: bundle exec exe/inquirex-tty run examples/01_hello_world.rb
|
|
9
|
+
|
|
10
|
+
Inquirex.define id: "hello-world", version: "1.0.0" do
|
|
11
|
+
meta title: "Hello World", subtitle: "A simple introduction"
|
|
12
|
+
|
|
13
|
+
start :name
|
|
14
|
+
|
|
15
|
+
ask :name do
|
|
16
|
+
type :string
|
|
17
|
+
question "What is your name?"
|
|
18
|
+
widget target: :tty, type: :text_input
|
|
19
|
+
transition to: :age
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
ask :age do
|
|
23
|
+
type :integer
|
|
24
|
+
question "How old are you?"
|
|
25
|
+
widget target: :tty, type: :number_input
|
|
26
|
+
transition to: :farewell
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
say :farewell do
|
|
30
|
+
text "Thanks for stopping by! Have a great day."
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Example 2: Yes or No
|
|
4
|
+
#
|
|
5
|
+
# A single boolean branch. Answering "yes" takes a detour;
|
|
6
|
+
# answering "no" goes straight to the end.
|
|
7
|
+
# Demonstrates: confirm verb, equals rule, simple branching.
|
|
8
|
+
#
|
|
9
|
+
# Run: bundle exec exe/inquirex-tty run examples/02_yes_or_no.rb
|
|
10
|
+
|
|
11
|
+
Inquirex.define id: "yes-or-no", version: "1.0.0" do
|
|
12
|
+
meta title: "Yes or No", subtitle: "A simple boolean branch"
|
|
13
|
+
|
|
14
|
+
start :has_pet
|
|
15
|
+
|
|
16
|
+
confirm :has_pet do
|
|
17
|
+
question "Do you have a pet?"
|
|
18
|
+
widget target: :tty, type: :yes_no
|
|
19
|
+
transition to: :pet_name, if_rule: equals(:has_pet, true)
|
|
20
|
+
transition to: :done
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
ask :pet_name do
|
|
24
|
+
type :string
|
|
25
|
+
question "What is your pet's name?"
|
|
26
|
+
widget target: :tty, type: :text_input
|
|
27
|
+
transition to: :done
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
say :done do
|
|
31
|
+
text "All done! Thanks for answering."
|
|
32
|
+
end
|
|
33
|
+
end
|
|
Binary file
|