inquirex-llm 0.4.1 → 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 +89 -113
- data/docs/badges/coverage_badge.svg +2 -2
- 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 +5 -5
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,42 +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
11
|
> [!IMPORTANT]
|
|
12
12
|
>
|
|
13
|
-
> Note that `
|
|
14
|
-
>
|
|
15
|
-
> `nquirex` is the base gem of the ecosystem that contains:
|
|
13
|
+
> Note that `inquirex-llm` is part of an entire ecosystem that contains:
|
|
16
14
|
>
|
|
17
15
|
> - [`inquirex`](https://github.com/inquirex/inquirex)
|
|
18
16
|
> - [`inquirex-llm`](https://github.com/inquirex/inquirex-llm)
|
|
19
17
|
> - [`inquirex-tty`](https://github.com/inquirex/inquirex-)
|
|
20
|
-
> - [`inquirex-js`](https://github.com/inquirex/inquirex-js)
|
|
18
|
+
> - [`inquirex-js`](https://github.com/inquirex/inquirex-js) (`npmjs` module [`@kigster/inquirex-js`](https://www.npmjs.com/package/@kigster/inquirex-js))
|
|
21
19
|
>
|
|
22
|
-
> For a presentation
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
- Version: `0.1.0`
|
|
27
|
-
- Ruby: `>= 4.0.0`
|
|
28
|
-
- Test suite: `111 examples, 0 failures`
|
|
29
|
-
- Depends on: `inquirex` (core gem)
|
|
30
|
-
|
|
31
|
-
## Installation
|
|
32
|
-
|
|
33
|
-
```ruby
|
|
34
|
-
gem "inquirex-llm"
|
|
35
|
-
```
|
|
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.
|
|
36
23
|
|
|
37
24
|
## Usage
|
|
38
25
|
|
|
39
|
-
`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.
|
|
40
29
|
|
|
41
30
|
```ruby
|
|
42
31
|
require "inquirex"
|
|
@@ -51,7 +40,7 @@ definition = Inquirex.define id: "tax-intake-2026", version: "1.0.0" do
|
|
|
51
40
|
transition to: :extracted
|
|
52
41
|
end
|
|
53
42
|
|
|
54
|
-
|
|
43
|
+
extract :extracted do
|
|
55
44
|
from :description
|
|
56
45
|
prompt "Extract structured business information from the description."
|
|
57
46
|
schema industry: :string,
|
|
@@ -60,15 +49,9 @@ definition = Inquirex.define id: "tax-intake-2026", version: "1.0.0" do
|
|
|
60
49
|
estimated_revenue: :currency
|
|
61
50
|
model :claude_sonnet
|
|
62
51
|
temperature 0.2
|
|
63
|
-
transition to: :summary
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
summarize :summary do
|
|
67
|
-
from_all
|
|
68
|
-
prompt "Summarize this client's tax situation and flag complexity concerns."
|
|
69
52
|
transition to: :done
|
|
70
53
|
end
|
|
71
|
-
|
|
54
|
+
|
|
72
55
|
say :done do
|
|
73
56
|
text "Thank you! We'll be in touch."
|
|
74
57
|
end
|
|
@@ -77,14 +60,14 @@ end
|
|
|
77
60
|
|
|
78
61
|
All core verbs (`ask`, `say`, `header`, `btw`, `warning`, `confirm`) and widget hints work alongside LLM verbs in the same `Inquirex.define` block.
|
|
79
62
|
|
|
80
|
-
## LLM Verbs
|
|
63
|
+
## Currently Supported LLM Verbs
|
|
81
64
|
|
|
82
|
-
### `clarify`
|
|
65
|
+
### `extract` (alias: `clarify`)
|
|
83
66
|
|
|
84
|
-
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.
|
|
85
68
|
|
|
86
69
|
```ruby
|
|
87
|
-
|
|
70
|
+
extract :business_extracted do
|
|
88
71
|
from :business_description
|
|
89
72
|
prompt "Extract structured business information."
|
|
90
73
|
schema industry: :string, employee_count: :integer, revenue: :currency
|
|
@@ -95,57 +78,20 @@ clarify :business_extracted do
|
|
|
95
78
|
end
|
|
96
79
|
```
|
|
97
80
|
|
|
98
|
-
### `describe`
|
|
99
|
-
|
|
100
|
-
Generate natural-language text from structured data. Requires `from` and `prompt`. No schema needed.
|
|
101
|
-
|
|
102
|
-
```ruby
|
|
103
|
-
describe :business_narrative do
|
|
104
|
-
from :business_extracted
|
|
105
|
-
prompt "Write a brief narrative of this business for the intake report."
|
|
106
|
-
transition to: :next_step
|
|
107
|
-
end
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
### `summarize`
|
|
111
|
-
|
|
112
|
-
Produce a summary of all or selected answers. Use `from_all` to pass everything, or `from` to select specific steps.
|
|
113
|
-
|
|
114
|
-
```ruby
|
|
115
|
-
summarize :intake_summary do
|
|
116
|
-
from_all
|
|
117
|
-
prompt "Summarize this client's tax situation."
|
|
118
|
-
transition to: :review
|
|
119
|
-
end
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
### `detour`
|
|
123
|
-
|
|
124
|
-
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`.
|
|
125
|
-
|
|
126
|
-
```ruby
|
|
127
|
-
detour :followup do
|
|
128
|
-
from :description
|
|
129
|
-
prompt "Generate 2-3 follow-up questions to clarify the tax situation."
|
|
130
|
-
schema questions: :array, answers: :hash
|
|
131
|
-
transition to: :next_step
|
|
132
|
-
end
|
|
133
|
-
```
|
|
134
|
-
|
|
135
81
|
## DSL Methods (inside LLM verb blocks)
|
|
136
82
|
|
|
137
|
-
| Method
|
|
138
|
-
|
|
139
|
-
| `prompt "..."`
|
|
140
|
-
| `schema key: :type, ...`
|
|
141
|
-
| `from :step_id`
|
|
142
|
-
| `from_all`
|
|
143
|
-
| `model :claude_sonnet`
|
|
144
|
-
| `temperature 0.3`
|
|
145
|
-
| `max_tokens 1024`
|
|
146
|
-
| `fallback { \|answers\| ... }` | Server-side fallback (stripped from JSON) | No
|
|
147
|
-
| `transition to: :step`
|
|
148
|
-
| `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 |
|
|
149
95
|
|
|
150
96
|
## Engine Integration
|
|
151
97
|
|
|
@@ -176,16 +122,14 @@ result = adapter.call(engine.current_step)
|
|
|
176
122
|
|
|
177
123
|
## Built-in Adapters
|
|
178
124
|
|
|
179
|
-
| Class
|
|
180
|
-
|
|
181
|
-
| `Inquirex::LLM::NullAdapter`
|
|
182
|
-
| `Inquirex::LLM::AnthropicAdapter`
|
|
183
|
-
| `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 |
|
|
184
131
|
|
|
185
|
-
|
|
186
|
-
declared `schema` into the system prompt as a strict JSON contract, and raise
|
|
187
|
-
`Inquirex::LLM::Errors::AdapterError` on HTTP / parse failures and
|
|
188
|
-
`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.
|
|
189
133
|
|
|
190
134
|
### AnthropicAdapter
|
|
191
135
|
|
|
@@ -196,8 +140,7 @@ adapter = Inquirex::LLM::AnthropicAdapter.new(
|
|
|
196
140
|
)
|
|
197
141
|
```
|
|
198
142
|
|
|
199
|
-
Recognized `model :symbol` values in the DSL: `:claude_sonnet`,
|
|
200
|
-
`: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).
|
|
201
144
|
|
|
202
145
|
### OpenAIAdapter
|
|
203
146
|
|
|
@@ -208,18 +151,11 @@ adapter = Inquirex::LLM::OpenAIAdapter.new(
|
|
|
208
151
|
)
|
|
209
152
|
```
|
|
210
153
|
|
|
211
|
-
Uses Chat Completions with `response_format: { type: "json_object" }` so the
|
|
212
|
-
model is constrained to return valid JSON. Recognized DSL symbols: `:gpt_4o`,
|
|
213
|
-
`:gpt_4o_mini`, `:gpt_4_1`, `:gpt_4_1_mini`. For cross-provider portability,
|
|
214
|
-
the adapter also accepts the Claude symbols (`:claude_sonnet` → `gpt-4o` etc.)
|
|
215
|
-
so a flow file that says `model :claude_sonnet` runs unchanged against either
|
|
216
|
-
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.
|
|
217
155
|
|
|
218
156
|
## LLM-assisted Pre-fill Pattern
|
|
219
157
|
|
|
220
|
-
A common use case: ask *one* open-ended question, let the LLM extract answers
|
|
221
|
-
for *many* downstream questions, and only prompt the user for what the LLM
|
|
222
|
-
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:
|
|
223
159
|
|
|
224
160
|
```ruby
|
|
225
161
|
definition = Inquirex.define id: "tax-intake" do
|
|
@@ -231,7 +167,7 @@ definition = Inquirex.define id: "tax-intake" do
|
|
|
231
167
|
transition to: :extracted
|
|
232
168
|
end
|
|
233
169
|
|
|
234
|
-
|
|
170
|
+
extract :extracted do
|
|
235
171
|
from :describe
|
|
236
172
|
prompt "Extract: filing_status, dependents, income_types, state_filing."
|
|
237
173
|
schema filing_status: :string,
|
|
@@ -256,7 +192,7 @@ definition = Inquirex.define id: "tax-intake" do
|
|
|
256
192
|
skip_if not_empty(:dependents)
|
|
257
193
|
transition to: :income_types
|
|
258
194
|
end
|
|
259
|
-
# …and so on for every field in the
|
|
195
|
+
# …and so on for every field in the extract schema
|
|
260
196
|
end
|
|
261
197
|
|
|
262
198
|
engine = Inquirex::Engine.new(definition)
|
|
@@ -272,12 +208,7 @@ engine.prefill!(result) # splats into top-level answers
|
|
|
272
208
|
# whichever field the LLM couldn't fill in.
|
|
273
209
|
```
|
|
274
210
|
|
|
275
|
-
`Engine#prefill!` is non-destructive (won't clobber an answer the user already
|
|
276
|
-
gave), ignores `nil`/empty values so they don't spuriously trigger
|
|
277
|
-
`not_empty`, and auto-advances past any step whose `skip_if` now evaluates
|
|
278
|
-
true. See [examples/09_tax_preparer_llm.rb](../inquirex-tty/examples/09_tax_preparer_llm.rb)
|
|
279
|
-
for a complete runnable flow, or the repo-level `demo_llm_intake.rb` for a
|
|
280
|
-
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.
|
|
281
212
|
|
|
282
213
|
## JSON Serialization
|
|
283
214
|
|
|
@@ -285,9 +216,9 @@ LLM steps serialize with `"requires_server": true` so the JS widget knows to rou
|
|
|
285
216
|
|
|
286
217
|
```json
|
|
287
218
|
{
|
|
288
|
-
"verb": "
|
|
219
|
+
"verb": "extract",
|
|
289
220
|
"requires_server": true,
|
|
290
|
-
"transitions": [{ "to": "
|
|
221
|
+
"transitions": [{ "to": "next_step", "requires_server": true }],
|
|
291
222
|
"llm": {
|
|
292
223
|
"prompt": "Extract structured business information.",
|
|
293
224
|
"schema": {
|
|
@@ -328,6 +259,47 @@ end
|
|
|
328
259
|
|
|
329
260
|
The base class provides `#source_answers` (gathers relevant answers) and `#validate_output!` (checks schema conformance).
|
|
330
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
|
+
|
|
331
303
|
## Development
|
|
332
304
|
|
|
333
305
|
```bash
|
|
@@ -338,4 +310,8 @@ bundle exec rubocop
|
|
|
338
310
|
|
|
339
311
|
## License
|
|
340
312
|
|
|
341
|
-
|
|
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/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
|
|
@@ -23,10 +23,10 @@ dependencies:
|
|
|
23
23
|
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: '0.4'
|
|
26
|
-
description: Extends the Inquirex DSL with
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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:
|