inquirex-llm 0.10.0 → 0.12.2
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/CHANGELOG.md +37 -4
- data/README.md +118 -15
- data/lib/inquirex/llm/adapter.rb +28 -0
- data/lib/inquirex/llm/anthropic_adapter.rb +54 -38
- data/lib/inquirex/llm/configuration.rb +181 -0
- data/lib/inquirex/llm/errors.rb +4 -0
- data/lib/inquirex/llm/evals/adapter.rb +81 -0
- data/lib/inquirex/llm/evals/dataset.rb +78 -0
- data/lib/inquirex/llm/evals/field_score.rb +176 -0
- data/lib/inquirex/llm/evals/model_adapter.rb +58 -0
- data/lib/inquirex/llm/evals/scorer.rb +111 -0
- data/lib/inquirex/llm/evals.rb +34 -0
- data/lib/inquirex/llm/extraction.rb +54 -0
- data/lib/inquirex/llm/jev_adapter.rb +254 -0
- data/lib/inquirex/llm/models.rb +67 -0
- data/lib/inquirex/llm/numbers.rb +74 -0
- data/lib/inquirex/llm/open_router_adapter.rb +53 -0
- data/lib/inquirex/llm/openai_adapter.rb +64 -53
- data/lib/inquirex/llm/version.rb +1 -1
- data/lib/inquirex/llm.rb +43 -0
- metadata +68 -55
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1261cf90cb18c935731001fc135e896e271d63e64f84aba6fc5b45105bfcaa8c
|
|
4
|
+
data.tar.gz: 74ddcc8813674ae65e5979a3becc1c9ecbd7bdf4b0e64e71161a6ddc15c86a01
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1eda2a5ae132b1027700dc058f90ff47fe3a29e5e8804669b5ad4c9c1771f76b8c01484963e8f3d63d5e3fc9f3311e568d67d73b0f35f7e3cff9c1723a4d723c
|
|
7
|
+
data.tar.gz: 7af624fbd2546c4cf1404f3ac51aecc72c670e013888fb7fdb654be5f2f3ee455023e08737dfea510295bdfa9fd783c4ffe801f2e383004f8491ed463ad02955
|
data/CHANGELOG.md
CHANGED
|
@@ -1,12 +1,47 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.12.2 (unreleased)
|
|
4
|
+
|
|
5
|
+
- **The eval suite now runs on llm-evals.** `inquirex/llm/evals` requires `llm/evals` and subclasses `::LLM::Evals`, because `agent-evals` was renamed and is no longer on RubyGems. The config file is `evals/.llm-evals.config.yml` and the executable is `evals`. Inside `Inquirex::LLM`, a bare `LLM` means this gem, so the leading `::` is required.
|
|
6
|
+
|
|
7
|
+
## 0.12.0 (unreleased)
|
|
8
|
+
|
|
9
|
+
Hand-written until `github_changelog_generator` regenerates this file at release. The family-wide notes are in inquirex's `INQUIREX_VERSION_LOG.md`.
|
|
10
|
+
|
|
11
|
+
- **Current Claude model ids.** `AnthropicAdapter::MODELS` lists `claude-fable-5-1`, `claude-opus-5-5`, `claude-sonnet-5-5` and `claude-haiku-5-5`; the default (`claude-sonnet-5-5`) and the `:claude_sonnet`, `:claude_haiku`, `:claude_opus` and new `:claude_fable` aliases are looked up in it. 0.10.0 defaulted to `claude-sonnet-4-20250514`.
|
|
12
|
+
- **`Inquirex::LLM.configuration`.** Provider, API keys, default models, token limits, temperatures and debug are set in one place, from ENV first (`INQUIREX_LLM_ADAPTER`, `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, new `INQUIREX_LLM_ANTHROPIC_MODEL`, `INQUIREX_LLM_OPENAI_MODEL`, `INQUIREX_LLM_MAX_TOKENS`, `INQUIREX_LLM_TEMPERATURE`, `INQUIREX_LLM_SUMMARY_MAX_TOKENS`, `INQUIREX_LLM_SUMMARY_TEMPERATURE`, `INQUIREX_DEBUG`), then today's defaults; `Inquirex::LLM.configure` overrides them and `reset_configuration!` re-reads ENV. Invalid values raise `Errors::ConfigurationError` naming the variable. Model ids moved to `Inquirex::LLM::Models`; the adapter constants still answer. `Inquirex::LLM.adapter` builds the adapter with the selection rules inquirex-tty used to carry.
|
|
13
|
+
- **`OpenRouterAdapter`.** Any OpenRouter model through the OpenAI adapter's prompts and parsing, with an optional reasoning `effort:`. New `OPENROUTER_API_KEY` and `INQUIREX_LLM_OPENROUTER_MODEL` (default `anthropic/claude-sonnet-5.5`).
|
|
14
|
+
- **Extraction result object.** An `extract` call returns an `Inquirex::LLM::Extraction`: the schema-shaped Hash it always returned (it is a `Hash`, so `prefill!`, `==` and JSON are unchanged) with `raw`, the JSON exactly as the model sent it before enum values were canonicalized, and `usage`, the tokens and cost the provider reported. One LLM call per extraction, as before.
|
|
15
|
+
- **`JevAdapter`.** TypeSafe's Jev through its native typed-question API: `enum`, `boolean`, `multi_enum` and `integer` fields only, one request per step, `min_confidence:` to leave unsure fields unset, and per-field confidence in the `Extraction#raw`. Reads `TYPESAFE_API_KEY` and `INQUIREX_LLM_JEV_MODEL`, whose default is now `jev-latest` (was the unverified `jev-1.13.0`). Neither new adapter is auto-detected; `INQUIREX_LLM_ADAPTER` accepts `openrouter` and `jev`.
|
|
16
|
+
- **The extraction eval suite now runs on agent-evals.** `evals/` holds `.agent-eval.config.yml` and `datasets/<NN-slug>/` (dsl.rb, schema.rb, `cases/{complete,vague}/`); the adapter and the five-outcome scorer are `Inquirex::LLM::Evals`, in the gem but loaded only by `require "inquirex/llm/evals"`. The standalone `extract-eval` runner is gone; its results are in `docs/evals-history.md`.
|
|
17
|
+
- **Fix:** an `OpenAIAdapter` subclass's `MODEL_MAP` was ignored; model symbols always resolved through OpenAI's map.
|
|
18
|
+
- **No `temperature` for current models.** They answer any sampling parameter with HTTP 400, so the adapter omits it for ids in `MODELS` and still sends it to a model named by an older id.
|
|
19
|
+
- **Requires `inquirex ~> 0.12`**, which declares `prism` as a runtime dependency and so loads on Ruby 4.1.
|
|
20
|
+
- `spec/inquirex/llm/gemspec_spec.rb` fails when `lib/` requires a library that neither ships with the running Ruby nor is a declared runtime dependency.
|
|
21
|
+
- CircleCI config copied from inquirex core; `.ruby-version` pins 4.1.0-preview1.
|
|
22
|
+
|
|
23
|
+
## [v0.10.0](https://github.com/inquirex/inquirex-llm/tree/v0.10.0) (2026-08-17)
|
|
24
|
+
|
|
25
|
+
[Full Changelog](https://github.com/inquirex/inquirex-llm/compare/v0.8.0...v0.10.0)
|
|
26
|
+
|
|
27
|
+
**Merged pull requests:**
|
|
28
|
+
|
|
29
|
+
- Set the lockstep version to 0.10.0 [#7](https://github.com/inquirex/inquirex-llm/pull/7) ([kigster](https://github.com/kigster))
|
|
30
|
+
- Bump to 0.9.6 [#6](https://github.com/inquirex/inquirex-llm/pull/6) ([kigster](https://github.com/kigster))
|
|
31
|
+
- Add the summarize verb [#5](https://github.com/inquirex/inquirex-llm/pull/5) ([kigster](https://github.com/kigster))
|
|
32
|
+
- Canonicalize LLM output against schema values [#3](https://github.com/inquirex/inquirex-llm/pull/3) ([kigster](https://github.com/kigster))
|
|
33
|
+
|
|
34
|
+
## [v0.8.0](https://github.com/inquirex/inquirex-llm/tree/v0.8.0) (2026-08-01)
|
|
35
|
+
|
|
36
|
+
[Full Changelog](https://github.com/inquirex/inquirex-llm/compare/v0.6.0...v0.8.0)
|
|
37
|
+
|
|
3
38
|
## [v0.6.0](https://github.com/inquirex/inquirex-llm/tree/v0.6.0) (2026-07-20)
|
|
4
39
|
|
|
5
40
|
[Full Changelog](https://github.com/inquirex/inquirex-llm/compare/v0.5.0...v0.6.0)
|
|
6
41
|
|
|
7
42
|
**Merged pull requests:**
|
|
8
43
|
|
|
9
|
-
- Resolve extract schemas from question references [
|
|
44
|
+
- Resolve extract schemas from question references [#2](https://github.com/inquirex/inquirex-llm/pull/2) ([kigster](https://github.com/kigster))
|
|
10
45
|
|
|
11
46
|
## [v0.5.0](https://github.com/inquirex/inquirex-llm/tree/v0.5.0) (2026-07-16)
|
|
12
47
|
|
|
@@ -14,7 +49,7 @@
|
|
|
14
49
|
|
|
15
50
|
**Merged pull requests:**
|
|
16
51
|
|
|
17
|
-
- Reduce LLM verbs to `extract` and release v0.5.0 [
|
|
52
|
+
- Reduce LLM verbs to `extract` and release v0.5.0 [#1](https://github.com/inquirex/inquirex-llm/pull/1) ([kigster](https://github.com/kigster))
|
|
18
53
|
|
|
19
54
|
## [v0.4.1](https://github.com/inquirex/inquirex-llm/tree/v0.4.1) (2026-07-13)
|
|
20
55
|
|
|
@@ -28,6 +63,4 @@
|
|
|
28
63
|
|
|
29
64
|
[Full Changelog](https://github.com/inquirex/inquirex-llm/compare/809890f89aa8230b34674820debf7a68ced283e5...v0.1.0)
|
|
30
65
|
|
|
31
|
-
|
|
32
|
-
|
|
33
66
|
\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)*
|
data/README.md
CHANGED
|
@@ -228,6 +228,8 @@ engine.answer(result)
|
|
|
228
228
|
# engine.current_step_id => :summary
|
|
229
229
|
```
|
|
230
230
|
|
|
231
|
+
The built-in adapters return an `Inquirex::LLM::Extraction` for an `extract` step. It is the Hash above (a `Hash` subclass, so `prefill!`, `==` and JSON serialization see only the values) carrying two more things from the one LLM call that produced it: `raw`, the JSON exactly as the model sent it before enum values were canonicalized (`raw[:entity_type] # => "LLC"` where `result[:entity_type] # => "llc"`), and `usage`, the tokens and, when the provider reports it, the cost.
|
|
232
|
+
|
|
231
233
|
For testing, use `NullAdapter` which returns schema-conformant placeholder values without any API calls:
|
|
232
234
|
|
|
233
235
|
```ruby
|
|
@@ -236,39 +238,116 @@ result = adapter.call(engine.current_step)
|
|
|
236
238
|
# => { industry: "", employee_count: 0, revenue: 0.0 }
|
|
237
239
|
```
|
|
238
240
|
|
|
241
|
+
## Configuration
|
|
242
|
+
|
|
243
|
+
Model ids, the provider and its credentials are set in one place, `Inquirex::LLM.configuration`. Each setting is read from its environment variable when that variable is set and not blank, and otherwise takes the default below. The adapters fall back on it whenever no `api_key:` or `model:` is passed and a node sets no `max_tokens` or `temperature` of its own.
|
|
244
|
+
|
|
245
|
+
| Variable | Setting | Default |
|
|
246
|
+
| ---------------------------------- | --------------------- | ---------------------------------------------------------------------- |
|
|
247
|
+
| `INQUIREX_LLM_ADAPTER` | `provider` | auto-detect; else `anthropic`, `openai`, `openrouter`, `jev` or `null` |
|
|
248
|
+
| `ANTHROPIC_API_KEY` | `anthropic_api_key` | none |
|
|
249
|
+
| `OPENAI_API_KEY` | `openai_api_key` | none |
|
|
250
|
+
| `OPENROUTER_API_KEY` | `openrouter_api_key` | none |
|
|
251
|
+
| `TYPESAFE_API_KEY` | `typesafe_api_key` | none |
|
|
252
|
+
| `INQUIREX_LLM_ANTHROPIC_MODEL` | `anthropic_model` | `Models::ANTHROPIC_DEFAULT` (`claude-sonnet-5-5`) |
|
|
253
|
+
| `INQUIREX_LLM_OPENAI_MODEL` | `openai_model` | `Models::OPENAI_DEFAULT` (`gpt-4o-mini`) |
|
|
254
|
+
| `INQUIREX_LLM_OPENROUTER_MODEL` | `openrouter_model` | `Models::OPENROUTER_DEFAULT` (`anthropic/claude-sonnet-5.5`) |
|
|
255
|
+
| `INQUIREX_LLM_JEV_MODEL` | `jev_model` | `Models::JEV_DEFAULT` (`jev-latest`) |
|
|
256
|
+
| `INQUIREX_LLM_MAX_TOKENS` | `max_tokens` | `2048` |
|
|
257
|
+
| `INQUIREX_LLM_TEMPERATURE` | `temperature` | `0.2` |
|
|
258
|
+
| `INQUIREX_LLM_SUMMARY_MAX_TOKENS` | `summary_max_tokens` | `4096` |
|
|
259
|
+
| `INQUIREX_LLM_SUMMARY_TEMPERATURE` | `summary_temperature` | `0.4` |
|
|
260
|
+
| `INQUIREX_DEBUG` | `debug` | off; any non-blank value turns it on |
|
|
261
|
+
|
|
262
|
+
An invalid value (an unknown provider, a token count that is not a positive integer, a temperature outside 0 to 2) raises `Inquirex::LLM::Errors::ConfigurationError` naming the variable. `to_h` and `inspect` print every `*_api_key` as `[REDACTED]`.
|
|
263
|
+
|
|
264
|
+
ENV is read once, on first use. Code can override any setting afterwards; `nil` restores the default:
|
|
265
|
+
|
|
266
|
+
```ruby
|
|
267
|
+
Inquirex::LLM.configure do |c|
|
|
268
|
+
c.provider = :anthropic
|
|
269
|
+
c.anthropic_model = "claude-opus-5-5"
|
|
270
|
+
c.max_tokens = 4096
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
adapter = Inquirex::LLM.adapter # built from the configuration
|
|
274
|
+
Inquirex::LLM.reset_configuration! # discard overrides and read ENV again
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
`Inquirex::LLM.adapter` picks the adapter, first match wins:
|
|
278
|
+
|
|
279
|
+
| Condition | Adapter |
|
|
280
|
+
| --------------------------- | ---------------------------------------- |
|
|
281
|
+
| `provider` is `:null` | `NullAdapter` |
|
|
282
|
+
| `provider` is `:anthropic` | `AnthropicAdapter` (needs key) |
|
|
283
|
+
| `provider` is `:openai` | `OpenAIAdapter` (needs key) |
|
|
284
|
+
| `provider` is `:openrouter` | `OpenRouterAdapter` (needs key) |
|
|
285
|
+
| `provider` is `:jev` | `JevAdapter` (needs key; `extract` only) |
|
|
286
|
+
| `anthropic_api_key` is set | `AnthropicAdapter` |
|
|
287
|
+
| `openai_api_key` is set | `OpenAIAdapter` |
|
|
288
|
+
| otherwise | `NullAdapter` |
|
|
289
|
+
|
|
290
|
+
`OPENROUTER_API_KEY` and `TYPESAFE_API_KEY` never select an adapter on their own: a host that has one of those keys for something else keeps the adapter it had, and Jev cannot summarize, so both must be asked for by name.
|
|
291
|
+
|
|
239
292
|
## Built-in Adapters
|
|
240
293
|
|
|
241
|
-
| Class
|
|
242
|
-
|
|
|
243
|
-
| `Inquirex::LLM::NullAdapter`
|
|
244
|
-
| `Inquirex::LLM::AnthropicAdapter`
|
|
245
|
-
| `Inquirex::LLM::OpenAIAdapter`
|
|
246
|
-
| `Inquirex::LLM::
|
|
294
|
+
| Class | Provider | API | Auth | Key env var |
|
|
295
|
+
| ---------------------------------- | ---------- | -------------------------------------- | ------------------------- | -------------------- |
|
|
296
|
+
| `Inquirex::LLM::NullAdapter` | — | none (placeholders) | none | — |
|
|
297
|
+
| `Inquirex::LLM::AnthropicAdapter` | Anthropic | `/v1/messages` | `x-api-key` header | `ANTHROPIC_API_KEY` |
|
|
298
|
+
| `Inquirex::LLM::OpenAIAdapter` | OpenAI | `/v1/chat/completions` (JSON mode) | `Authorization: Bearer …` | `OPENAI_API_KEY` |
|
|
299
|
+
| `Inquirex::LLM::OpenRouterAdapter` | OpenRouter | `/api/v1/chat/completions` (JSON mode) | `Authorization: Bearer …` | `OPENROUTER_API_KEY` |
|
|
300
|
+
| `Inquirex::LLM::JevAdapter` | TypeSafe | `/v1/systemone` (typed questions) | `Authorization: Bearer …` | `TYPESAFE_API_KEY` |
|
|
301
|
+
|
|
302
|
+
OpenRouter and Jev are never auto-detected; select them with `INQUIREX_LLM_ADAPTER` or build them directly.
|
|
247
303
|
|
|
248
|
-
|
|
304
|
+
All 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.
|
|
249
305
|
|
|
250
306
|
### AnthropicAdapter
|
|
251
307
|
|
|
252
308
|
```ruby
|
|
253
|
-
adapter = Inquirex::LLM::AnthropicAdapter.new
|
|
254
|
-
|
|
255
|
-
|
|
309
|
+
adapter = Inquirex::LLM::AnthropicAdapter.new # key and model from the configuration
|
|
310
|
+
adapter = Inquirex::LLM::AnthropicAdapter.new( # or name them; arguments win
|
|
311
|
+
api_key: "sk-ant-...",
|
|
312
|
+
model: "claude-opus-5-5" # or pass the short symbol in the DSL
|
|
256
313
|
)
|
|
257
314
|
```
|
|
258
315
|
|
|
259
|
-
Recognized `model :symbol` values in the DSL: `:claude_sonnet`, `:claude_haiku`, `:claude_opus`
|
|
316
|
+
The current model ids live in one constant, `Inquirex::LLM::Models::ANTHROPIC` (also reachable as `AnthropicAdapter::MODELS`): `claude-fable-5-1`, `claude-opus-5-5`, `claude-sonnet-5-5` and `claude-haiku-5-5`. The default is `Models::ANTHROPIC_DEFAULT`, `claude-sonnet-5-5`, unless `INQUIREX_LLM_ANTHROPIC_MODEL` names another. Recognized `model :symbol` values in the DSL: `:claude_sonnet`, `:claude_haiku`, `:claude_opus` and `:claude_fable`, each mapped to its family's id in `MODELS`. `:claude_fable` is Anthropic-only; the OpenAI adapter does not map it.
|
|
317
|
+
|
|
318
|
+
These models reject sampling parameters, so the adapter does not send `temperature` to them. A `temperature` in the DSL still reaches a model you name by an older id, such as `model: "claude-sonnet-4-6"`.
|
|
260
319
|
|
|
261
320
|
### OpenAIAdapter
|
|
262
321
|
|
|
263
322
|
```ruby
|
|
264
|
-
adapter = Inquirex::LLM::OpenAIAdapter.new
|
|
265
|
-
|
|
266
|
-
model: "gpt-4o-mini"
|
|
267
|
-
)
|
|
323
|
+
adapter = Inquirex::LLM::OpenAIAdapter.new # key and model from the configuration
|
|
324
|
+
adapter = Inquirex::LLM::OpenAIAdapter.new(api_key: "sk-...", model: "gpt-4.1")
|
|
268
325
|
```
|
|
269
326
|
|
|
270
327
|
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.
|
|
271
328
|
|
|
329
|
+
### OpenRouterAdapter
|
|
330
|
+
|
|
331
|
+
```ruby
|
|
332
|
+
adapter = Inquirex::LLM::OpenRouterAdapter.new # key and model from the configuration
|
|
333
|
+
adapter = Inquirex::LLM::OpenRouterAdapter.new(model: "google/gemini-2.5-flash", effort: "low")
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
Any model OpenRouter hosts, with the OpenAI adapter's prompts and parsing. The Claude and GPT symbols map to OpenRouter ids (`:claude_haiku` → `anthropic/claude-haiku-5.5`); any other model is sent as written. `effort:` (`none` … `max`) becomes OpenRouter's `reasoning.effort`; without it the model decides.
|
|
337
|
+
|
|
338
|
+
### JevAdapter
|
|
339
|
+
|
|
340
|
+
```ruby
|
|
341
|
+
adapter = Inquirex::LLM::JevAdapter.new # key and model from the configuration
|
|
342
|
+
adapter = Inquirex::LLM::JevAdapter.new(min_confidence: 0.6) # less sure than 0.6 → nil
|
|
343
|
+
extraction = adapter.call(node, answers) # the values, as every adapter returns them
|
|
344
|
+
extraction.raw.dig("filing_status", "confidence") # how sure Jev was, from TypeSafe's answer
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
Jev, TypeSafe's System One model, answers typed questions with calibrated probabilities instead of writing JSON. The adapter turns each schema field into questions and sends them all in one request: a Choice per `enum` (plus "not stated"), a yes / no / not-stated Choice per `boolean`, a presence Choice plus a Noul per value for `multi_enum`, and for `integer` a Choice among the numbers the text states (digits or words, `Inquirex::LLM::Numbers`) and the counts 0 to 12. It retries 429 and 529 twice with backoff.
|
|
348
|
+
|
|
349
|
+
Jev chooses; it cannot write. A schema with a `string`, `text`, `date`, `email`, `phone` or other free-form field raises `AdapterError` before anything is sent (`JevAdapter.unsupported_fields(schema)` checks a flow up front), and `summarize` always raises. Route those to a text model.
|
|
350
|
+
|
|
272
351
|
## LLM-assisted Pre-fill Pattern
|
|
273
352
|
|
|
274
353
|
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:
|
|
@@ -406,6 +485,30 @@ detour :followup do
|
|
|
406
485
|
end
|
|
407
486
|
```
|
|
408
487
|
|
|
488
|
+
## Evals
|
|
489
|
+
|
|
490
|
+
Which model extracts best, measured with this gem's own prompts: ten flows, a complete and a vague text each, run on [llm-evals](https://github.com/kigster/llm-evals). The lineup and the data live in [`evals/`](evals/README.md); the adapter and the scorer are `Inquirex::LLM::Evals`, loaded only by `require "inquirex/llm/evals"`.
|
|
491
|
+
|
|
492
|
+
How a run works:
|
|
493
|
+
|
|
494
|
+
1. **Verify** (offline): each dataset's `schema.rb` must match the `extract` step in its `dsl.rb`, and every `expected.json` must fit that schema.
|
|
495
|
+
1. **Expand the lineup**: every model under `vendors:` in `evals/.llm-evals.config.yml`, times each `effort`/`temperature` it lists, becomes one contender. Each contender runs every case.
|
|
496
|
+
1. **Call the model**: one OpenRouter chat completion per case, built from this gem's production prompt and the step's schema.
|
|
497
|
+
1. **Score the raw reply** field by field: `correct` 1.0, `missed` 0.2, `hallucinated` 0.15, `incorrect` 0.1, `invalid` 0. A case's score is the mean over its fields. The rules are in [`evals/README.md`](evals/README.md#scoring).
|
|
498
|
+
1. **Report**: a leaderboard on screen and a JSON log under `log/llm-evals/`. If `BRAINTRUST_API_KEY` or `LOGFIRE_TOKEN` is set, each case is also sent to that one tracker; `--local` turns that off.
|
|
499
|
+
|
|
500
|
+
```bash
|
|
501
|
+
just evals -n # the whole matrix, calling nothing
|
|
502
|
+
just evals # every configured model on every dataset
|
|
503
|
+
just evals --local -d 05-managed-it-services # one dataset, no remote tracker
|
|
504
|
+
just evals-verify # step 1 only
|
|
505
|
+
|
|
506
|
+
bundle exec evals cases # the 20 cases
|
|
507
|
+
bundle exec evals run -m openai/gpt-4o-mini # one model, per-case breakdown
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
`just evals` needs `OPENROUTER_API_KEY` and the `:evals` bundle group. It is a paid run: 14 contenders × 20 cases = 280 calls.
|
|
511
|
+
|
|
409
512
|
## Development
|
|
410
513
|
|
|
411
514
|
```bash
|
data/lib/inquirex/llm/adapter.rb
CHANGED
|
@@ -128,6 +128,22 @@ module Inquirex
|
|
|
128
128
|
|
|
129
129
|
protected
|
|
130
130
|
|
|
131
|
+
# The tokens a response reports, in one shape whatever the provider's:
|
|
132
|
+
# Anthropic counts input_tokens/output_tokens, OpenAI and OpenRouter
|
|
133
|
+
# prompt_tokens/completion_tokens, and OpenRouter adds the cost it
|
|
134
|
+
# billed. Empty when the response carries no usage at all.
|
|
135
|
+
#
|
|
136
|
+
# @param usage [Hash, nil] the response's "usage" object
|
|
137
|
+
# @return [Hash{Symbol => Numeric}] input_tokens, output_tokens, and cost_usd when reported
|
|
138
|
+
def usage_from(usage)
|
|
139
|
+
return {} unless usage.is_a?(Hash)
|
|
140
|
+
|
|
141
|
+
counts = usage.transform_keys(&:to_s)
|
|
142
|
+
{ input_tokens: (counts["input_tokens"] || counts["prompt_tokens"]).to_i,
|
|
143
|
+
output_tokens: (counts["output_tokens"] || counts["completion_tokens"]).to_i,
|
|
144
|
+
cost_usd: counts["cost"]&.to_f }.compact
|
|
145
|
+
end
|
|
146
|
+
|
|
131
147
|
# The canonical form value for a raw LLM answer, or nil when the answer
|
|
132
148
|
# is outside the allowed list (after trimming and case-folding).
|
|
133
149
|
#
|
|
@@ -166,6 +182,18 @@ module Inquirex
|
|
|
166
182
|
"\"multi_enum\" fields. Never invent a value outside the list."
|
|
167
183
|
end
|
|
168
184
|
|
|
185
|
+
# Prompt instruction for fields the source text does not answer. They
|
|
186
|
+
# must come back null: 0, "", false and [] are real answers (no
|
|
187
|
+
# dependents, not self-employed), and the engine prefills them and
|
|
188
|
+
# skips the question as though the user had said so.
|
|
189
|
+
#
|
|
190
|
+
# @return [String]
|
|
191
|
+
def missing_values_instruction
|
|
192
|
+
"Every key in the schema must be present in your output. When the source text does not " \
|
|
193
|
+
"provide a value, use null. Never use 0, \"\", false, or [] for a value that is missing: " \
|
|
194
|
+
"those are answers, and they would be recorded as what the user said."
|
|
195
|
+
end
|
|
196
|
+
|
|
169
197
|
# One human-readable line per schema field, used in user prompts:
|
|
170
198
|
# income_types (multi_enum: W2 | business | crypto)
|
|
171
199
|
# dependents (integer)
|
|
@@ -4,16 +4,16 @@ require "net/http"
|
|
|
4
4
|
require "uri"
|
|
5
5
|
require "json"
|
|
6
6
|
require "inquirex/llm/adapter"
|
|
7
|
+
require "inquirex/llm/extraction"
|
|
8
|
+
require "inquirex/llm/configuration"
|
|
7
9
|
|
|
8
10
|
module Inquirex
|
|
9
11
|
module LLM
|
|
10
12
|
# Real Anthropic Claude adapter for inquirex-llm.
|
|
11
13
|
#
|
|
12
14
|
# Usage:
|
|
13
|
-
# adapter = Inquirex::LLM::AnthropicAdapter.new
|
|
14
|
-
#
|
|
15
|
-
# model: "claude-sonnet-4-20250514"
|
|
16
|
-
# )
|
|
15
|
+
# adapter = Inquirex::LLM::AnthropicAdapter.new # key and model from Inquirex::LLM.configuration
|
|
16
|
+
# adapter = Inquirex::LLM::AnthropicAdapter.new(api_key: "sk-...", model: "claude-opus-5-5")
|
|
17
17
|
# result = adapter.call(engine.current_step, engine.answers)
|
|
18
18
|
#
|
|
19
19
|
# The adapter:
|
|
@@ -26,42 +26,38 @@ module Inquirex
|
|
|
26
26
|
class AnthropicAdapter < Adapter
|
|
27
27
|
API_URL = "https://api.anthropic.com/v1/messages"
|
|
28
28
|
API_VERSION = "2023-06-01"
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}.freeze
|
|
44
|
-
|
|
45
|
-
# @param api_key [String, nil] defaults to ENV["ANTHROPIC_API_KEY"]
|
|
46
|
-
# @param model [String, nil] default model id when a node does not specify one
|
|
29
|
+
# The current Claude model ids. Kept as aliases of {Models} so callers
|
|
30
|
+
# that read them here keep working; {Models} is where they are written.
|
|
31
|
+
MODELS = Models::ANTHROPIC
|
|
32
|
+
MODEL_MAP = Models::ANTHROPIC_ALIASES
|
|
33
|
+
DEFAULT_MODEL = Models::ANTHROPIC_DEFAULT
|
|
34
|
+
|
|
35
|
+
DEFAULT_MAX_TOKENS = Configuration::DEFAULT_MAX_TOKENS
|
|
36
|
+
DEFAULT_SUMMARY_MAX_TOKENS = Configuration::DEFAULT_SUMMARY_MAX_TOKENS
|
|
37
|
+
DEFAULT_SUMMARY_TEMPERATURE = Configuration::DEFAULT_SUMMARY_TEMPERATURE
|
|
38
|
+
|
|
39
|
+
# @param api_key [String, nil] defaults to the configuration's anthropic_api_key (ANTHROPIC_API_KEY)
|
|
40
|
+
# @param model [String, nil] default model id when a node does not specify one;
|
|
41
|
+
# defaults to the configuration's anthropic_model (INQUIREX_LLM_ANTHROPIC_MODEL)
|
|
42
|
+
# @raise [ArgumentError] when no key is passed or configured
|
|
47
43
|
def initialize(api_key: nil, model: nil)
|
|
48
44
|
super()
|
|
49
|
-
@api_key = api_key ||
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
@default_model = model ||
|
|
45
|
+
@api_key = api_key || LLM.configuration.anthropic_api_key
|
|
46
|
+
raise ArgumentError, "ANTHROPIC_API_KEY is required (pass api_key: or set the env var)" unless @api_key
|
|
47
|
+
|
|
48
|
+
@default_model = model || LLM.configuration.anthropic_model
|
|
53
49
|
end
|
|
54
50
|
|
|
55
51
|
# @param node [Inquirex::LLM::Node] the current LLM step
|
|
56
52
|
# @param answers [Hash] all collected answers so far
|
|
57
|
-
# @return [
|
|
53
|
+
# @return [Extraction] the schema-shaped values, with the model's raw JSON and the usage
|
|
58
54
|
# @raise [Errors::AdapterError] on API / parse failures
|
|
59
55
|
# @raise [Errors::SchemaViolationError] when the LLM output misses schema fields
|
|
60
56
|
def call(node, answers = {})
|
|
61
57
|
source = source_answers(node, answers)
|
|
62
58
|
model = resolve_model(node)
|
|
63
|
-
temperature = node.respond_to?(:temperature) ? (node.temperature ||
|
|
64
|
-
max_tokens = node.respond_to?(:max_tokens) ? (node.max_tokens ||
|
|
59
|
+
temperature = node.respond_to?(:temperature) ? (node.temperature || config.temperature) : config.temperature
|
|
60
|
+
max_tokens = node.respond_to?(:max_tokens) ? (node.max_tokens || config.max_tokens) : config.max_tokens
|
|
65
61
|
|
|
66
62
|
response = call_api(
|
|
67
63
|
model: model,
|
|
@@ -71,7 +67,8 @@ module Inquirex
|
|
|
71
67
|
max_tokens: max_tokens
|
|
72
68
|
)
|
|
73
69
|
|
|
74
|
-
|
|
70
|
+
raw = parse_response(response)
|
|
71
|
+
result = Extraction.new(output: normalize_output(node, raw), raw: raw, usage: usage_from(response["usage"]))
|
|
75
72
|
validate_output!(node, result)
|
|
76
73
|
result
|
|
77
74
|
end
|
|
@@ -93,20 +90,38 @@ module Inquirex
|
|
|
93
90
|
model: resolve_model(node),
|
|
94
91
|
system: Prompts::SUMMARIZE,
|
|
95
92
|
user: summary_input(transcript),
|
|
96
|
-
temperature: node.respond_to?(:temperature) ? (node.temperature ||
|
|
97
|
-
max_tokens: node.respond_to?(:max_tokens) ? (node.max_tokens ||
|
|
93
|
+
temperature: node.respond_to?(:temperature) ? (node.temperature || config.summary_temperature) : config.summary_temperature,
|
|
94
|
+
max_tokens: node.respond_to?(:max_tokens) ? (node.max_tokens || config.summary_max_tokens) : config.summary_max_tokens
|
|
98
95
|
)
|
|
99
96
|
parse_text_response(response)
|
|
100
97
|
end
|
|
101
98
|
|
|
102
99
|
private
|
|
103
100
|
|
|
101
|
+
# Read on every call rather than kept from #initialize, so limits set
|
|
102
|
+
# through Inquirex::LLM.configure after the adapter was built still apply.
|
|
103
|
+
#
|
|
104
|
+
# @return [Configuration]
|
|
105
|
+
def config = LLM.configuration
|
|
106
|
+
|
|
104
107
|
def resolve_model(node)
|
|
105
108
|
return @default_model unless node.respond_to?(:model) && node.model
|
|
106
109
|
|
|
107
110
|
MODEL_MAP[node.model.to_sym] || node.model.to_s
|
|
108
111
|
end
|
|
109
112
|
|
|
113
|
+
# The models in {MODELS} reject sampling parameters: a request carrying
|
|
114
|
+
# `temperature` is answered with HTTP 400, whatever its value. The DSL's
|
|
115
|
+
# `temperature` is therefore sent only to an explicitly named older
|
|
116
|
+
# model that still accepts it.
|
|
117
|
+
#
|
|
118
|
+
# @param model [String] resolved model id
|
|
119
|
+
# @param temperature [Float]
|
|
120
|
+
# @return [Hash] `{}` or `{ temperature: }`
|
|
121
|
+
def sampling(model, temperature)
|
|
122
|
+
MODELS.include?(model) ? {} : { temperature: temperature }
|
|
123
|
+
end
|
|
124
|
+
|
|
110
125
|
def build_system_prompt(node)
|
|
111
126
|
"You are a data extraction assistant for a questionnaire engine. " \
|
|
112
127
|
"Your job is to analyze user input and extract structured data." +
|
|
@@ -118,6 +133,7 @@ module Inquirex
|
|
|
118
133
|
"\n\nYou MUST respond with ONLY a valid JSON object matching this schema:\n" \
|
|
119
134
|
"#{schema_contract_json(node.schema)}\n" \
|
|
120
135
|
"#{values_instruction(node.schema)}\n\n" \
|
|
136
|
+
"#{missing_values_instruction}\n" \
|
|
121
137
|
"Do not include any text before or after the JSON. No markdown fences. Just the raw JSON object."
|
|
122
138
|
else
|
|
123
139
|
"\n\nRespond with a valid JSON object containing your analysis. " \
|
|
@@ -159,14 +175,14 @@ module Inquirex
|
|
|
159
175
|
request["x-api-key"] = @api_key
|
|
160
176
|
request["anthropic-version"] = API_VERSION
|
|
161
177
|
request.body = JSON.generate(
|
|
162
|
-
model:
|
|
163
|
-
max_tokens:
|
|
164
|
-
|
|
165
|
-
system:
|
|
166
|
-
messages:
|
|
178
|
+
model: model,
|
|
179
|
+
max_tokens: max_tokens,
|
|
180
|
+
**sampling(model, temperature),
|
|
181
|
+
system: system,
|
|
182
|
+
messages: [{ role: "user", content: user }]
|
|
167
183
|
)
|
|
168
184
|
|
|
169
|
-
warn "[inquirex-llm] Calling #{model}..." if
|
|
185
|
+
warn "[inquirex-llm] Calling #{model}..." if config.debug?
|
|
170
186
|
|
|
171
187
|
response = http.request(request)
|
|
172
188
|
unless response.is_a?(Net::HTTPSuccess)
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "inquirex/llm/errors"
|
|
4
|
+
require "inquirex/llm/models"
|
|
5
|
+
|
|
6
|
+
module Inquirex
|
|
7
|
+
module LLM
|
|
8
|
+
# Gem-wide settings: which provider to use, its credentials, its default
|
|
9
|
+
# model, and the generation limits the adapters fall back on when a node
|
|
10
|
+
# does not set its own.
|
|
11
|
+
#
|
|
12
|
+
# Each setting is read from its environment variable when that variable
|
|
13
|
+
# is set and not blank, and otherwise takes the default below. ENV is read
|
|
14
|
+
# once, when the configuration is built; {LLM.reset_configuration!} builds
|
|
15
|
+
# it again. Writers override either, and a nil written restores the
|
|
16
|
+
# default.
|
|
17
|
+
#
|
|
18
|
+
# Invalid values raise {Errors::ConfigurationError} naming the variable
|
|
19
|
+
# (or the writer) that carried them, at the moment they are read, so a
|
|
20
|
+
# typo in INQUIREX_LLM_ADAPTER fails loudly instead of silently falling
|
|
21
|
+
# through to auto-detection.
|
|
22
|
+
#
|
|
23
|
+
# @example
|
|
24
|
+
# Inquirex::LLM.configure do |c|
|
|
25
|
+
# c.provider = :anthropic
|
|
26
|
+
# c.anthropic_model = "claude-opus-5-5"
|
|
27
|
+
# c.max_tokens = 4096
|
|
28
|
+
# end
|
|
29
|
+
class Configuration
|
|
30
|
+
# The providers {LLM.adapter} can build. nil means auto-detect.
|
|
31
|
+
PROVIDERS = %i[anthropic openai openrouter jev null].freeze
|
|
32
|
+
|
|
33
|
+
DEFAULT_MAX_TOKENS = 2048
|
|
34
|
+
DEFAULT_TEMPERATURE = 0.2
|
|
35
|
+
|
|
36
|
+
# Summaries want more room than an extraction (prose, not a handful of
|
|
37
|
+
# fields) and more warmth than one (0.2 produces summaries that read
|
|
38
|
+
# like a database dump).
|
|
39
|
+
DEFAULT_SUMMARY_MAX_TOKENS = 4096
|
|
40
|
+
DEFAULT_SUMMARY_TEMPERATURE = 0.4
|
|
41
|
+
|
|
42
|
+
# The range both providers accept; OpenAI's is the wider of the two.
|
|
43
|
+
TEMPERATURES = (0.0..2.0)
|
|
44
|
+
|
|
45
|
+
# What {#to_h} and {#inspect} show instead of a credential.
|
|
46
|
+
REDACTED = "[REDACTED]"
|
|
47
|
+
|
|
48
|
+
# One setting: the variable it is read from, its default, and the
|
|
49
|
+
# coercion its value goes through.
|
|
50
|
+
Setting = Data.define(:env, :default, :type)
|
|
51
|
+
|
|
52
|
+
# Every setting, in the order {#to_h} lists them. Readers, writers,
|
|
53
|
+
# the ENV table in the README and the specs all follow this table.
|
|
54
|
+
SETTINGS = {
|
|
55
|
+
provider: Setting.new("INQUIREX_LLM_ADAPTER", nil, :provider),
|
|
56
|
+
anthropic_api_key: Setting.new("ANTHROPIC_API_KEY", nil, :string),
|
|
57
|
+
openai_api_key: Setting.new("OPENAI_API_KEY", nil, :string),
|
|
58
|
+
openrouter_api_key: Setting.new("OPENROUTER_API_KEY", nil, :string),
|
|
59
|
+
typesafe_api_key: Setting.new("TYPESAFE_API_KEY", nil, :string),
|
|
60
|
+
anthropic_model: Setting.new("INQUIREX_LLM_ANTHROPIC_MODEL", Models::ANTHROPIC_DEFAULT, :string),
|
|
61
|
+
openai_model: Setting.new("INQUIREX_LLM_OPENAI_MODEL", Models::OPENAI_DEFAULT, :string),
|
|
62
|
+
openrouter_model: Setting.new("INQUIREX_LLM_OPENROUTER_MODEL", Models::OPENROUTER_DEFAULT, :string),
|
|
63
|
+
jev_model: Setting.new("INQUIREX_LLM_JEV_MODEL", Models::JEV_DEFAULT, :string),
|
|
64
|
+
max_tokens: Setting.new("INQUIREX_LLM_MAX_TOKENS", DEFAULT_MAX_TOKENS, :positive_integer),
|
|
65
|
+
temperature: Setting.new("INQUIREX_LLM_TEMPERATURE", DEFAULT_TEMPERATURE, :temperature),
|
|
66
|
+
summary_max_tokens: Setting.new("INQUIREX_LLM_SUMMARY_MAX_TOKENS", DEFAULT_SUMMARY_MAX_TOKENS, :positive_integer),
|
|
67
|
+
summary_temperature: Setting.new("INQUIREX_LLM_SUMMARY_TEMPERATURE", DEFAULT_SUMMARY_TEMPERATURE, :temperature),
|
|
68
|
+
debug: Setting.new("INQUIREX_DEBUG", false, :flag)
|
|
69
|
+
}.freeze
|
|
70
|
+
|
|
71
|
+
SETTINGS.each_key do |name|
|
|
72
|
+
attr_reader name
|
|
73
|
+
|
|
74
|
+
# @param value [Object, nil] nil restores the default
|
|
75
|
+
# @raise [Errors::ConfigurationError] when the value is invalid
|
|
76
|
+
define_method(:"#{name}=") do |value|
|
|
77
|
+
instance_variable_set(:"@#{name}", resolve(name, value, "Inquirex::LLM.configuration.#{name}"))
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# @param env [#[]] the environment to read; ENV unless a spec says otherwise
|
|
82
|
+
# @raise [Errors::ConfigurationError] when a variable holds an invalid value
|
|
83
|
+
def initialize(env = ENV)
|
|
84
|
+
SETTINGS.each do |name, setting|
|
|
85
|
+
instance_variable_set(:"@#{name}", resolve(name, env[setting.env], setting.env))
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# @return [Boolean] whether debug logging is on
|
|
90
|
+
def debug? = debug
|
|
91
|
+
|
|
92
|
+
# Every setting, with credentials replaced by {REDACTED} when set.
|
|
93
|
+
#
|
|
94
|
+
# @return [Hash{Symbol => Object}]
|
|
95
|
+
def to_h
|
|
96
|
+
SETTINGS.keys.to_h do |name|
|
|
97
|
+
value = public_send(name)
|
|
98
|
+
[name, secret?(name) && value ? REDACTED : value]
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Never shows a credential: an exception report or a console session
|
|
103
|
+
# that prints the configuration must not leak a key.
|
|
104
|
+
#
|
|
105
|
+
# @return [String]
|
|
106
|
+
def inspect
|
|
107
|
+
"#<#{self.class.name} #{to_h.map { |name, value| "#{name}=#{value.inspect}" }.join(", ")}>"
|
|
108
|
+
end
|
|
109
|
+
alias to_s inspect
|
|
110
|
+
|
|
111
|
+
private
|
|
112
|
+
|
|
113
|
+
def secret?(name) = name.end_with?("_api_key")
|
|
114
|
+
|
|
115
|
+
# @param name [Symbol] the setting
|
|
116
|
+
# @param value [Object, nil] raw value from ENV or a writer
|
|
117
|
+
# @param source [String] what to name in an error
|
|
118
|
+
# @return [Object] the coerced value, or the default when blank
|
|
119
|
+
def resolve(name, value, source)
|
|
120
|
+
setting = SETTINGS.fetch(name)
|
|
121
|
+
return setting.default if value.nil? || (value.is_a?(String) && value.strip.empty?)
|
|
122
|
+
|
|
123
|
+
send(:"coerce_#{setting.type}", value, source)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def coerce_string(value, _source) = value.to_s.strip
|
|
127
|
+
|
|
128
|
+
def coerce_provider(value, source)
|
|
129
|
+
provider = value.to_s.strip.downcase.to_sym
|
|
130
|
+
return provider if PROVIDERS.include?(provider)
|
|
131
|
+
|
|
132
|
+
invalid!(source, value, "one of #{PROVIDERS.join(", ")}")
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def coerce_positive_integer(value, source)
|
|
136
|
+
integer = Integer(value.to_s.strip, 10)
|
|
137
|
+
integer.positive? ? integer : invalid!(source, value, "a positive integer")
|
|
138
|
+
rescue ArgumentError, TypeError
|
|
139
|
+
invalid!(source, value, "a positive integer")
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def coerce_temperature(value, source)
|
|
143
|
+
float = Float(value.to_s.strip)
|
|
144
|
+
TEMPERATURES.cover?(float) ? float : invalid!(source, value, "a number from #{TEMPERATURES.min} to #{TEMPERATURES.max}")
|
|
145
|
+
rescue ArgumentError, TypeError
|
|
146
|
+
invalid!(source, value, "a number from #{TEMPERATURES.min} to #{TEMPERATURES.max}")
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Any non-blank value turns debugging on, as INQUIREX_DEBUG always has.
|
|
150
|
+
def coerce_flag(value, _source) = value.is_a?(String) || value == true
|
|
151
|
+
|
|
152
|
+
def invalid!(source, value, expected)
|
|
153
|
+
raise Errors::ConfigurationError, "#{source} must be #{expected} (got #{value.inspect})"
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
class << self
|
|
158
|
+
# The gem-wide configuration, built from ENV on first use.
|
|
159
|
+
#
|
|
160
|
+
# @return [Configuration]
|
|
161
|
+
def configuration
|
|
162
|
+
@configuration ||= Configuration.new
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# @yieldparam config [Configuration]
|
|
166
|
+
# @return [Configuration]
|
|
167
|
+
def configure
|
|
168
|
+
yield configuration
|
|
169
|
+
configuration
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Discards every write and reads ENV again. Call it after changing ENV
|
|
173
|
+
# (a .env file loaded late, or a spec), since ENV is read only once.
|
|
174
|
+
#
|
|
175
|
+
# @return [Configuration]
|
|
176
|
+
def reset_configuration!
|
|
177
|
+
@configuration = Configuration.new
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|