inquirex 0.4.1 → 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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +18 -0
  3. data/README.md +203 -52
  4. data/docs/badges/coverage_badge.svg +2 -2
  5. data/examples/01_readme_example.rb +55 -0
  6. data/examples/02_readme_mermaid.png +0 -0
  7. data/examples/02_readme_mermaid.rb +36 -0
  8. data/examples/03_send_email_actions.rb +97 -0
  9. data/examples/README.md +33 -0
  10. data/justfile +3 -2
  11. data/lib/inquirex/accumulator.rb +29 -0
  12. data/lib/inquirex/actions/action.rb +68 -0
  13. data/lib/inquirex/actions/base.rb +41 -0
  14. data/lib/inquirex/actions/custom.rb +31 -0
  15. data/lib/inquirex/actions/outbox.rb +57 -0
  16. data/lib/inquirex/actions/runner.rb +52 -0
  17. data/lib/inquirex/actions/send_email.rb +174 -0
  18. data/lib/inquirex/actions/template.rb +95 -0
  19. data/lib/inquirex/actions/webhook.rb +139 -0
  20. data/lib/inquirex/actions.rb +57 -0
  21. data/lib/inquirex/answers.rb +13 -0
  22. data/lib/inquirex/completion_metadata.rb +82 -0
  23. data/lib/inquirex/definition.rb +67 -5
  24. data/lib/inquirex/dsl/action_builder.rb +53 -0
  25. data/lib/inquirex/dsl/flow_builder.rb +49 -6
  26. data/lib/inquirex/engine/state_serializer.rb +6 -4
  27. data/lib/inquirex/engine.rb +97 -5
  28. data/lib/inquirex/errors.rb +5 -0
  29. data/lib/inquirex/graph/mermaid_exporter.rb +2 -0
  30. data/lib/inquirex/node.rb +2 -0
  31. data/lib/inquirex/rules/all.rb +12 -0
  32. data/lib/inquirex/rules/any.rb +11 -0
  33. data/lib/inquirex/rules/base.rb +6 -0
  34. data/lib/inquirex/rules/contains.rb +12 -0
  35. data/lib/inquirex/rules/equals.rb +12 -0
  36. data/lib/inquirex/rules/greater_than.rb +12 -0
  37. data/lib/inquirex/rules/less_than.rb +12 -0
  38. data/lib/inquirex/rules/not_empty.rb +12 -0
  39. data/lib/inquirex/validation/adapter.rb +1 -0
  40. data/lib/inquirex/validation/null_adapter.rb +5 -0
  41. data/lib/inquirex/version.rb +1 -1
  42. data/lib/inquirex/widget_registry.rb +1 -0
  43. data/lib/inquirex.rb +13 -0
  44. metadata +35 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ef8604fa6b1eb6fb6cd9506c696c806067d209abf6260872f5d0cc87d5b6eae0
4
- data.tar.gz: 1a543f219f202ff5ae05fcae1040e460391e044b177a94612519618d9604f943
3
+ metadata.gz: f99f7adba97c905c55490b57467fe062251719bbb98d698fe49e6dd04360fcf7
4
+ data.tar.gz: 2c6850590153b96ee376ddaa0314c2de3cf729f6ea2d7c8f3d0ccb20d2359683
5
5
  SHA512:
6
- metadata.gz: eeeb13a340aaf5169f661c4fce77bdc666e68d8fac9c7b4ae52757c5b5999b46ae0a9ec83042c680d3f1b57302cf9f0e08c4e9b80467ccd98e2447294f0110cb
7
- data.tar.gz: 0b213dde96f8ec277e31d302a557bb665361046529b9ebf75c19a159518f7f84e7d18ec6db4927e7d23d1ca4bd1dd1f0e68208c971624cd2c96b1624939dcc74
6
+ metadata.gz: 18b95d28461f971551997bb177c6ead5c4165562f1d627460d58656440a0884bf0972eef2d97a31e3883f34720349be82e24e2fa567b7181eea0f14361182355
7
+ data.tar.gz: ed93da41fe8d2f66098b0165224e472d794d6f7ab9313dd8ec9a8a80b0d299ef5623d957361d83a27680b1ce3c025dd294fc6690c855c02a32df90348726718c
data/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.6.0] - 2026-07-19
4
+
5
+ - Post-completion `action` DSL verb with an extensible effect registry
6
+ - `send_email` effect: builds `Mail::Message` objects (multipart text/HTML)
7
+ from `{{field}}` templates into `Answers#outbox`; delivery is the host
8
+ application's responsibility. The `mail` gem is a soft dependency.
9
+ - `run { |answers, outbox| ... }` escape-hatch effect (stripped from JSON)
10
+ - `webhook` effect: POSTs the answers envelope to a static https URL whose
11
+ host must be covered by the new top-level `allowed_domains` declaration;
12
+ enforcement runs in `Definition#validate!`, so tampered JSON definitions
13
+ fail at rehydration
14
+ - `Inquirex::Actions::Runner` and `Inquirex::Actions.run(definition, answers)`
15
+ - Actions serialize to/from JSON with their rule gates
16
+ - `CompletionMetadata` (OpenStruct; `engine` and `engine_version` required)
17
+ stamped by the engine at flow completion, enrichable by front-ends via the
18
+ new `Engine#after_completion` hook, persisted in engine state, and merged
19
+ into answers by `Engine#answers_with_metadata`
20
+
3
21
  ## [0.1.0] - 2026-04-13
