inquirex-llm 0.3.0 → 0.5.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 +97 -106
- data/docs/badges/coverage_badge.svg +2 -2
- data/justfile +47 -16
- data/lib/inquirex/llm/adapter.rb +1 -1
- data/lib/inquirex/llm/dsl/flow_builder.rb +27 -25
- data/lib/inquirex/llm/dsl/llm_step_builder.rb +11 -6
- data/lib/inquirex/llm/node.rb +9 -7
- data/lib/inquirex/llm/null_adapter.rb +3 -3
- data/lib/inquirex/llm/version.rb +1 -1
- data/lib/inquirex/llm.rb +7 -7
- metadata +8 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: db7e0d183259c896f2b1b5d9bd6ce1808f3a0c89de415a7129d2da2133a1ea49
|
|
4
|
+
data.tar.gz: dc3a1659f93182d83972dfccaaacbec820509823c293a97de4ac6df596409701
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6b8e8a8c3069ae95d904e3635d3b4947997e812d678dbf562040ae0647563692edef7364977e20ac2eb972d18d3aaeed88f606a1b05fc2d842ba201ff487b748
|
|
7
|
+
data.tar.gz: a3d8a7a89d4f22b7c3420ae1b22114895bee402314b6e4d303dfd3702f8afae1aeb87eac9a35e53090279a188b2d838c56759a8d2cecfc31179006f64d0d8fe7
|
data/README.md
CHANGED
|
@@ -1,27 +1,31 @@
|
|
|
1
|
-
[](https://github.com/inquirex/inquirex-llm/actions/workflows/main.yml)
|
|
1
|
+
[](https://badge.fury.io/rb/inquirex-llm) [](https://github.com/inquirex/inquirex-llm/actions/workflows/main.yml) 
|
|
2
2
|
|
|
3
3
|
# inquirex-llm
|
|
4
4
|
|
|
5
5
|
LLM integration verbs for the [Inquirex](https://github.com/inquirex/inquirex) questionnaire engine.
|
|
6
6
|
|
|
7
|
-
Extends the core DSL with
|
|
7
|
+
Extends the core DSL with a server-side `extract` verb (alias: `clarify`) that turns free-text answers into structured data via LLM processing. Ships with a pluggable adapter interface and a `NullAdapter` for testing. (`describe`, `summarize`, and `detour` are temporarily parked.)
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
`inquirex` is a pure Ruby, declarative, rules-driven questionnaire engine for building conditional intake forms, qualification wizards, and branching surveys.
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
11
|
+
> [!IMPORTANT]
|
|
12
|
+
>
|
|
13
|
+
> Note that `inquirex-llm` is part of an entire ecosystem that contains:
|
|
14
|
+
>
|
|
15
|
+
> - [`inquirex`](https://github.com/inquirex/inquirex)
|
|
16
|
+
> - [`inquirex-llm`](https://github.com/inquirex/inquirex-llm)
|
|
17
|
+
> - [`inquirex-tty`](https://github.com/inquirex/inquirex-)
|
|
18
|
+
> - [`inquirex-js`](https://github.com/inquirex/inquirex-js) (`npmjs` module [`@kigster/inquirex-js`](https://www.npmjs.com/package/@kigster/inquirex-js))
|
|
19
|
+
>
|
|
20
|
+
> For a presentation about these gems and what they do please watch the [RubySF presentation](https://www.youtube.com/watch?v=iaoKW7Ap3_M&t=1s) and you can also [view the slides form the presentation](https://reinvent.one/images/talks/pdfs/2026.inquirex.pdf).
|
|
21
|
+
>
|
|
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.
|
|
21
23
|
|
|
22
24
|
## Usage
|
|
23
25
|
|
|
24
|
-
`require "inquirex-llm"` injects the LLM verbs into the core `Inquirex.define` DSL.
|
|
26
|
+
`require "inquirex-llm"` injects the LLM verbs into the core `Inquirex.define` DSL.
|
|
27
|
+
|
|
28
|
+
No separate entry point needed.
|
|
25
29
|
|
|
26
30
|
```ruby
|
|
27
31
|
require "inquirex"
|
|
@@ -36,7 +40,7 @@ definition = Inquirex.define id: "tax-intake-2026", version: "1.0.0" do
|
|
|
36
40
|
transition to: :extracted
|
|
37
41
|
end
|
|
38
42
|
|
|
39
|
-
|
|
43
|
+
extract :extracted do
|
|
40
44
|
from :description
|
|
41
45
|
prompt "Extract structured business information from the description."
|
|
42
46
|
schema industry: :string,
|
|
@@ -45,15 +49,9 @@ definition = Inquirex.define id: "tax-intake-2026", version: "1.0.0" do
|
|
|
45
49
|
estimated_revenue: :currency
|
|
46
50
|
model :claude_sonnet
|
|
47
51
|
temperature 0.2
|
|
48
|
-
transition to: :summary
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
summarize :summary do
|
|
52
|
-
from_all
|
|
53
|
-
prompt "Summarize this client's tax situation and flag complexity concerns."
|
|
54
52
|
transition to: :done
|
|
55
53
|
end
|
|
56
|
-
|
|
54
|
+
|
|
57
55
|
say :done do
|
|
58
56
|
text "Thank you! We'll be in touch."
|
|
59
57
|
end
|
|
@@ -62,14 +60,14 @@ end
|
|
|
62
60
|
|
|
63
61
|
All core verbs (`ask`, `say`, `header`, `btw`, `warning`, `confirm`) and widget hints work alongside LLM verbs in the same `Inquirex.define` block.
|
|
64
62
|
|
|
65
|
-
## LLM Verbs
|
|
63
|
+
## Currently Supported LLM Verbs
|
|
66
64
|
|
|
67
|
-
### `clarify`
|
|
65
|
+
### `extract` (alias: `clarify`)
|
|
68
66
|
|
|
69
|
-
Extract structured data from a free-text answer. Requires `from
|
|
67
|
+
Extract structured data from a free-text answer. Requires `from` (or `from_all`), `prompt`, and `schema`. The stored/serialized verb is always `"extract"`; `clarify` is a DSL-only alias.
|
|
70
68
|
|
|
71
69
|
```ruby
|
|
72
|
-
|
|
70
|
+
extract :business_extracted do
|
|
73
71
|
from :business_description
|
|
74
72
|
prompt "Extract structured business information."
|
|
75
73
|
schema industry: :string, employee_count: :integer, revenue: :currency
|
|
@@ -80,57 +78,20 @@ clarify :business_extracted do
|
|
|
80
78
|
end
|
|
81
79
|
```
|
|
82
80
|
|
|
83
|
-
### `describe`
|
|
84
|
-
|
|
85
|
-
Generate natural-language text from structured data. Requires `from` and `prompt`. No schema needed.
|
|
86
|
-
|
|
87
|
-
```ruby
|
|
88
|
-
describe :business_narrative do
|
|
89
|
-
from :business_extracted
|
|
90
|
-
prompt "Write a brief narrative of this business for the intake report."
|
|
91
|
-
transition to: :next_step
|
|
92
|
-
end
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
### `summarize`
|
|
96
|
-
|
|
97
|
-
Produce a summary of all or selected answers. Use `from_all` to pass everything, or `from` to select specific steps.
|
|
98
|
-
|
|
99
|
-
```ruby
|
|
100
|
-
summarize :intake_summary do
|
|
101
|
-
from_all
|
|
102
|
-
prompt "Summarize this client's tax situation."
|
|
103
|
-
transition to: :review
|
|
104
|
-
end
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
### `detour`
|
|
108
|
-
|
|
109
|
-
Dynamically generate follow-up questions based on an answer. The server adapter handles presenting the generated questions and collecting responses. Requires `from`, `prompt`, and `schema`.
|
|
110
|
-
|
|
111
|
-
```ruby
|
|
112
|
-
detour :followup do
|
|
113
|
-
from :description
|
|
114
|
-
prompt "Generate 2-3 follow-up questions to clarify the tax situation."
|
|
115
|
-
schema questions: :array, answers: :hash
|
|
116
|
-
transition to: :next_step
|
|
117
|
-
end
|
|
118
|
-
```
|
|
119
|
-
|
|
120
81
|
## DSL Methods (inside LLM verb blocks)
|
|
121
82
|
|
|
122
|
-
| Method
|
|
123
|
-
|
|
124
|
-
| `prompt "..."`
|
|
125
|
-
| `schema key: :type, ...`
|
|
126
|
-
| `from :step_id`
|
|
127
|
-
| `from_all`
|
|
128
|
-
| `model :claude_sonnet`
|
|
129
|
-
| `temperature 0.3`
|
|
130
|
-
| `max_tokens 1024`
|
|
131
|
-
| `fallback { \|answers\| ... }` | Server-side fallback (stripped from JSON) | No
|
|
132
|
-
| `transition to: :step`
|
|
133
|
-
| `skip_if rule`
|
|
83
|
+
| Method | Purpose | Required |
|
|
84
|
+
| ------------------------------ | ----------------------------------------- | ----------------------------- |
|
|
85
|
+
| `prompt "..."` | LLM prompt template | Always |
|
|
86
|
+
| `schema key: :type, ...` | Expected output structure | `extract` |
|
|
87
|
+
| `from :step_id` | Source step(s) whose answers feed the LLM | `extract` (or use `from_all`) |
|
|
88
|
+
| `from_all` | Pass all collected answers to the LLM | Alternative to `from` |
|
|
89
|
+
| `model :claude_sonnet` | Optional model hint for the adapter | No |
|
|
90
|
+
| `temperature 0.3` | Optional sampling temperature | No |
|
|
91
|
+
| `max_tokens 1024` | Optional max output tokens | No |
|
|
92
|
+
| `fallback { \|answers\| ... }` | Server-side fallback (stripped from JSON) | No |
|
|
93
|
+
| `transition to: :step` | Conditional transition (same as core) | No |
|
|
94
|
+
| `skip_if rule` | Skip step when condition is true | No |
|
|
134
95
|
|
|
135
96
|
## Engine Integration
|
|
136
97
|
|
|
@@ -161,16 +122,14 @@ result = adapter.call(engine.current_step)
|
|
|
161
122
|
|
|
162
123
|
## Built-in Adapters
|
|
163
124
|
|
|
164
|
-
| Class
|
|
165
|
-
|
|
166
|
-
| `Inquirex::LLM::NullAdapter`
|
|
167
|
-
| `Inquirex::LLM::AnthropicAdapter`
|
|
168
|
-
| `Inquirex::LLM::OpenAIAdapter`
|
|
125
|
+
| Class | Provider | API | Auth | Key env var |
|
|
126
|
+
| --------------------------------------- | --------- | ---------------------------------- | ------------------------- | ------------------- |
|
|
127
|
+
| `Inquirex::LLM::NullAdapter` | — | none (placeholders) | none | — |
|
|
128
|
+
| `Inquirex::LLM::AnthropicAdapter` | Anthropic | `/v1/messages` | `x-api-key` header | `ANTHROPIC_API_KEY` |
|
|
129
|
+
| `Inquirex::LLM::OpenAIAdapter` | OpenAI | `/v1/chat/completions` (JSON mode) | `Authorization: Bearer …` | `OPENAI_API_KEY` |
|
|
130
|
+
| `Inquirex::LLM::LittleLLMAdapter` (TBD) | Any | OpenAI Compatible API | OpenAI Compatible Auth | Provider Specific |
|
|
169
131
|
|
|
170
|
-
|
|
171
|
-
declared `schema` into the system prompt as a strict JSON contract, and raise
|
|
172
|
-
`Inquirex::LLM::Errors::AdapterError` on HTTP / parse failures and
|
|
173
|
-
`SchemaViolationError` when the model's output is missing declared fields.
|
|
132
|
+
The Anthropic and OpenAI adapters use `net/http` (stdlib, no extra dependency), inject the declared `schema` into the system prompt as a strict JSON contract, and raise `Inquirex::LLM::Errors::AdapterError` on HTTP / parse failures and `SchemaViolationError` when the model's output is missing declared fields.
|
|
174
133
|
|
|
175
134
|
### AnthropicAdapter
|
|
176
135
|
|
|
@@ -181,8 +140,7 @@ adapter = Inquirex::LLM::AnthropicAdapter.new(
|
|
|
181
140
|
)
|
|
182
141
|
```
|
|
183
142
|
|
|
184
|
-
Recognized `model :symbol` values in the DSL: `:claude_sonnet`,
|
|
185
|
-
`:claude_haiku`, `:claude_opus` (mapped to the current concrete model ids).
|
|
143
|
+
Recognized `model :symbol` values in the DSL: `:claude_sonnet`, `:claude_haiku`, `:claude_opus` (mapped to the current concrete model ids).
|
|
186
144
|
|
|
187
145
|
### OpenAIAdapter
|
|
188
146
|
|
|
@@ -193,18 +151,11 @@ adapter = Inquirex::LLM::OpenAIAdapter.new(
|
|
|
193
151
|
)
|
|
194
152
|
```
|
|
195
153
|
|
|
196
|
-
Uses Chat Completions with `response_format: { type: "json_object" }` so the
|
|
197
|
-
model is constrained to return valid JSON. Recognized DSL symbols: `:gpt_4o`,
|
|
198
|
-
`:gpt_4o_mini`, `:gpt_4_1`, `:gpt_4_1_mini`. For cross-provider portability,
|
|
199
|
-
the adapter also accepts the Claude symbols (`:claude_sonnet` → `gpt-4o` etc.)
|
|
200
|
-
so a flow file that says `model :claude_sonnet` runs unchanged against either
|
|
201
|
-
provider.
|
|
154
|
+
Uses Chat Completions with `response_format: { type: "json_object" }` so the model is constrained to return valid JSON. Recognized DSL symbols: `:gpt_4o`, `:gpt_4o_mini`, `:gpt_4_1`, `:gpt_4_1_mini`. For cross-provider portability, the adapter also accepts the Claude symbols (`:claude_sonnet` → `gpt-4o` etc.) so a flow file that says `model :claude_sonnet` runs unchanged against either provider.
|
|
202
155
|
|
|
203
156
|
## LLM-assisted Pre-fill Pattern
|
|
204
157
|
|
|
205
|
-
A common use case: ask *one* open-ended question, let the LLM extract answers
|
|
206
|
-
for *many* downstream questions, and only prompt the user for what the LLM
|
|
207
|
-
couldn't determine. This is what the core engine's `Engine#prefill!` is for:
|
|
158
|
+
A common use case: ask *one* open-ended question, let the LLM extract answers for *many* downstream questions, and only prompt the user for what the LLM couldn't determine. This is what the core engine's `Engine#prefill!` is for:
|
|
208
159
|
|
|
209
160
|
```ruby
|
|
210
161
|
definition = Inquirex.define id: "tax-intake" do
|
|
@@ -216,7 +167,7 @@ definition = Inquirex.define id: "tax-intake" do
|
|
|
216
167
|
transition to: :extracted
|
|
217
168
|
end
|
|
218
169
|
|
|
219
|
-
|
|
170
|
+
extract :extracted do
|
|
220
171
|
from :describe
|
|
221
172
|
prompt "Extract: filing_status, dependents, income_types, state_filing."
|
|
222
173
|
schema filing_status: :string,
|
|
@@ -241,7 +192,7 @@ definition = Inquirex.define id: "tax-intake" do
|
|
|
241
192
|
skip_if not_empty(:dependents)
|
|
242
193
|
transition to: :income_types
|
|
243
194
|
end
|
|
244
|
-
# …and so on for every field in the
|
|
195
|
+
# …and so on for every field in the extract schema
|
|
245
196
|
end
|
|
246
197
|
|
|
247
198
|
engine = Inquirex::Engine.new(definition)
|
|
@@ -257,12 +208,7 @@ engine.prefill!(result) # splats into top-level answers
|
|
|
257
208
|
# whichever field the LLM couldn't fill in.
|
|
258
209
|
```
|
|
259
210
|
|
|
260
|
-
`Engine#prefill!` is non-destructive (won't clobber an answer the user already
|
|
261
|
-
gave), ignores `nil`/empty values so they don't spuriously trigger
|
|
262
|
-
`not_empty`, and auto-advances past any step whose `skip_if` now evaluates
|
|
263
|
-
true. See [examples/09_tax_preparer_llm.rb](../inquirex-tty/examples/09_tax_preparer_llm.rb)
|
|
264
|
-
for a complete runnable flow, or the repo-level `demo_llm_intake.rb` for a
|
|
265
|
-
scripted end-to-end walkthrough.
|
|
211
|
+
`Engine#prefill!` is non-destructive (won't clobber an answer the user already gave), ignores `nil`/empty values so they don't spuriously trigger `not_empty`, and auto-advances past any step whose `skip_if` now evaluates true. See [examples/09_tax_preparer_llm.rb](../inquirex-tty/examples/09_tax_preparer_llm.rb) for a complete runnable flow, or the repo-level `demo_llm_intake.rb` for a scripted end-to-end walkthrough.
|
|
266
212
|
|
|
267
213
|
## JSON Serialization
|
|
268
214
|
|
|
@@ -270,9 +216,9 @@ LLM steps serialize with `"requires_server": true` so the JS widget knows to rou
|
|
|
270
216
|
|
|
271
217
|
```json
|
|
272
218
|
{
|
|
273
|
-
"verb": "
|
|
219
|
+
"verb": "extract",
|
|
274
220
|
"requires_server": true,
|
|
275
|
-
"transitions": [{ "to": "
|
|
221
|
+
"transitions": [{ "to": "next_step", "requires_server": true }],
|
|
276
222
|
"llm": {
|
|
277
223
|
"prompt": "Extract structured business information.",
|
|
278
224
|
"schema": {
|
|
@@ -313,6 +259,47 @@ end
|
|
|
313
259
|
|
|
314
260
|
The base class provides `#source_answers` (gathers relevant answers) and `#validate_output!` (checks schema conformance).
|
|
315
261
|
|
|
262
|
+
## Future Possible LLM Verbs
|
|
263
|
+
|
|
264
|
+
### `describe`
|
|
265
|
+
|
|
266
|
+
Generate natural-language text from structured data. Requires `from` and `prompt`. No schema needed.
|
|
267
|
+
|
|
268
|
+
```ruby
|
|
269
|
+
describe :business_narrative do
|
|
270
|
+
from :business_extracted
|
|
271
|
+
prompt "Write a brief narrative of this business for the intake report."
|
|
272
|
+
transition to: :next_step
|
|
273
|
+
end
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### `summarize`
|
|
277
|
+
|
|
278
|
+
Produce a summary of all or selected answers. Use `from_all` to pass everything, or `from` to select specific steps.
|
|
279
|
+
|
|
280
|
+
```ruby
|
|
281
|
+
summarize :intake_summary do
|
|
282
|
+
from_all
|
|
283
|
+
prompt "Summarize this client's tax situation."
|
|
284
|
+
transition to: :review
|
|
285
|
+
end
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### `detour` (parked)
|
|
289
|
+
|
|
290
|
+
Dynamically generate follow-up questions based on an answer. The server adapter handles presenting the generated questions and collecting responses. Requires `from`, `prompt`, and `schema`.
|
|
291
|
+
|
|
292
|
+
```ruby
|
|
293
|
+
detour :followup do
|
|
294
|
+
from :description
|
|
295
|
+
prompt "Generate 2-3 follow-up questions to clarify the tax situation."
|
|
296
|
+
schema questions: :array, answers: :hash
|
|
297
|
+
transition to: :next_step
|
|
298
|
+
end
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
##
|
|
302
|
+
|
|
316
303
|
## Development
|
|
317
304
|
|
|
318
305
|
```bash
|
|
@@ -323,4 +310,8 @@ bundle exec rubocop
|
|
|
323
310
|
|
|
324
311
|
## License
|
|
325
312
|
|
|
326
|
-
|
|
313
|
+
© 2026 Konstantin Gredeskoul.
|
|
314
|
+
|
|
315
|
+
Distributed under the MIT License.
|
|
316
|
+
|
|
317
|
+
See [LICENSE.txt](LICENSE.txt) for details.
|
|
@@ -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">
|
|
19
|
-
<text x="80" y="14">
|
|
18
|
+
<text x="80" y="15" fill="#010101" fill-opacity=".3">98%</text>
|
|
19
|
+
<text x="80" y="14">98%</text>
|
|
20
20
|
</g>
|
|
21
21
|
</svg>
|
data/justfile
CHANGED
|
@@ -1,38 +1,69 @@
|
|
|
1
|
-
|
|
1
|
+
# Tell 'just' to run bash, source our setup script, then execute the recipe
|
|
2
|
+
set shell := ["bash", "-c"]
|
|
2
3
|
|
|
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'
|
|
4
7
|
|
|
5
8
|
[no-exit-message]
|
|
6
9
|
recipes:
|
|
7
|
-
|
|
10
|
+
just --choose
|
|
8
11
|
|
|
9
12
|
# Sync all dependencies
|
|
10
13
|
install:
|
|
11
|
-
|
|
14
|
+
bin/setup
|
|
12
15
|
|
|
13
|
-
|
|
14
|
-
lint-fix *args:
|
|
15
|
-
{{rbenv}} && bundle exec rubocop -a
|
|
16
|
-
|
|
17
|
-
alias format := lint-fix
|
|
16
|
+
build: install
|
|
18
17
|
|
|
19
18
|
# Lint and reformat files
|
|
20
19
|
lint:
|
|
21
|
-
{{rbenv}}
|
|
20
|
+
{{ rbenv }} rubocop
|
|
21
|
+
|
|
22
|
+
# Lint and reformat files (-a) — pass -A as an argument
|
|
23
|
+
format *args:
|
|
24
|
+
{{ rbenv }} rubocop -a {{ args }}
|
|
22
25
|
|
|
23
26
|
# Run all the tests
|
|
24
27
|
test *args:
|
|
25
|
-
|
|
28
|
+
export ENVIRONMENT=test; {{ rbenv }} rspec {{args}}
|
|
26
29
|
|
|
27
30
|
# Run tests with coverage
|
|
28
31
|
test-coverage *args:
|
|
29
|
-
ENVIRONMENT=test COVERAGE=true
|
|
32
|
+
export ENVIRONMENT=test; export COVERAGE=true; {{ rbenv }} rspec {{ args }}
|
|
33
|
+
|
|
34
|
+
check-all: install lint test-coverage
|
|
30
35
|
|
|
31
36
|
clean:
|
|
32
37
|
#!/usr/bin/env bash
|
|
33
|
-
find . -name .DS_Store -delete -print || true
|
|
34
|
-
rm -rf tmp/*
|
|
38
|
+
@find . -name .DS_Store -delete -print || true
|
|
39
|
+
@rm -rf tmp/*
|
|
35
40
|
|
|
36
41
|
# Run all lefthook pre-commit hooks
|
|
37
|
-
|
|
38
|
-
{{rbenv}}
|
|
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/lib/inquirex/llm/adapter.rb
CHANGED
|
@@ -28,7 +28,7 @@ module Inquirex
|
|
|
28
28
|
#
|
|
29
29
|
# @param node [LLM::Node] the LLM step to process
|
|
30
30
|
# @param answers [Hash] current collected answers
|
|
31
|
-
# @return [Hash, String] structured output (for
|
|
31
|
+
# @return [Hash, String] structured output (for extract) or text (when no schema)
|
|
32
32
|
# @raise [Errors::AdapterError] if the LLM call fails
|
|
33
33
|
# @raise [Errors::SchemaViolationError] if output doesn't match schema
|
|
34
34
|
def call(node, answers)
|
|
@@ -5,7 +5,7 @@ module Inquirex
|
|
|
5
5
|
module DSL
|
|
6
6
|
# Mixin that adds LLM verb methods to Inquirex::DSL::FlowBuilder.
|
|
7
7
|
# Included automatically when `require "inquirex-llm"` is called,
|
|
8
|
-
# so that `Inquirex.define` gains clarify
|
|
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
|
|
@@ -15,34 +15,36 @@ module Inquirex
|
|
|
15
15
|
# structured data matching the declared schema.
|
|
16
16
|
#
|
|
17
17
|
# @param id [Symbol] step id
|
|
18
|
-
def
|
|
19
|
-
add_llm_step(id, :
|
|
18
|
+
def extract(id, &)
|
|
19
|
+
add_llm_step(id, :extract, &)
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
# natural-language text.
|
|
24
|
-
#
|
|
25
|
-
# @param id [Symbol] step id
|
|
26
|
-
def describe(id, &)
|
|
27
|
-
add_llm_step(id, :describe, &)
|
|
28
|
-
end
|
|
22
|
+
alias clarify extract
|
|
29
23
|
|
|
30
|
-
# Defines an LLM
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
# @param id [Symbol] step id
|
|
34
|
-
def
|
|
35
|
-
|
|
36
|
-
end
|
|
24
|
+
# # Defines an LLM description step: takes structured data and produces
|
|
25
|
+
# # natural-language text.
|
|
26
|
+
# #
|
|
27
|
+
# # @param id [Symbol] step id
|
|
28
|
+
# def describe(id, &)
|
|
29
|
+
# add_llm_step(id, :describe, &)
|
|
30
|
+
# end
|
|
37
31
|
|
|
38
|
-
# Defines an LLM
|
|
39
|
-
#
|
|
40
|
-
#
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
32
|
+
# # Defines an LLM summarization step: takes all or selected answers and
|
|
33
|
+
# # produces a textual summary.
|
|
34
|
+
# #
|
|
35
|
+
# # @param id [Symbol] step id
|
|
36
|
+
# def summarize(id, &)
|
|
37
|
+
# add_llm_step(id, :summarize, &)
|
|
38
|
+
# end
|
|
39
|
+
|
|
40
|
+
# # Defines an LLM detour step: based on an answer, dynamically generates
|
|
41
|
+
# # follow-up questions. The server adapter handles presenting the generated
|
|
42
|
+
# # questions and collecting responses.
|
|
43
|
+
# #
|
|
44
|
+
# # @param id [Symbol] step id
|
|
45
|
+
# def detour(id, &)
|
|
46
|
+
# add_llm_step(id, :detour, &)
|
|
47
|
+
# end
|
|
46
48
|
|
|
47
49
|
private
|
|
48
50
|
|
|
@@ -8,7 +8,7 @@ module Inquirex
|
|
|
8
8
|
# while inheriting transition and skip_if from the core StepBuilder.
|
|
9
9
|
#
|
|
10
10
|
# @example
|
|
11
|
-
#
|
|
11
|
+
# extract :business_extracted do
|
|
12
12
|
# from :business_description
|
|
13
13
|
# prompt "Extract structured business info."
|
|
14
14
|
# schema industry: :string, employee_count: :integer
|
|
@@ -112,7 +112,7 @@ module Inquirex
|
|
|
112
112
|
@skip_if = rule
|
|
113
113
|
end
|
|
114
114
|
|
|
115
|
-
# Optional display text (
|
|
115
|
+
# Optional display text (user-visible label for the step).
|
|
116
116
|
#
|
|
117
117
|
# @param content [String]
|
|
118
118
|
def question(content)
|
|
@@ -159,15 +159,20 @@ module Inquirex
|
|
|
159
159
|
def validate!(id)
|
|
160
160
|
raise Errors::DefinitionError, "LLM step #{id.inspect} requires a prompt" if @prompt.nil?
|
|
161
161
|
|
|
162
|
-
|
|
162
|
+
# Schema required for extract (and formerly detour).
|
|
163
|
+
if %i[extract].include?(@verb) && @schema_fields.empty?
|
|
164
|
+
# if %i[extract detour].include?(@verb) && @schema_fields.empty?
|
|
163
165
|
raise Errors::DefinitionError,
|
|
164
166
|
"LLM step #{id.inspect} (#{@verb}) requires a schema"
|
|
165
167
|
end
|
|
166
168
|
|
|
167
|
-
return unless @from_steps.empty? && !@from_all
|
|
169
|
+
return unless @from_steps.empty? && !@from_all
|
|
170
|
+
# return unless @from_steps.empty? && !@from_all && @verb != :summarize
|
|
168
171
|
|
|
169
|
-
#
|
|
170
|
-
return unless %i[
|
|
172
|
+
# extract (and formerly describe/detour) should reference source steps or from_all
|
|
173
|
+
return unless %i[extract].include?(@verb)
|
|
174
|
+
|
|
175
|
+
# return unless %i[extract describe detour].include?(@verb)
|
|
171
176
|
|
|
172
177
|
raise Errors::DefinitionError,
|
|
173
178
|
"LLM step #{id.inspect} (#{@verb}) requires `from` or `from_all`"
|
data/lib/inquirex/llm/node.rb
CHANGED
|
@@ -7,16 +7,16 @@ module Inquirex
|
|
|
7
7
|
# step references, and model configuration.
|
|
8
8
|
#
|
|
9
9
|
# LLM verbs:
|
|
10
|
-
# :
|
|
11
|
-
# :describe — generate natural-language text from structured data
|
|
12
|
-
# :summarize — produce a summary of all or selected answers
|
|
13
|
-
# :detour — dynamically generate follow-up questions based on an answer
|
|
10
|
+
# :extract — extract structured data from a free-text answer
|
|
11
|
+
# # :describe — generate natural-language text from structured data
|
|
12
|
+
# # :summarize — produce a summary of all or selected answers
|
|
13
|
+
# # :detour — dynamically generate follow-up questions based on an answer
|
|
14
14
|
#
|
|
15
15
|
# All LLM nodes are collecting (they produce answers) and require server
|
|
16
16
|
# round-trips. The frontend shows a "thinking" state while the server processes.
|
|
17
17
|
#
|
|
18
18
|
# @attr_reader prompt [String] LLM prompt template
|
|
19
|
-
# @attr_reader schema [Schema, nil] expected output structure (required for
|
|
19
|
+
# @attr_reader schema [Schema, nil] expected output structure (required for extract)
|
|
20
20
|
# @attr_reader from_steps [Array<Symbol>] source step ids whose answers feed the LLM
|
|
21
21
|
# @attr_reader from_all [Boolean] whether to pass all collected answers to the LLM
|
|
22
22
|
# @attr_reader model [Symbol, nil] optional model hint (e.g. :claude_sonnet)
|
|
@@ -24,7 +24,8 @@ module Inquirex
|
|
|
24
24
|
# @attr_reader max_tokens [Integer, nil] optional max output tokens
|
|
25
25
|
# @attr_reader fallback [Proc, nil] server-side fallback (stripped from JSON)
|
|
26
26
|
class Node < Inquirex::Node
|
|
27
|
-
LLM_VERBS = %i[
|
|
27
|
+
LLM_VERBS = %i[extract].freeze
|
|
28
|
+
# LLM_VERBS = %i[extract describe summarize detour].freeze
|
|
28
29
|
|
|
29
30
|
attr_reader :prompt,
|
|
30
31
|
:schema,
|
|
@@ -89,7 +90,8 @@ module Inquirex
|
|
|
89
90
|
# @param hash [Hash]
|
|
90
91
|
# @return [LLM::Node]
|
|
91
92
|
def self.from_h(id, hash)
|
|
92
|
-
verb = hash["verb"]
|
|
93
|
+
verb = hash["verb"] || hash[:verb]
|
|
94
|
+
verb = :extract if verb.to_s == "clarify" # DSL alias; normalize wire format
|
|
93
95
|
question = hash["question"] || hash[:question]
|
|
94
96
|
text = hash["text"] || hash[:text]
|
|
95
97
|
transitions_data = hash["transitions"] || hash[:transitions] || []
|
|
@@ -5,13 +5,13 @@ module Inquirex
|
|
|
5
5
|
# Test adapter that returns schema-conformant placeholder values without
|
|
6
6
|
# calling any LLM API. Useful for testing flows that include LLM steps.
|
|
7
7
|
#
|
|
8
|
-
# For
|
|
9
|
-
# matching each field's declared type.
|
|
8
|
+
# For extract steps with a schema, returns a hash of default values
|
|
9
|
+
# matching each field's declared type. Without a schema, returns
|
|
10
10
|
# a placeholder string.
|
|
11
11
|
#
|
|
12
12
|
# @example
|
|
13
13
|
# adapter = Inquirex::LLM::NullAdapter.new
|
|
14
|
-
# result = adapter.call(
|
|
14
|
+
# result = adapter.call(extract_node, answers)
|
|
15
15
|
# # => { industry: "", employee_count: 0, ... }
|
|
16
16
|
class NullAdapter < Adapter
|
|
17
17
|
TYPE_DEFAULTS = {
|
data/lib/inquirex/llm/version.rb
CHANGED
data/lib/inquirex/llm.rb
CHANGED
|
@@ -17,11 +17,11 @@ require_relative "llm/dsl/flow_builder"
|
|
|
17
17
|
module Inquirex
|
|
18
18
|
# LLM integration layer for Inquirex flows.
|
|
19
19
|
#
|
|
20
|
-
# Extends the core DSL with
|
|
21
|
-
# -
|
|
22
|
-
# - describe — generate natural-language text from structured data
|
|
23
|
-
# - summarize — produce a summary of all or selected answers
|
|
24
|
-
# - detour — dynamically generate follow-up questions
|
|
20
|
+
# Extends the core DSL with LLM-powered verbs that run server-side:
|
|
21
|
+
# - extract — extract structured data from free-text answers (`clarify` is an alias)
|
|
22
|
+
# # - describe — generate natural-language text from structured data
|
|
23
|
+
# # - summarize — produce a summary of all or selected answers
|
|
24
|
+
# # - detour — dynamically generate follow-up questions
|
|
25
25
|
#
|
|
26
26
|
# LLM calls never happen on the frontend. Steps are marked `requires_server: true`
|
|
27
27
|
# in the JSON wire format so the JS widget knows to round-trip to the server.
|
|
@@ -33,7 +33,7 @@ module Inquirex
|
|
|
33
33
|
# Inquirex.define id: "intake" do
|
|
34
34
|
# start :description
|
|
35
35
|
# ask(:description) { type :text; question "Describe your business."; transition to: :extracted }
|
|
36
|
-
#
|
|
36
|
+
# extract(:extracted) { from :description; prompt "Extract info."; schema name: :string; transition to: :done }
|
|
37
37
|
# say(:done) { text "Done!" }
|
|
38
38
|
# end
|
|
39
39
|
module LLM
|
|
@@ -41,5 +41,5 @@ module Inquirex
|
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
# Inject LLM verbs into the core FlowBuilder so that Inquirex.define
|
|
44
|
-
# gains clarify
|
|
44
|
+
# gains extract (and clarify as an alias) when this gem is loaded.
|
|
45
45
|
Inquirex::DSL::FlowBuilder.include(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.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Konstantin Gredeskoul
|
|
@@ -15,18 +15,18 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - "~>"
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: '0.
|
|
18
|
+
version: '0.4'
|
|
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.
|
|
26
|
-
description: Extends the Inquirex DSL with
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
version: '0.4'
|
|
26
|
+
description: 'Extends the Inquirex DSL with an LLM-powered `extract` verb (alias:
|
|
27
|
+
`clarify`) that runs server-side to turn free-text answers into structured data.
|
|
28
|
+
Pluggable adapter interface keeps the gem LLM-agnostic; a NullAdapter ships for
|
|
29
|
+
testing.'
|
|
30
30
|
email:
|
|
31
31
|
- kigster@gmail.com
|
|
32
32
|
executables:
|
|
@@ -81,7 +81,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
81
81
|
- !ruby/object:Gem::Version
|
|
82
82
|
version: '0'
|
|
83
83
|
requirements: []
|
|
84
|
-
rubygems_version: 4.0.
|
|
84
|
+
rubygems_version: 4.0.16
|
|
85
85
|
specification_version: 4
|
|
86
86
|
summary: LLM integration verbs for the Inquirex questionnaire engine
|
|
87
87
|
test_files: []
|