4
22
 
5
23
  - Initial release
data/README.md CHANGED
@@ -1,50 +1,72 @@
1
- [![Ruby](https://github.com/inquirex/inquirex/actions/workflows/main.yml/badge.svg)](https://github.com/inquirex/inquirex/actions/workflows/main.yml)   ![Coverage](docs/badges/coverage_badge.svg)
1
+ [![Gem Version](https://badge.fury.io/rb/inquirex.svg)](https://badge.fury.io/rb/inquirex) [![Ruby](https://github.com/inquirex/inquirex/actions/workflows/main.yml/badge.svg)](https://github.com/inquirex/inquirex/actions/workflows/main.yml) ![Coverage](docs/badges/coverage_badge.svg)
2
2
 
3
3
  # Inquirex
4
4
 
5
- `inquirex` is a pure Ruby, declarative, rules-driven questionnaire engine for building conditional intake forms, qualification wizards, and branching surveys.
5
+ `inquirex` family of libraries (part Ruby, part JavaScript), is a declarative, rules-driven questionnaire engine for building conditional intake forms, qualification wizards, and branching surveys, which can be AI-enabled, and rendered on your site inside a "copilot" widget window or in a TUI (Terminal UI) all the same.
6
6
 
7
7
  > [!IMPORTANT]
8
8
  >
9
- > Note that `inquirex` is the base gem of the ecosystem that contains:
9
+ > Today the ecosystem contains:
10
10
  >
11
- > - [`inquirex`](https://github.com/inquirex/inquirex)
12
- > - [`inquirex-llm`](https://github.com/inquirex/inquirex-llm)
13
- > - [`inquirex-tty`](https://github.com/inquirex/inquirex-)
14
- > - [`inquirex-js`](https://github.com/inquirex/inquirex-js)
11
+ > - [`inquirex`](https://github.com/inquirex/inquirex): the base gem that defines the graph via DSL and provides most of the backend features
12
+ > - [`inquirex-llm`](https://github.com/inquirex/inquirex-llm): a tiny gem that extends the DSL by the word `extract` which, given a previous question answered in the form of free text, can use the model of your choice to return structured breakdown of text into answers to questions that might follow, thus shortening the form considerably.
13
+ > - [`inquirex-tty`](https://github.com/inquirex/inquirex-tty): is the gem that renders the forms on the TUI (Terminal UI). This is also the gem that provides the CLI `inqurex` for performing various tasks such as validating DSL files, converting them from Ruby to JSON and back, and more. It is also the gem where a folder of DSL `examples` can be used to get a feel for how this works.
14
+ > - [`inquirex-js`](https://github.com/inquirex/inquirex-js) (`npmjs` module [`@kigster/inquirex-js`](https://www.npmjs.com/package/@kigster/inquirex-js)) is the NPM package that connects web UI with the form definition in JSON format. If LLM is not needed, the entire flow becomes deterministic and collects answers as the user answers your questions, and then POSTS them to the URL of your choice.
15
15
  >
16
- > For a presentation on 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).
16
+ > 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).
17
+ >
18
+ > Finally, the SaaS application [qualified.at](https://qualified.at) allows busy professionals such as consultants, doctors, tax-preparers, who are short on time, or can't be bothered to figure out the technical side of integrating these libraries, to leverage the entire ecosystem by creating their own custom lead intake forms on the SaaS application, dropping the auto-generated widget on their (potentially static website), and showing the copilot to their customers, customizing from nothing at all, to what triggers copilot's appearance, it's look and feel, and so on. The site automatically supports the LLM keyword `extract` as part of the DSL, and also collects the answers from your leads in your account: the data that you own, and can export at any time into a CSV download, a Google Spreadsheet, etc.
19
+
20
+ ## Why Inquirex?
21
+
22
+ There are plenty of form builders, state machines, workflow libraries, etc. And yet, none of them combine the convenience of a DSL with logical separation between the form substance, and the rendering UI quite like this.
23
+
24
+ There are plenty of form building gems, hard-coded branching controllers, React components, or database callbacks.
25
+
26
+ ***Inquirex is quite different.***
27
+
28
+ It provides a rich DSL for creating dynamic user intake forms, with complexity ranging from a simple straight-line forms to multi-branch, conditional forms with dozens of potential branches, *UI widget hints* for various rendering platforms, with *accumulators* that allow computing sums or products based on user's answers (which allow you to compute — for user or for yourself — that the service you are requesting will cost between $X & $Y).
29
+
30
+ > [!NOTE]
31
+ >
32
+ > For technically inclined, Inquirex turns user forms into a directed graph, where nodes are either questions or statements (or UI transitions), while edges are AST-based logical conditions that can be stacked and joined in arbitrarily complex ways, allowing you to move from one question to any other based on the previous answer. See the details below.
33
+
34
+ ***Don't want to deal with figuring it out? Head to [Qualified.At](https://qualified.at/onboarding) and walk through the demo onboarding form, that exists specifically to show you how quickly you can have the same conceptually on your site, tailored to YOUR users.***
17
35
 
18
36
  ## Summary
19
37
 
20
- It is the core gem in the Inquirex ecosystem and focuses on:
38
+ This one is the core gem in the Inquirex ecosystem that focuses on:
21
39
 
22
40
  - A conversational DSL (`ask`, `say`, `header`, `btw`, `warning`, `confirm`)
41
+
23
42
  - A serializable AST rule system (`contains`, `equals`, `greater_than`, `less_than`, `not_empty`, `all`, `any`)
43
+
24
44
  - Framework-agnostic widget rendering hints (`widget` DSL verb, `WidgetHint`, `WidgetRegistry`)
25
- - Named **accumulators** for running totals (pricing, complexity scoring, credit scoring, lead qualification)
45
+
46
+ - Named ***accumulators*** for running totals (pricing, complexity scoring, credit scoring, lead qualification)
47
+
26
48
  - An immutable flow definition graph
49
+
27
50
  - A runtime engine for stateful step traversal
51
+
28
52
  - JSON round-trip serialization for cross-platform clients
29
- - A structured `Answers` wrapper and Mermaid graph export
30
53
 
31
- ## Status
54
+ - A structured `Answers` wrapper and Mermaid graph export (provided by `inquirex-tty` gem's CLI)
32
55
 
33
- - Version: `0.4.0`
34
- - Ruby: `>= 4.0.0` (project currently uses `4.0.5`)
35
- - Test suite: `220 examples, 0 failures`
36
- - Coverage: ~`94%` line coverage
56
+ - In short:
37
57
 
38
- ## Why Inquirex
58
+ - Define once in Ruby
59
+ - Serialize to JSON if you need to show the questions on the web UI
60
+ - Evaluate transitions consistently using rule AST objects
61
+ - Run the same flow identically in concept in different frontends (web widget, TUI, etc.)
39
62
 
40
- Many form builders hard-code branching in controllers, React components, or database callbacks. Inquirex keeps flow logic in one portable graph:
63
+ So, if you ever wanted to ask users who arrive at your site a few simple questions (PII questions are strongly discouraged due to the fact that the gem is typically used by non-logged in users on your end — except, perhaps, name and email), and depending on their answers you might want to dig a bit deeper, so that once you get on the phone with them you'll already have a general picture, these gems are for you (or head to [qualified.at](https://qualified.at) and set up your free account to see how this works in practice.
41
64
 
42
- - Define once in Ruby
43
- - Serialize to JSON
44
- - Evaluate transitions consistently using rule AST objects
45
- - Run the same flow in different frontends (web widget, terminal, etc.)
65
+ ## Examples
46
66
 
47
- This design is the foundation for the broader ecosystem (`inquirex-ui`, `inquirex-tty`, `inquirex-js`, `inquirex-llm`, `inquirex-rails`).
67
+ > [!NOTE]
68
+ >
69
+ > There are a couple of examples shown in this `README` file, and also in the [`./examples`](./examples) folder. You can read about running them in a [`README.md`](./examples/README.md) inside that folder.
48
70
 
49
71
  ## Installation
50
72
 
@@ -52,6 +74,10 @@ Add to your Gemfile:
52
74
 
53
75
  ```ruby
54
76
  gem "inquirex"
77
+ # if you are building a TUI or need the CLI version of this gem
78
+ gem "inquirex-tty"
79
+ # if you want to add the keyword `extract` to the DSL vocabilary
80
+ gem "inquire-llm"
55
81
  ```
56
82
 
57
83
  Then install:
@@ -60,6 +86,8 @@ Then install:
60
86
  bundle install
61
87
  ```
62
88
 
89
+ And then, define your form as a Ruby DSL file (see [examples](https://github.com/inquirex/inquirex-tty/tree/main/examples) on Github), and consume it by either the `inquirex-tty` gem on the command line, or on the web via the [@kigster/inqiurex-js](https://www.npmjs.com/package/@kigster/inquirex-js) npmjs package.
90
+
63
91
  ## Quick Start
64
92
 
65
93
  ```ruby
@@ -170,26 +198,35 @@ end
170
198
 
171
199
  When no explicit `widget` is set, `WidgetRegistry` fills in sensible defaults per data type:
172
200
 
173
- | Data Type | Desktop | Mobile | TTY |
174
- |-----------|---------|--------|-----|
175
- | `:enum` | `radio_group` | `dropdown` | `select` |
201
+ | Data Type | Desktop | Mobile | TTY |
202
+ | ------------- | ---------------- | ---------------- | -------------- |
203
+ | `:enum` | `radio_group` | `dropdown` | `select` |
176
204
  | `:multi_enum` | `checkbox_group` | `checkbox_group` | `multi_select` |
177
- | `:boolean` | `toggle` | `yes_no_buttons` | `yes_no` |
178
- | `:string` | `text_input` | `text_input` | `text_input` |
179
- | `:text` | `textarea` | `textarea` | `multiline` |
180
- | `:integer` | `number_input` | `number_input` | `number_input` |
181
- | `:currency` | `currency_input` | `currency_input` | `number_input` |
182
- | `:date` | `date_picker` | `date_picker` | `text_input` |
205
+ | `:boolean` | `toggle` | `yes_no_buttons` | `yes_no` |
206
+ | `:string` | `text_input` | `text_input` | `text_input` |
207
+ | `:text` | `textarea` | `textarea` | `multiline` |
208
+ | `:integer` | `number_input` | `number_input` | `number_input` |
209
+ | `:currency` | `currency_input` | `currency_input` | `number_input` |
210
+ | `:date` | `date_picker` | `date_picker` | `text_input` |
183
211
 
184
212
  Display verbs (`say`, `header`, `btw`, `warning`) have no widget hints.
185
213
 
186
214
  Widget hints are included in JSON serialization under a `"widget"` key, keyed by target:
187
215
 
188
216
  ```json
189
- "widget": {
190
- "desktop": { "type": "radio_group", "columns": 3 },
191
- "mobile": { "type": "dropdown" },
192
- "tty": { "type": "select" }
217
+ {
218
+ "widget": {
219
+ "desktop": {
220
+ "type": "radio_group",
221
+ "columns": 3
222
+ },
223
+ "mobile": {
224
+ "type": "dropdown"
225
+ },
226
+ "tty": {
227
+ "type": "select"
228
+ }
229
+ }
193
230
  }
194
231
  ```
195
232
 
@@ -197,7 +234,7 @@ Widget hints are included in JSON serialization under a `"widget"` key, keyed by
197
234
 
198
235
  ```ruby
199
236
  step = definition.step(:priority)
200
- step.widget_hint_for(target: :desktop) # explicit hint or nil
237
+ step.widget_hint_for(target: :desktop) # explicit hint or nil
201
238
  step.effective_widget_hint_for(target: :desktop) # explicit hint or registry default
202
239
  ```
203
240
 
@@ -225,12 +262,12 @@ end
225
262
 
226
263
  Use the `accumulate` verb inside any `ask`/`confirm` step. Exactly one **shape** key must be provided:
227
264
 
228
- | Shape | Fits | Semantics |
229
- |-------|------|-----------|
230
- | `lookup: { ... }` | `:enum` | Adds the amount mapped to the chosen option value |
231
- | `per_selection: { ... }` | `:multi_enum` | Sums the amounts for every selected option |
232
- | `per_unit: N` | `:integer`, `:decimal` | Multiplies the numeric answer by `N` |
233
- | `flat: N` | any type | Adds `N` when the step has a truthy, non-empty answer |
265
+ | Shape | Fits | Semantics |
266
+ | ------------------------ | ---------------------- | ----------------------------------------------------- |
267
+ | `lookup: { ... }` | `:enum` | Adds the amount mapped to the chosen option value |
268
+ | `per_selection: { ... }` | `:multi_enum` | Sums the amounts for every selected option |
269
+ | `per_unit: N` | `:integer`, `:decimal` | Multiplies the numeric answer by `N` |
270
+ | `flat: N` | any type | Adds `N` when the step has a truthy, non-empty answer |
234
271
 
235
272
  ```ruby
236
273
  ask :filing_status do
@@ -429,6 +466,120 @@ Pass a custom adapter to the engine:
429
466
  engine = Inquirex::Engine.new(definition, validator: my_validator)
430
467
  ```
431
468
 
469
+ ## Completion Metadata
470
+
471
+ When a flow finishes, the engine guarantees a `CompletionMetadata` — an
472
+ OpenStruct describing how the answers were collected. Only `engine` and
473
+ `engine_version` are required members; rendering front-ends attach richer
474
+ environment details from an `after_completion` hook:
475
+
476
+ ```ruby
477
+ engine.after_completion do |eng|
478
+ eng.completion_metadata = Inquirex::CompletionMetadata.new(
479
+ engine: "inquirex-tty",
480
+ engine_version: Inquirex::TTY::VERSION,
481
+ uname: OpenStruct.new(Etc.uname),
482
+ user: Etc.getlogin,
483
+ terminal: ENV["LC_TERMINAL"] || ENV["TERM_PROGRAM"] || "Unknown"
484
+ )
485
+ end
486
+ ```
487
+
488
+ If no hook supplies one, the engine stamps the minimal core version
489
+ (`engine: "inquirex"`, `engine_version: Inquirex::VERSION`). The metadata
490
+ persists through `Engine#to_state` / `Engine.from_state`, and
491
+ `engine.answers_with_metadata` merges it into the answers hash under
492
+ `:completion_metadata` — which also makes it available to post-completion
493
+ actions: webhook payloads carry it, and email templates can interpolate
494
+ `{{completion_metadata.engine}}`.
495
+
496
+ ## Post-Completion Actions
497
+
498
+ After all questions are answered, `action` declarations run server-side with
499
+ the collected answers. The flagship effect is `send_email`, which **builds**
500
+ `Mail::Message` objects (the same object ActionMailer wraps) and attaches them
501
+ to `answers.outbox` — **nothing is delivered**; the host application decides
502
+ how and when to send.
503
+
504
+ ```ruby
505
+ Inquirex.define id: "tax-intake-2025" do
506
+ allowed_domains "*.agentica.group" # egress allowlist for webhook effects
507
+
508
+ # ... ask steps ...
509
+
510
+ action :client_receipt, if: not_empty(:email) do
511
+ send_email to: "{{email}}",
512
+ from: "forms@agentica.group",
513
+ subject: "Thanks {{name}} — we received your intake",
514
+ text: <<~TEXT,
515
+ Hi {{name}},
516
+ We received your answers:
517
+
518
+ {{answers_summary}}
519
+ TEXT
520
+ html: <<~HTML
521
+ <p>Hi {{name}},</p>
522
+ <img src="https://cdn.agentica.group/logo.png" alt="Logo">
523
+ {{answers_summary}}
524
+ HTML
525
+ end
526
+
527
+ action :admin_alert do
528
+ send_email to: "owner@agentica.group", from: "forms@agentica.group",
529
+ subject: "New lead: {{name}} <{{email}}>",
530
+ html: "{{answers_summary}}"
531
+ run { |answers, outbox| Metrics.count(:lead, answers.to_flat_h) }
532
+ end
533
+
534
+ action :crm_push do
535
+ webhook url: "https://hooks.agentica.group/inquirex",
536
+ headers: { "X-Api-Key" => "..." }
537
+ end
538
+ end
539
+ ```
540
+
541
+ Execution and delivery:
542
+
543
+ ```ruby
544
+ answers = Inquirex::Actions.run(definition, engine.answers)
545
+ answers.outbox.messages # => [Mail::Message, ...]
546
+ answers.outbox.results # => per-action :ok / :skipped / :failed trail
547
+
548
+ # In a Rails host:
549
+ answers.outbox.each do |message|
550
+ ActionMailer::Base.wrap_delivery_behavior(message) # adopt Rails delivery config
551
+ message.deliver
552
+ end
553
+ ```
554
+
555
+ Key semantics:
556
+
557
+ - **Templating is `{{field}}` interpolation only** — dot-notation keys resolved
558
+ against `Answers#to_flat_h`, deliberately inert (no code execution), so
559
+ definitions stored in a database render safely. Values interpolated into
560
+ `html:` bodies are HTML-escaped automatically; `text:` bodies stay verbatim.
561
+ The built-in `{{answers_summary}}` expands to all collected answers.
562
+ - **`if:` gates** reuse the serializable rule AST (`not_empty(:email)`, ...).
563
+ A false rule records `:skipped` — declare no actions (or gate them) when a
564
+ flow should only save answers.
565
+ - **`run { |answers, outbox| ... }`** is the full-Ruby escape hatch; like all
566
+ lambdas it is stripped from JSON.
567
+ - **Failures are isolated**: a raising effect records `:failed` in
568
+ `outbox.results` and never blocks other actions.
569
+ - **Images** in HTML bodies must be external URLs; attachments are unsupported.
570
+ - The `mail` gem is a soft dependency, needed only when a message is built
571
+ (Rails hosts already have it via ActionMailer).
572
+ - **`webhook url:`** POSTs `{"answers": {...}}` as JSON to a static URL. The
573
+ URL's host must be covered by `allowed_domains`, declared at the top of the
574
+ definition so the flow's egress surface is auditable at a glance. The check
575
+ runs inside `Definition.new` — a JSON definition whose webhook URL was
576
+ tampered with fails at *rehydration*, before anything executes. Also
577
+ enforced: https only (plain http just for localhost), no userinfo, no
578
+ `{{field}}` templates in URLs (the destination must be static), redirects
579
+ are not followed, and non-2xx responses record `:failed`.
580
+ - Effects are extensible: `Inquirex::Actions.register(:save_record, MyEffect)`
581
+ gives a new verb both DSL and JSON wire support.
582
+
432
583
  ## Serialization
433
584
 
434
585
  Definitions support round-trip serialization:
@@ -446,6 +597,11 @@ Serialized structure includes:
446
597
  - Steps and transitions
447
598
  - Rule AST payloads
448
599
  - Widget hints
600
+ - Post-completion actions (`actions`) with their rules and effects; `run`
601
+ blocks are stripped, and an action left with no serializable effects is
602
+ omitted entirely
603
+ - The `allowed_domains` egress allowlist, re-enforced against webhook URLs
604
+ every time a definition is rehydrated
449
605
 
450
606
  Important serialization details:
451
607
 
@@ -540,15 +696,10 @@ Inquirex.define do # one entry point, all verbs available
540
696
  end
541
697
  ```
542
698
 
543
- ### Ecosystem
544
-
545
- - **`inquirex-llm`** -- LLM-powered verbs (`clarify`, `describe`, `summarize`, `detour`) for server-side AI processing
546
- - **`inquirex-tty`** -- terminal adapter using TTY Toolkit
547
- - **`inquirex-js`** -- embeddable browser widget (chat-style)
548
- - **`inquirex-rails`** -- Rails Engine for persistence, API, and asset serving
699
+ ## License
549
700
 
550
- > **Note:** `inquirex-ui` has been merged into core as of v0.2.0. Widget hints (`widget` DSL verb, `WidgetHint`, `WidgetRegistry`) are now built in.
701
+ MIT. See [`LICENSE.txt`](LICENSE.txt).
551
702
 
552
- ## License
703
+ © 2026 Konstantin Gredeskoul.
553
704
 
554
- MIT. See `LICENSE.txt`.
705
+ 0
@@ -15,7 +15,7 @@
15
15
  <g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">
16
16
  <text x="31.5" y="15" fill="#010101" fill-opacity=".3">coverage</text>
17
17
  <text x="31.5" y="14">coverage</text>
18
- <text x="80" y="15" fill="#010101" fill-opacity=".3">95%</text>
19
- <text x="80" y="14">95%</text>
18
+ <text x="80" y="15" fill="#010101" fill-opacity=".3">96%</text>
19
+ <text x="80" y="14">96%</text>
20
20
  </g>
21
21
  </svg>
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "inquirex"
6
+
7
+ DEFINITION = Inquirex.define id: "tax-intake-2025", version: "1.0.0" do
8
+ meta title: "Tax Preparation Intake", subtitle: "Let's understand your situation"
9
+ start :filing_status
10
+
11
+ ask :filing_status do
12
+ type :enum
13
+ question "What is your filing status?"
14
+ options single: "Single", married_jointly: "Married Filing Jointly"
15
+ widget target: :desktop, type: :radio_group, columns: 2
16
+ widget target: :mobile, type: :dropdown
17
+ transition to: :dependents
18
+ end
19
+
20
+ ask :dependents do
21
+ type :integer
22
+ question "How many dependents?"
23
+ default 0
24
+ transition to: :business_income
25
+ end
26
+
27
+ confirm :business_income do
28
+ question "Do you have business income?"
29
+ transition to: :business_count, if_rule: equals(:business_income, true)
30
+ transition to: :done
31
+ end
32
+
33
+ ask :business_count do
34
+ type :integer
35
+ question "How many businesses?"
36
+ transition to: :done
37
+ end
38
+
39
+ say :done do
40
+ text "Thanks for completing the intake."
41
+ end
42
+ end
43
+
44
+ engine = Inquirex::Engine.new(DEFINITION)
45
+
46
+ engine.answer("single") # filing_status
47
+ engine.answer(2) # dependents
48
+ engine.answer(false) # business_income
49
+ engine.advance # done (display step)
50
+ engine.finished? # => true
51
+
52
+ if __FILE__ == $0
53
+ require 'json'
54
+ puts JSON.pretty_generate(DEFINITION.to_h)
55
+ end
Binary file
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative '01_readme_example'
4
+ require 'inquirex'
5
+ require 'stringio'
6
+
7
+ output = StringIO.new
8
+ exporter = Inquirex::Graph::MermaidExporter.new(DEFINITION)
9
+ output << exporter.export
10
+
11
+ source = <<~HTML
12
+ <html>
13
+ <head>
14
+ <meta charset="utf-8">
15
+ </head>
16
+ <body>
17
+ <meta charset="utf-8">
18
+ <div class="mermaid">
19
+ #{output.string}
20
+ </div>
21
+ <script type="module">
22
+ import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
23
+ mermaid.initialize({ startOnLoad: true });
24
+ </script>
25
+ </html>
26
+ HTML
27
+
28
+ if __FILE__ == $0
29
+ File.write('mermaid.html', source)
30
+ system("open mermaid.html")
31
+ sleep 10
32
+
33
+ at_exit do
34
+ File.delete('mermaid.html')
35
+ end
36
+ end
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Example 03: Post-completion actions building emails into the outbox.
5
+ #
6
+ # The `action` verb runs server-side after the flow finishes. send_email
7
+ # builds Mail::Message objects — nothing is delivered here; a host Rails app
8
+ # would iterate answers.outbox and deliver via its own ActionMailer config.
9
+ #
10
+ # Run:
11
+ # bundle exec ruby examples/03_send_email_actions.rb
12
+
13
+ require "bundler/setup"
14
+ require "inquirex"
15
+
16
+ DEFINITION = Inquirex.define id: "lead-intake", version: "1.0.0" do
17
+ # Egress allowlist: webhook effects may only POST to these hosts.
18
+ allowed_domains "*.agentica.group"
19
+
20
+ meta title: "Lead Intake", subtitle: "Tell us about your project"
21
+ start :name
22
+
23
+ ask :name do
24
+ type :string
25
+ question "What is your name?"
26
+ transition to: :email
27
+ end
28
+
29
+ ask :email do
30
+ type :email
31
+ question "Where can we reach you? (leave blank to skip the receipt)"
32
+ transition to: :budget
33
+ end
34
+
35
+ ask :budget do
36
+ type :currency
37
+ question "What is your approximate budget?"
38
+ end
39
+
40
+ # Sent only when the visitor left an email address.
41
+ action :client_receipt, if: not_empty(:email) do
42
+ send_email to: "{{email}}",
43
+ from: "forms@agentica.group",
44
+ subject: "Thanks {{name}} — we got your inquiry",
45
+ text: <<~TEXT,
46
+ Hi {{name}},
47
+
48
+ We received your answers and will reply within one business day.
49
+
50
+ {{answers_summary}}
51
+ TEXT
52
+ html: <<~HTML
53
+ <p>Hi {{name}},</p>
54
+ <p>We received your answers and will reply within one business day.</p>
55
+ {{answers_summary}}
56
+ HTML
57
+ end
58
+
59
+ # Always notify the site owner, and demonstrate the Ruby escape hatch.
60
+ action :admin_alert do
61
+ send_email to: "owner@agentica.group",
62
+ from: "forms@agentica.group",
63
+ subject: "New lead: {{name}} (budget {{budget}})",
64
+ html: "{{answers_summary}}"
65
+ run { |answers, _outbox| puts ">> run{} saw budget: #{answers.budget}" }
66
+ end
67
+
68
+ # Webhook to an allowed_domains host. Gated by an always-false rule so this
69
+ # example runs offline — it records :skipped instead of POSTing. Drop the
70
+ # if: to exercise it for real.
71
+ action :crm_push, if: equals(:name, "__network_demo__") do
72
+ webhook url: "https://hooks.agentica.group/inquirex",
73
+ headers: { "X-Api-Key" => "demo" }
74
+ end
75
+ end
76
+
77
+ answers = Inquirex::Actions.run(
78
+ DEFINITION,
79
+ name: "Ada Lovelace",
80
+ email: "ada@lovelace.io",
81
+ budget: 12_500.00
82
+ )
83
+
84
+ puts "Built #{answers.outbox.size} message(s):"
85
+ answers.outbox.each do |mail|
86
+ puts "-" * 60
87
+ puts "To: #{mail.to.join(", ")}"
88
+ puts "Subject: #{mail.subject}"
89
+ puts "Parts: #{mail.multipart? ? "text + html" : mail.content_type}"
90
+ end
91
+
92
+ puts "-" * 60
93
+ puts "Results: #{answers.outbox.results.map { |r| "#{r.action_id}=#{r.status}" }.join(", ")}"
94
+
95
+ # The definition (rules and all) round-trips through JSON — run{} is stripped:
96
+ restored = Inquirex::Definition.from_json(DEFINITION.to_json)
97
+ puts "Actions after JSON round-trip: #{restored.actions.map(&:id).inspect}"
@@ -0,0 +1,33 @@
1
+ # Examples
2
+
3
+ This folder contains just a couple examples to demonstrate the API and the functionality.
4
+
5
+ For a fuller list of more comprehensive examples, please see the [examples in the `inquirex-tty` gem](https://github.com/inquirex/inquirex-tty/tree/main/examples).
6
+
7
+ ## Running Examples
8
+
9
+ These examples are meant to be run as executables from the root level of the gem folder.
10
+
11
+ ### First Example
12
+
13
+ This example should print to stdout the DSL definition in JSON:
14
+
15
+ ```bash
16
+ # First example — creates a DSL definition and prints it to stdout in JSON format
17
+ ./examples/01_readme_example.rb | jq
18
+ ```
19
+
20
+ ### Second Example
21
+
22
+ This is a more interesting example. It builds on the first, takes that definition and uses the mermaid converter to write a tiny HTML file with a mermaid diagram inside, which represents the definition defined in the first file.
23
+
24
+ ```bash
25
+ # creates a mermaid.html file at the root of the project
26
+ # opens the browser with the file and waits 10 seconds
27
+ # deletes the HTML file and exits
28
+ ./examples/02_readme_mermaid.rb
29
+ ```
30
+
31
+ The mermaid file generated should look like this:
32
+
33
+ <img src="02_readme_mermaid.png" width="300px" text-align="left"></img>
data/justfile CHANGED
@@ -31,7 +31,9 @@ test *args:
31
31
  test-coverage *args:
32
32
  export ENVIRONMENT=test; export COVERAGE=true; {{ rbenv }} rspec {{ args }}
33
33
 
34
- check-all: lint test-coverage
34
+ ci: lint test-coverage
35
+
36
+ alias check-all := ci
35
37
 
36
38
  clean:
37
39
  #!/usr/bin/env bash
@@ -59,7 +61,6 @@ doc:
59
61
  publish: build
60
62
  {{ rbenv }} rake release[remote]
61
63
 
62
-
63
64
  # Tag v{{ version }}, publish the GH release, & refresh the Homebrew tap.
64
65
  release:
65
66
  git fetch --tags