@devjonaed/typesafe-ai 0.1.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.
- package/CHANGELOG.md +18 -0
- package/LICENSE +21 -0
- package/README.md +353 -0
- package/dist/index.d.ts +56 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +102 -0
- package/dist/index.js.map +1 -0
- package/package.json +54 -0
- package/src/index.ts +206 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
First release.
|
|
6
|
+
|
|
7
|
+
- `ask(state, questions)` — answers named questions in one call and returns them unwrapped: a
|
|
8
|
+
probability for a Noul, the selected label for a Choice, the expected score for a Score.
|
|
9
|
+
- Shorthand specs: a string is a yes/no question, an array of labels is a choice. `choice()`,
|
|
10
|
+
`score()` and `noul()` are re-exported for questions that need descriptions.
|
|
11
|
+
- `$` carries the full SDK result — answers, probabilities, confidence, usage, model and request ID.
|
|
12
|
+
- `configure()` and `client()` for the client, which is otherwise built from `TYPESAFE_API_KEY` on
|
|
13
|
+
first use.
|
|
14
|
+
- Bad input fails before the request: non-object question maps, unusable question values, choices
|
|
15
|
+
with fewer than two distinct string labels, a blank model override, a blank API key, and `$` as a
|
|
16
|
+
question name.
|
|
17
|
+
- Bad responses fail loudly: a missing answer, or an answer whose type does not match the question
|
|
18
|
+
asked, throws instead of yielding `undefined` or a plausible-looking wrong value.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Md Jonaed Hasan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
# @devjonaed/typesafe-ai
|
|
2
|
+
|
|
3
|
+
[](https://github.com/jonaed1230/typesafe-ai/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/@devjonaed/typesafe-ai)
|
|
5
|
+
|
|
6
|
+
> **Unofficial.** A community wrapper around TypeSafe AI's official SDK, not built or endorsed by
|
|
7
|
+
> TypeSafe. Bugs here belong in
|
|
8
|
+
> [this repo's issues](https://github.com/jonaed1230/typesafe-ai/issues), not TypeSafe's. For the
|
|
9
|
+
> official client, use [`@typesafe-ai/sdk`](https://www.npmjs.com/package/@typesafe-ai/sdk) directly.
|
|
10
|
+
|
|
11
|
+
A thin wrapper around [`@typesafe-ai/sdk`](https://www.npmjs.com/package/@typesafe-ai/sdk). Ask typed
|
|
12
|
+
questions about some state, get the answers back already unwrapped.
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
// the SDK
|
|
16
|
+
const client = new TypeSafeClient();
|
|
17
|
+
const r = await client.systemOne({
|
|
18
|
+
state,
|
|
19
|
+
questions: {
|
|
20
|
+
category: choice("...", { billing: null, technical: null, other: null }),
|
|
21
|
+
urgent: noul("Is the customer angry?"),
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
r.answers.category.choice;
|
|
25
|
+
r.answers.urgent.noul;
|
|
26
|
+
|
|
27
|
+
// this package
|
|
28
|
+
const a = await ask(state, {
|
|
29
|
+
category: ["billing", "technical", "other"],
|
|
30
|
+
urgent: "Is the customer angry?",
|
|
31
|
+
});
|
|
32
|
+
a.category;
|
|
33
|
+
a.urgent;
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
No client to construct, shorthand for the common questions, and answers that are values rather than
|
|
37
|
+
envelopes. Everything the SDK returns is still on `a.$`, and `client()` hands you the SDK itself.
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
npm install @devjonaed/typesafe-ai
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Set `TYPESAFE_API_KEY` in your environment. That is the whole setup — there is no client to construct.
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { ask } from "@devjonaed/typesafe-ai";
|
|
47
|
+
|
|
48
|
+
const a = await ask("I was charged twice. Please fix this ASAP.", {
|
|
49
|
+
category: ["billing", "technical", "other"],
|
|
50
|
+
urgent: "Is the customer angry?",
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
a.category; // "billing" — typed as "billing" | "technical" | "other"
|
|
54
|
+
a.urgent; // 0.81 — probability from 0 to 1
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Writing questions
|
|
58
|
+
|
|
59
|
+
| You write | You get | Underneath |
|
|
60
|
+
| --- | --- | --- |
|
|
61
|
+
| `"Is the customer angry?"` | a probability from 0 to 1 | a Noul question |
|
|
62
|
+
| `["billing", "technical"]` | the selected label, as a narrow union | a Choice question |
|
|
63
|
+
| `choice(instructions, criteria)` | the selected label | a Choice question with descriptions |
|
|
64
|
+
| `score(instructions, criteria)` | the expected score | a Score question |
|
|
65
|
+
| `noul(instructions, criteria)` | a probability from 0 to 1 | a Noul question with descriptions |
|
|
66
|
+
|
|
67
|
+
All three helpers take optional descriptions, including `noul`:
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
noul("Is the customer angry?", { true: "Furious or threatening to leave.", false: "Calm." });
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`choice`, `score` and `noul` are re-exported from the SDK, so one import covers everything:
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
import { ask, choice, score } from "@devjonaed/typesafe-ai";
|
|
77
|
+
|
|
78
|
+
const a = await ask(ticket, {
|
|
79
|
+
team: choice("Who should handle this?", {
|
|
80
|
+
billing: "Charges, refunds, invoices.",
|
|
81
|
+
platform: "Outages, errors, latency.",
|
|
82
|
+
}),
|
|
83
|
+
severity: score("How badly is the customer blocked?", [
|
|
84
|
+
"Not blocked.",
|
|
85
|
+
"Inconvenienced.",
|
|
86
|
+
"Cannot work.",
|
|
87
|
+
]),
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
a.team; // "billing" | "platform"
|
|
91
|
+
a.severity; // 0.83 — the expected score, from 0 to 2 here
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Every question is answered in one API call, in parallel, and extra questions do not slow the call
|
|
95
|
+
down. That makes it cheap to ask everything a branch might need up front and let your code decide
|
|
96
|
+
what matters:
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
const a = await ask(ticket, {
|
|
100
|
+
category: ["billing", "technical", "other"],
|
|
101
|
+
refundRequested: "Is the customer asking for money back?",
|
|
102
|
+
angry: "Is the customer angry?",
|
|
103
|
+
mentionsLegal: "Does the customer mention lawyers or legal action?",
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
if (a.mentionsLegal > 0.8) return escalate(ticket);
|
|
107
|
+
if (a.category === "billing" && a.refundRequested > 0.6) return refundQueue(ticket);
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Keep each question narrow. One question per decision beats one question that tries to decide
|
|
111
|
+
everything — see [Patterns](https://docs.typesafe.ai/patterns) for the shapes that work.
|
|
112
|
+
|
|
113
|
+
An array is the shortest form, but it sends no instructions — the labels alone have to carry the
|
|
114
|
+
question. Reach for `choice()` as soon as a label needs explaining. It needs at least two distinct
|
|
115
|
+
labels; anything less is rejected before the request goes out.
|
|
116
|
+
|
|
117
|
+
A score is answered on `0` to `levels - 1`, and the answer may fall between levels. The rubric comes
|
|
118
|
+
back on `$`, so you never have to keep the level list in sync by hand:
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
a.severity; // 0.83
|
|
122
|
+
a.$.answers.severity.legend; // { 0: "Not blocked.", 1: "Inconvenienced.", 2: "Cannot work." }
|
|
123
|
+
a.$.answers.severity.probabilities; // { 0: 0.17, 1: 0.82, 2: 0.01 }
|
|
124
|
+
a.$.answers.severity.confidence; // 0.73
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Structured instructions
|
|
128
|
+
|
|
129
|
+
Instructions and criteria are not limited to strings — any JSON object or array works, which keeps a
|
|
130
|
+
long rubric readable:
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
const a = await ask(ticket, {
|
|
134
|
+
team: choice(
|
|
135
|
+
{ question: "Who handles this?", rules: ["route by topic", "prefer specialists"] },
|
|
136
|
+
{ billing: { owns: ["charges", "refunds"] }, platform: { owns: ["outages"] }, other: null },
|
|
137
|
+
),
|
|
138
|
+
});
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
`null` as a description leaves that label undescribed, which is exactly what the array shorthand
|
|
142
|
+
sends for every label.
|
|
143
|
+
|
|
144
|
+
## Confidence, probabilities and usage
|
|
145
|
+
|
|
146
|
+
Unwrapped answers cover the common case. The full SDK result is always on `$`, typed per question:
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
a.$.answers.category.confidence; // 1
|
|
150
|
+
a.$.answers.category.probabilities; // { billing: 1, technical: 0, other: 0 }
|
|
151
|
+
a.$.usage; // { input_tokens: 415, output_tokens: 96 }
|
|
152
|
+
a.$.requestId; // "req_01a0ca7f5a747639aaac05b46c7dc7db"
|
|
153
|
+
a.$.model; // "jev-1.13.0"
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Gate on confidence when a wrong answer is expensive:
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
const { category, $ } = await ask(ticket, { category: ["billing", "technical", "other"] });
|
|
160
|
+
if ($.answers.category.confidence < 0.7) return escalateToHuman(ticket);
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
**A Noul answer has no confidence and no probabilities.** Its probability *is* the answer, so the
|
|
164
|
+
full shape is just `{ type: "noul", noul: 0.91 }`. Only Choice and Score report confidence — for a
|
|
165
|
+
yes/no question, read how far `a.urgent` sits from `0.5`.
|
|
166
|
+
|
|
167
|
+
Because `$` is reserved, `ask` throws a `TypeSafeError` if you name a question `$`.
|
|
168
|
+
|
|
169
|
+
## State
|
|
170
|
+
|
|
171
|
+
State can be text, or any JSON object or array:
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
await ask(
|
|
175
|
+
{ ticket: { body, customer: { plan: "enterprise", tenureMonths: 14 } } },
|
|
176
|
+
{ churnRisk: "Is this customer likely to cancel?" },
|
|
177
|
+
);
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
State is required. The SDK's types also accept `null`, but the API rejects it with
|
|
181
|
+
`UnprocessableEntityError: 422 state: Field required`, so pass something.
|
|
182
|
+
|
|
183
|
+
## Options
|
|
184
|
+
|
|
185
|
+
Per-call options are the SDK's, plus `model`:
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
await ask(state, questions, {
|
|
189
|
+
model: "jev-preview",
|
|
190
|
+
timeout: 5000,
|
|
191
|
+
signal: controller.signal,
|
|
192
|
+
retry: { maxRetries: 4 },
|
|
193
|
+
headers: { "x-request-source": "support-inbox" },
|
|
194
|
+
});
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
The client is created from the environment on first use. To configure it yourself, call `configure`
|
|
198
|
+
once at startup:
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
import { configure } from "@devjonaed/typesafe-ai";
|
|
202
|
+
|
|
203
|
+
configure({ apiKey: process.env.MY_KEY, defaultModel: "jev-preview", timeout: 30_000 });
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
`configure` takes the SDK's `TypeSafeClientConfig` and returns the client. `client()` returns the
|
|
207
|
+
current one, so the full SDK surface — `client().models.list()`, `client().systemOne(...)` — stays
|
|
208
|
+
reachable when you need it.
|
|
209
|
+
|
|
210
|
+
## Configuration
|
|
211
|
+
|
|
212
|
+
Explicit options beat environment variables, which beat the defaults:
|
|
213
|
+
|
|
214
|
+
| Variable | `configure` option | Default |
|
|
215
|
+
| --- | --- | --- |
|
|
216
|
+
| `TYPESAFE_API_KEY` | `apiKey` | required |
|
|
217
|
+
| `TYPESAFE_BASE_URL` | `baseURL` | `https://api.typesafe.ai` |
|
|
218
|
+
| `TYPESAFE_DEFAULT_MODEL` | `defaultModel` | `jev-latest` |
|
|
219
|
+
| `TYPESAFE_LOG_LEVEL` | `logLevel` | `warn` |
|
|
220
|
+
|
|
221
|
+
Other defaults: `timeout` 10000 ms per attempt, `retry.maxRetries` 2. Available models today are
|
|
222
|
+
`jev-latest` and `jev-preview` — `client().models.list()` is the current answer.
|
|
223
|
+
|
|
224
|
+
Set `logLevel: "info"` for request summaries, or `"debug"` to log headers and bodies. Credential
|
|
225
|
+
headers are redacted; **bodies are not**, so keep `debug` away from production if your state
|
|
226
|
+
contains personal data.
|
|
227
|
+
|
|
228
|
+
The client refuses to run in a browser, because that would ship your API key to every visitor. Call
|
|
229
|
+
it from your server. `dangerouslyAllowBrowser: true` overrides that, and the name is the warning.
|
|
230
|
+
|
|
231
|
+
Three more `configure` options are worth knowing:
|
|
232
|
+
|
|
233
|
+
```ts
|
|
234
|
+
configure({
|
|
235
|
+
logger: myPinoLikeLogger, // anything with debug/info/warn/error; defaults to console
|
|
236
|
+
defaultHeaders: { "x-app": "support-inbox" }, // sent on every request
|
|
237
|
+
fetch: myFetch, // swap the transport — this is the seam the test suite uses
|
|
238
|
+
});
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## Request IDs
|
|
242
|
+
|
|
243
|
+
Every call's request ID is on `$`, ready to quote to support:
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
const a = await ask(ticket, { category: ["billing", "technical", "other"] });
|
|
247
|
+
a.$.requestId; // "req_01a0ca7f5a747639aaac05b46c7dc7db"
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
It is `undefined` when the server sends no `x-typesafe-request-id` header. Failures carry it too —
|
|
251
|
+
every `APIError` has a `requestId`. For the raw `Response` itself, use the SDK promise underneath:
|
|
252
|
+
`client().systemOne(...).withResponse()`, which also offers `asResponse()` and `map()`.
|
|
253
|
+
|
|
254
|
+
## Types
|
|
255
|
+
|
|
256
|
+
`ask` infers everything from the spec you pass, so you rarely need these, but they are exported for
|
|
257
|
+
signatures of your own:
|
|
258
|
+
|
|
259
|
+
```ts
|
|
260
|
+
import type { Answer, Answers, Meta, Spec, SpecMap, QuestionOf } from "@devjonaed/typesafe-ai";
|
|
261
|
+
|
|
262
|
+
type Routing = SpecMap & { category: readonly ["billing", "technical"] };
|
|
263
|
+
type Category = Answer<readonly ["billing", "technical"]>; // "billing" | "technical"
|
|
264
|
+
type Result = Answers<Routing>; // { category: ... } & { $: Meta<Routing> }
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
`EntryType`, `Question` and `SystemOneResult` are re-exported from the SDK for the same reason.
|
|
268
|
+
|
|
269
|
+
## Errors
|
|
270
|
+
|
|
271
|
+
Errors come from the SDK unchanged: `AuthenticationError` (401), `BadRequestError` (400),
|
|
272
|
+
`PermissionDeniedError` (403), `UnprocessableEntityError` (422), `RateLimitError` (429, with
|
|
273
|
+
`retryAfterMs`), `InternalServerError` (5xx), `APIConnectionError`, `APITimeoutError` and
|
|
274
|
+
`APIUserAbortError` — all extending `TypeSafeError`. Import them from `@typesafe-ai/sdk`.
|
|
275
|
+
|
|
276
|
+
Failed requests are retried twice by default, with backoff on 408, 429 and 5xx. Override per call
|
|
277
|
+
with `retry`, or for every call through `configure`.
|
|
278
|
+
|
|
279
|
+
Bad input is caught before any request goes out, always as `TypeSafeError`:
|
|
280
|
+
|
|
281
|
+
```
|
|
282
|
+
Questions must be an object keyed by answer name; got an array.
|
|
283
|
+
"$" is reserved for the full result; rename that question.
|
|
284
|
+
Question "category" must be a string, an array of at least two labels, or a question from choice(), score() or noul(); got number.
|
|
285
|
+
Choice question "category" has 1 label; at least two are required.
|
|
286
|
+
Choice question "category" repeats a label; every label must be distinct.
|
|
287
|
+
Choice question "category" has a non-string label (1); labels must be strings.
|
|
288
|
+
The model override is blank. Pass a model name, or omit `model`.
|
|
289
|
+
The API key is blank. Pass a real key, or omit `apiKey` to read TYPESAFE_API_KEY from the environment.
|
|
290
|
+
At least one question is required. (from the SDK)
|
|
291
|
+
Score question "severity" has 1 criteria; at least two scores are required. (from the SDK)
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
A rejected `configure` leaves any previous client untouched. Every failure in this package is a
|
|
295
|
+
`TypeSafeError`, so one check covers input mistakes, API errors and transport errors alike:
|
|
296
|
+
|
|
297
|
+
```ts
|
|
298
|
+
import { APIError, TypeSafeError } from "@typesafe-ai/sdk";
|
|
299
|
+
|
|
300
|
+
try {
|
|
301
|
+
const a = await ask(ticket, { category: ["billing", "technical", "other"] });
|
|
302
|
+
} catch (e) {
|
|
303
|
+
if (e instanceof APIError) console.error(e.status, e.requestId);
|
|
304
|
+
else if (e instanceof TypeSafeError) console.error("bad request shape:", e.message);
|
|
305
|
+
else throw e;
|
|
306
|
+
}
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
A bad *response* is an error too, rather than a value that looks plausible:
|
|
310
|
+
|
|
311
|
+
```
|
|
312
|
+
The API returned no answer for question "category". Request ID: req_01a0ca7f5a747639aaac05b46c7dc7db.
|
|
313
|
+
The API answered question "category" with a "noul" answer, but a "choice" question was asked.
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
The second one matters: without it, asking a Choice and receiving a Noul would hand you `0.5` where
|
|
317
|
+
you expected a label. If a future model adds a question type this version does not know, you get
|
|
318
|
+
that error instead of `undefined`.
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
## Tests
|
|
322
|
+
|
|
323
|
+
```sh
|
|
324
|
+
npm test # build, then 34 offline tests
|
|
325
|
+
TYPESAFE_API_KEY=... npm test # adds 8 tests against the real API
|
|
326
|
+
npm run check:docs # type-checks every snippet in this file
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
CI runs all of it on Node 20, 22 and 24 for every push and pull request. The live tests skip
|
|
330
|
+
themselves when no key is present, so forks get a meaningful green run without secrets.
|
|
331
|
+
|
|
332
|
+
The offline tests stub `fetch`, so they assert the exact request body that goes out — including that
|
|
333
|
+
the array shorthand produces a byte-identical body to hand-written SDK questions. The live tests are
|
|
334
|
+
skipped without a key.
|
|
335
|
+
|
|
336
|
+
## Learn more
|
|
337
|
+
|
|
338
|
+
This README documents the wrapper. For the model itself — when to reach for Choice over Score, how
|
|
339
|
+
to decompose a judgment into atomic questions, and how confidence is calibrated — see the TypeSafe
|
|
340
|
+
docs:
|
|
341
|
+
|
|
342
|
+
- [Primitives](https://docs.typesafe.ai/primitives) — Choice, Score and Noul in depth
|
|
343
|
+
- [Confidence](https://docs.typesafe.ai/confidence) — what it measures and how to gate on it
|
|
344
|
+
- [Patterns](https://docs.typesafe.ai/patterns) — fan-out, confidence routing, composite scoring
|
|
345
|
+
- [How to build with TypeSafe](https://docs.typesafe.ai/concepts/how-to-build-with-system-one)
|
|
346
|
+
|
|
347
|
+
## Requirements
|
|
348
|
+
|
|
349
|
+
Node 20 or newer. ESM only — there is no CommonJS build.
|
|
350
|
+
|
|
351
|
+
## License
|
|
352
|
+
|
|
353
|
+
MIT. Copyright (c) 2026 Md Jonaed Hasan. See [LICENSE](LICENSE).
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { TypeSafeClient, type ChoiceQuestion, type EntryType, type NoulQuestion, type Question, type Questions, type RequestOptions, type ScoreQuestion, type SystemOneResult, type TypeSafeClientConfig } from "@typesafe-ai/sdk";
|
|
2
|
+
export { choice, noul, score } from "@typesafe-ai/sdk";
|
|
3
|
+
export type { EntryType, Question, SystemOneResult } from "@typesafe-ai/sdk";
|
|
4
|
+
/** A question written the short way, or a full SDK question object. */
|
|
5
|
+
export type Spec = string | readonly string[] | Question;
|
|
6
|
+
/** Questions keyed by the name their answer is returned under. */
|
|
7
|
+
export type SpecMap = Record<string, Spec>;
|
|
8
|
+
/** The unwrapped answer for one spec: a probability, a label, or a score. */
|
|
9
|
+
export type Answer<S> = S extends string ? number : S extends readonly (infer L)[] ? L extends string ? L : never : S extends ChoiceQuestion<infer C> ? keyof C & string : S extends ScoreQuestion<infer _> ? number : S extends NoulQuestion ? number : never;
|
|
10
|
+
/** The SDK question a spec expands to, so `$` keeps its precise answer types. */
|
|
11
|
+
export type QuestionOf<S> = S extends string ? NoulQuestion : S extends readonly (infer L)[] ? [L] extends [string] ? ChoiceQuestion<{
|
|
12
|
+
[Label in L]: null;
|
|
13
|
+
}> : never : S extends Question ? S : never;
|
|
14
|
+
/** The full SDK result, plus the request ID for the call that produced it. */
|
|
15
|
+
export type Meta<S extends SpecMap> = SystemOneResult<{
|
|
16
|
+
[K in keyof S]: QuestionOf<S[K]>;
|
|
17
|
+
} & Questions> & {
|
|
18
|
+
/** Request ID from `x-typesafe-request-id`, or `undefined` when absent. Quote it to support. */
|
|
19
|
+
readonly requestId: string | undefined;
|
|
20
|
+
};
|
|
21
|
+
/** Unwrapped answers by name, plus `$` holding the full SDK result. */
|
|
22
|
+
export type Answers<S extends SpecMap> = {
|
|
23
|
+
[K in keyof S]: Answer<S[K]>;
|
|
24
|
+
} & {
|
|
25
|
+
$: Meta<S>;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Replace the client `ask` uses. Call once at startup, or not at all.
|
|
29
|
+
*
|
|
30
|
+
* @throws {TypeSafeError} `apiKey` is present but blank. The SDK would accept it and fail
|
|
31
|
+
* later with a 401; failing here points at the unset variable that actually caused it.
|
|
32
|
+
*/
|
|
33
|
+
export declare const configure: (config?: TypeSafeClientConfig) => TypeSafeClient;
|
|
34
|
+
/** The client `ask` uses, created from the environment on first use. */
|
|
35
|
+
export declare const client: () => TypeSafeClient;
|
|
36
|
+
/**
|
|
37
|
+
* Ask named questions about some state and get the answers back unwrapped.
|
|
38
|
+
*
|
|
39
|
+
* A string is a yes/no question and answers with a probability from 0 to 1.
|
|
40
|
+
* An array of labels is a choice and answers with the selected label.
|
|
41
|
+
* `choice()`, `score()` and `noul()` cover the cases that need descriptions.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* const a = await ask("I was charged twice. Fix this ASAP.", {
|
|
45
|
+
* category: ["billing", "technical", "other"],
|
|
46
|
+
* urgent: "Is the customer angry?",
|
|
47
|
+
* });
|
|
48
|
+
* a.category; // "billing"
|
|
49
|
+
* a.urgent; // 0.93
|
|
50
|
+
* a.$.answers.category.confidence; // 0.88
|
|
51
|
+
* a.$.requestId; // "req_01a0ca7f5a747639aaac05b46c7dc7db"
|
|
52
|
+
*/
|
|
53
|
+
export declare function ask<const S extends SpecMap>(state: EntryType, spec: S, options?: RequestOptions & {
|
|
54
|
+
model?: string;
|
|
55
|
+
}): Promise<Answers<S>>;
|
|
56
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EAId,KAAK,cAAc,EACnB,KAAK,SAAS,EACd,KAAK,YAAY,EACjB,KAAK,QAAQ,EACb,KAAK,SAAS,EAEd,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,eAAe,EACpB,KAAK,oBAAoB,EAC1B,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACvD,YAAY,EAAE,SAAS,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAE7E,uEAAuE;AACvE,MAAM,MAAM,IAAI,GAAG,MAAM,GAAG,SAAS,MAAM,EAAE,GAAG,QAAQ,CAAC;AAEzD,kEAAkE;AAClE,MAAM,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;AAE3C,6EAA6E;AAC7E,MAAM,MAAM,MAAM,CAAC,CAAC,IAAI,CAAC,SAAS,MAAM,GACpC,MAAM,GACN,CAAC,SAAS,SAAS,CAAC,MAAM,CAAC,CAAC,EAAE,GAC5B,CAAC,SAAS,MAAM,GACd,CAAC,GACD,KAAK,GACP,CAAC,SAAS,cAAc,CAAC,MAAM,CAAC,CAAC,GAC/B,MAAM,CAAC,GAAG,MAAM,GAChB,CAAC,SAAS,aAAa,CAAC,MAAM,CAAC,CAAC,GAC9B,MAAM,GACN,CAAC,SAAS,YAAY,GACpB,MAAM,GACN,KAAK,CAAC;AAElB,iFAAiF;AACjF,MAAM,MAAM,UAAU,CAAC,CAAC,IAAI,CAAC,SAAS,MAAM,GACxC,YAAY,GACZ,CAAC,SAAS,SAAS,CAAC,MAAM,CAAC,CAAC,EAAE,GAC5B,CAAC,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,GAClB,cAAc,CAAC;KAAG,KAAK,IAAI,CAAC,GAAG,IAAI;CAAE,CAAC,GACtC,KAAK,GACP,CAAC,SAAS,QAAQ,GAChB,CAAC,GACD,KAAK,CAAC;AAEd,8EAA8E;AAC9E,MAAM,MAAM,IAAI,CAAC,CAAC,SAAS,OAAO,IAAI,eAAe,CACnD;KAAG,CAAC,IAAI,MAAM,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CAAE,GAAG,SAAS,CACjD,GAAG;IACF,gGAAgG;IAChG,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;CACxC,CAAC;AAEF,uEAAuE;AACvE,MAAM,MAAM,OAAO,CAAC,CAAC,SAAS,OAAO,IAAI;KAAG,CAAC,IAAI,MAAM,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CAAE,GAAG;IAC1E,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC;CACZ,CAAC;AAuEF;;;;;GAKG;AACH,eAAO,MAAM,SAAS,GAAI,SAAQ,oBAAyB,KAAG,cAM7D,CAAC;AAEF,wEAAwE;AACxE,eAAO,MAAM,MAAM,QAAO,cAAmD,CAAC;AAE9E;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,GAAG,CAAC,KAAK,CAAC,CAAC,SAAS,OAAO,EAC/C,KAAK,EAAE,SAAS,EAChB,IAAI,EAAE,CAAC,EACP,OAAO,GAAE,cAAc,GAAG;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAAO,GAChD,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAiCrB"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { TypeSafeClient, TypeSafeError, choice, noul, } from "@typesafe-ai/sdk";
|
|
2
|
+
export { choice, noul, score } from "@typesafe-ai/sdk";
|
|
3
|
+
/** The question types this version understands. */
|
|
4
|
+
const TYPES = new Set(["noul", "choice", "score"]);
|
|
5
|
+
const toQuestion = (name, spec) => {
|
|
6
|
+
if (typeof spec === "string")
|
|
7
|
+
return noul(spec);
|
|
8
|
+
if (Array.isArray(spec)) {
|
|
9
|
+
const bad = spec.find((label) => typeof label !== "string");
|
|
10
|
+
if (bad !== undefined)
|
|
11
|
+
throw new TypeSafeError(`Choice question "${name}" has a non-string label (${JSON.stringify(bad)}); labels must be strings.`);
|
|
12
|
+
// Mirror the SDK's own rule for score rubrics: a question nobody can answer is a
|
|
13
|
+
// mistake worth catching here, not a 422 from the server.
|
|
14
|
+
const labels = new Set(spec);
|
|
15
|
+
if (labels.size !== spec.length)
|
|
16
|
+
throw new TypeSafeError(`Choice question "${name}" repeats a label; every label must be distinct.`);
|
|
17
|
+
if (labels.size < 2)
|
|
18
|
+
throw new TypeSafeError(`Choice question "${name}" has ${labels.size} label${labels.size === 1 ? "" : "s"}; at least two are required.`);
|
|
19
|
+
return choice(null, Object.fromEntries([...labels].map((label) => [label, null])));
|
|
20
|
+
}
|
|
21
|
+
// Anything else has to be a question object, or the request would be silently malformed
|
|
22
|
+
// and the API would answer a question nobody asked.
|
|
23
|
+
if (spec === null || typeof spec !== "object" || !TYPES.has(spec.type))
|
|
24
|
+
throw new TypeSafeError(`Question "${name}" must be a string, an array of at least two labels, or a question from choice(), score() or noul(); got ${describe(spec)}.`);
|
|
25
|
+
return spec;
|
|
26
|
+
};
|
|
27
|
+
const describe = (value) => value === null ? "null" : Array.isArray(value) ? "an array" : typeof value;
|
|
28
|
+
/**
|
|
29
|
+
* Pull the one value a caller wants out of an answer.
|
|
30
|
+
*
|
|
31
|
+
* The answer has to match the question that was asked. A mismatch means the API and this
|
|
32
|
+
* package disagree about the request, and returning the wrong field would look like a
|
|
33
|
+
* plausible answer rather than a bug.
|
|
34
|
+
*/
|
|
35
|
+
const unwrap = (name, asked, answer) => {
|
|
36
|
+
if (answer === null || typeof answer !== "object")
|
|
37
|
+
throw new TypeSafeError(`The API answered question "${name}" with ${describe(answer)} instead of an answer.`);
|
|
38
|
+
if (answer.type !== asked.type)
|
|
39
|
+
throw new TypeSafeError(`The API answered question "${name}" with a ${JSON.stringify(answer.type)} answer, but a ${JSON.stringify(asked.type)} question was asked.`);
|
|
40
|
+
switch (answer.type) {
|
|
41
|
+
case "noul":
|
|
42
|
+
return answer.noul;
|
|
43
|
+
case "choice":
|
|
44
|
+
return answer.choice;
|
|
45
|
+
case "score":
|
|
46
|
+
return answer.score;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
let shared;
|
|
50
|
+
/**
|
|
51
|
+
* Replace the client `ask` uses. Call once at startup, or not at all.
|
|
52
|
+
*
|
|
53
|
+
* @throws {TypeSafeError} `apiKey` is present but blank. The SDK would accept it and fail
|
|
54
|
+
* later with a 401; failing here points at the unset variable that actually caused it.
|
|
55
|
+
*/
|
|
56
|
+
export const configure = (config = {}) => {
|
|
57
|
+
if (config.apiKey !== undefined && config.apiKey.trim() === "")
|
|
58
|
+
throw new TypeSafeError("The API key is blank. Pass a real key, or omit `apiKey` to read TYPESAFE_API_KEY from the environment.");
|
|
59
|
+
return (shared = new TypeSafeClient(config));
|
|
60
|
+
};
|
|
61
|
+
/** The client `ask` uses, created from the environment on first use. */
|
|
62
|
+
export const client = () => (shared ??= new TypeSafeClient());
|
|
63
|
+
/**
|
|
64
|
+
* Ask named questions about some state and get the answers back unwrapped.
|
|
65
|
+
*
|
|
66
|
+
* A string is a yes/no question and answers with a probability from 0 to 1.
|
|
67
|
+
* An array of labels is a choice and answers with the selected label.
|
|
68
|
+
* `choice()`, `score()` and `noul()` cover the cases that need descriptions.
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* const a = await ask("I was charged twice. Fix this ASAP.", {
|
|
72
|
+
* category: ["billing", "technical", "other"],
|
|
73
|
+
* urgent: "Is the customer angry?",
|
|
74
|
+
* });
|
|
75
|
+
* a.category; // "billing"
|
|
76
|
+
* a.urgent; // 0.93
|
|
77
|
+
* a.$.answers.category.confidence; // 0.88
|
|
78
|
+
* a.$.requestId; // "req_01a0ca7f5a747639aaac05b46c7dc7db"
|
|
79
|
+
*/
|
|
80
|
+
export async function ask(state, spec, options = {}) {
|
|
81
|
+
if (spec === null || typeof spec !== "object" || Array.isArray(spec))
|
|
82
|
+
throw new TypeSafeError(`Questions must be an object keyed by answer name; got ${describe(spec)}.`);
|
|
83
|
+
if ("$" in spec)
|
|
84
|
+
throw new TypeSafeError('"$" is reserved for the full result; rename that question.');
|
|
85
|
+
const { model, ...request } = options;
|
|
86
|
+
if (model !== undefined && model.trim() === "")
|
|
87
|
+
throw new TypeSafeError("The model override is blank. Pass a model name, or omit `model`.");
|
|
88
|
+
const questions = Object.fromEntries(Object.entries(spec).map(([name, value]) => [name, toQuestion(name, value)]));
|
|
89
|
+
const { data, requestId } = await client()
|
|
90
|
+
.systemOne({ state, questions, model }, request)
|
|
91
|
+
.withResponse();
|
|
92
|
+
// Object.fromEntries defines own properties, so a question named "__proto__" lands as
|
|
93
|
+
// data rather than silently reassigning the prototype.
|
|
94
|
+
const answers = Object.fromEntries(Object.keys(spec).map((name) => {
|
|
95
|
+
const answer = data.answers[name];
|
|
96
|
+
if (answer === undefined)
|
|
97
|
+
throw new TypeSafeError(`The API returned no answer for question "${name}". Request ID: ${requestId ?? "unknown"}.`);
|
|
98
|
+
return [name, unwrap(name, questions[name], answer)];
|
|
99
|
+
}));
|
|
100
|
+
return { ...answers, $: { ...data, requestId } };
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@devjonaed/typesafe-ai",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Unofficial two-line wrapper around the TypeSafe AI SDK: ask typed questions, get unwrapped answers.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"typesafe",
|
|
7
|
+
"jev",
|
|
8
|
+
"system-one",
|
|
9
|
+
"llm",
|
|
10
|
+
"classification",
|
|
11
|
+
"typescript",
|
|
12
|
+
"unofficial"
|
|
13
|
+
],
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public"
|
|
17
|
+
},
|
|
18
|
+
"author": "Md Jonaed Hasan",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/jonaed1230/typesafe-ai.git"
|
|
22
|
+
},
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/jonaed1230/typesafe-ai/issues"
|
|
25
|
+
},
|
|
26
|
+
"homepage": "https://github.com/jonaed1230/typesafe-ai#readme",
|
|
27
|
+
"type": "module",
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"default": "./dist/index.js"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"dist",
|
|
36
|
+
"src",
|
|
37
|
+
"CHANGELOG.md"
|
|
38
|
+
],
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=20"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "tsc",
|
|
44
|
+
"test": "npm run build && node --test",
|
|
45
|
+
"check:docs": "node scripts/check-readme.mjs",
|
|
46
|
+
"prepublishOnly": "npm test && npm run check:docs"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@typesafe-ai/sdk": "^0.6.0"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"typescript": "^5.9.0"
|
|
53
|
+
}
|
|
54
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import {
|
|
2
|
+
TypeSafeClient,
|
|
3
|
+
TypeSafeError,
|
|
4
|
+
choice,
|
|
5
|
+
noul,
|
|
6
|
+
type ChoiceQuestion,
|
|
7
|
+
type EntryType,
|
|
8
|
+
type NoulQuestion,
|
|
9
|
+
type Question,
|
|
10
|
+
type Questions,
|
|
11
|
+
type ResultFor,
|
|
12
|
+
type RequestOptions,
|
|
13
|
+
type ScoreQuestion,
|
|
14
|
+
type SystemOneResult,
|
|
15
|
+
type TypeSafeClientConfig,
|
|
16
|
+
} from "@typesafe-ai/sdk";
|
|
17
|
+
|
|
18
|
+
export { choice, noul, score } from "@typesafe-ai/sdk";
|
|
19
|
+
export type { EntryType, Question, SystemOneResult } from "@typesafe-ai/sdk";
|
|
20
|
+
|
|
21
|
+
/** A question written the short way, or a full SDK question object. */
|
|
22
|
+
export type Spec = string | readonly string[] | Question;
|
|
23
|
+
|
|
24
|
+
/** Questions keyed by the name their answer is returned under. */
|
|
25
|
+
export type SpecMap = Record<string, Spec>;
|
|
26
|
+
|
|
27
|
+
/** The unwrapped answer for one spec: a probability, a label, or a score. */
|
|
28
|
+
export type Answer<S> = S extends string
|
|
29
|
+
? number
|
|
30
|
+
: S extends readonly (infer L)[]
|
|
31
|
+
? L extends string
|
|
32
|
+
? L
|
|
33
|
+
: never
|
|
34
|
+
: S extends ChoiceQuestion<infer C>
|
|
35
|
+
? keyof C & string
|
|
36
|
+
: S extends ScoreQuestion<infer _>
|
|
37
|
+
? number
|
|
38
|
+
: S extends NoulQuestion
|
|
39
|
+
? number
|
|
40
|
+
: never;
|
|
41
|
+
|
|
42
|
+
/** The SDK question a spec expands to, so `$` keeps its precise answer types. */
|
|
43
|
+
export type QuestionOf<S> = S extends string
|
|
44
|
+
? NoulQuestion
|
|
45
|
+
: S extends readonly (infer L)[]
|
|
46
|
+
? [L] extends [string]
|
|
47
|
+
? ChoiceQuestion<{ [Label in L]: null }>
|
|
48
|
+
: never
|
|
49
|
+
: S extends Question
|
|
50
|
+
? S
|
|
51
|
+
: never;
|
|
52
|
+
|
|
53
|
+
/** The full SDK result, plus the request ID for the call that produced it. */
|
|
54
|
+
export type Meta<S extends SpecMap> = SystemOneResult<
|
|
55
|
+
{ [K in keyof S]: QuestionOf<S[K]> } & Questions
|
|
56
|
+
> & {
|
|
57
|
+
/** Request ID from `x-typesafe-request-id`, or `undefined` when absent. Quote it to support. */
|
|
58
|
+
readonly requestId: string | undefined;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/** Unwrapped answers by name, plus `$` holding the full SDK result. */
|
|
62
|
+
export type Answers<S extends SpecMap> = { [K in keyof S]: Answer<S[K]> } & {
|
|
63
|
+
$: Meta<S>;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/** The question types this version understands. */
|
|
67
|
+
const TYPES = new Set(["noul", "choice", "score"]);
|
|
68
|
+
|
|
69
|
+
const toQuestion = (name: string, spec: Spec): Question => {
|
|
70
|
+
if (typeof spec === "string") return noul(spec);
|
|
71
|
+
|
|
72
|
+
if (Array.isArray(spec)) {
|
|
73
|
+
const bad = spec.find((label) => typeof label !== "string");
|
|
74
|
+
if (bad !== undefined)
|
|
75
|
+
throw new TypeSafeError(
|
|
76
|
+
`Choice question "${name}" has a non-string label (${JSON.stringify(bad)}); labels must be strings.`,
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
// Mirror the SDK's own rule for score rubrics: a question nobody can answer is a
|
|
80
|
+
// mistake worth catching here, not a 422 from the server.
|
|
81
|
+
const labels = new Set<string>(spec);
|
|
82
|
+
if (labels.size !== spec.length)
|
|
83
|
+
throw new TypeSafeError(
|
|
84
|
+
`Choice question "${name}" repeats a label; every label must be distinct.`,
|
|
85
|
+
);
|
|
86
|
+
if (labels.size < 2)
|
|
87
|
+
throw new TypeSafeError(
|
|
88
|
+
`Choice question "${name}" has ${labels.size} label${labels.size === 1 ? "" : "s"}; at least two are required.`,
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
return choice(null, Object.fromEntries([...labels].map((label) => [label, null])));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Anything else has to be a question object, or the request would be silently malformed
|
|
95
|
+
// and the API would answer a question nobody asked.
|
|
96
|
+
if (spec === null || typeof spec !== "object" || !TYPES.has((spec as Question).type))
|
|
97
|
+
throw new TypeSafeError(
|
|
98
|
+
`Question "${name}" must be a string, an array of at least two labels, or a question from choice(), score() or noul(); got ${describe(spec)}.`,
|
|
99
|
+
);
|
|
100
|
+
return spec as Question;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const describe = (value: unknown): string =>
|
|
104
|
+
value === null ? "null" : Array.isArray(value) ? "an array" : typeof value;
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Pull the one value a caller wants out of an answer.
|
|
108
|
+
*
|
|
109
|
+
* The answer has to match the question that was asked. A mismatch means the API and this
|
|
110
|
+
* package disagree about the request, and returning the wrong field would look like a
|
|
111
|
+
* plausible answer rather than a bug.
|
|
112
|
+
*/
|
|
113
|
+
const unwrap = (name: string, asked: Question, answer: ResultFor<Question> | undefined) => {
|
|
114
|
+
if (answer === null || typeof answer !== "object")
|
|
115
|
+
throw new TypeSafeError(
|
|
116
|
+
`The API answered question "${name}" with ${describe(answer)} instead of an answer.`,
|
|
117
|
+
);
|
|
118
|
+
if (answer.type !== asked.type)
|
|
119
|
+
throw new TypeSafeError(
|
|
120
|
+
`The API answered question "${name}" with a ${JSON.stringify(answer.type)} answer, but a ${JSON.stringify(asked.type)} question was asked.`,
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
switch (answer.type) {
|
|
124
|
+
case "noul":
|
|
125
|
+
return answer.noul;
|
|
126
|
+
case "choice":
|
|
127
|
+
return answer.choice;
|
|
128
|
+
case "score":
|
|
129
|
+
return answer.score;
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
let shared: TypeSafeClient | undefined;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Replace the client `ask` uses. Call once at startup, or not at all.
|
|
137
|
+
*
|
|
138
|
+
* @throws {TypeSafeError} `apiKey` is present but blank. The SDK would accept it and fail
|
|
139
|
+
* later with a 401; failing here points at the unset variable that actually caused it.
|
|
140
|
+
*/
|
|
141
|
+
export const configure = (config: TypeSafeClientConfig = {}): TypeSafeClient => {
|
|
142
|
+
if (config.apiKey !== undefined && config.apiKey.trim() === "")
|
|
143
|
+
throw new TypeSafeError(
|
|
144
|
+
"The API key is blank. Pass a real key, or omit `apiKey` to read TYPESAFE_API_KEY from the environment.",
|
|
145
|
+
);
|
|
146
|
+
return (shared = new TypeSafeClient(config));
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
/** The client `ask` uses, created from the environment on first use. */
|
|
150
|
+
export const client = (): TypeSafeClient => (shared ??= new TypeSafeClient());
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Ask named questions about some state and get the answers back unwrapped.
|
|
154
|
+
*
|
|
155
|
+
* A string is a yes/no question and answers with a probability from 0 to 1.
|
|
156
|
+
* An array of labels is a choice and answers with the selected label.
|
|
157
|
+
* `choice()`, `score()` and `noul()` cover the cases that need descriptions.
|
|
158
|
+
*
|
|
159
|
+
* @example
|
|
160
|
+
* const a = await ask("I was charged twice. Fix this ASAP.", {
|
|
161
|
+
* category: ["billing", "technical", "other"],
|
|
162
|
+
* urgent: "Is the customer angry?",
|
|
163
|
+
* });
|
|
164
|
+
* a.category; // "billing"
|
|
165
|
+
* a.urgent; // 0.93
|
|
166
|
+
* a.$.answers.category.confidence; // 0.88
|
|
167
|
+
* a.$.requestId; // "req_01a0ca7f5a747639aaac05b46c7dc7db"
|
|
168
|
+
*/
|
|
169
|
+
export async function ask<const S extends SpecMap>(
|
|
170
|
+
state: EntryType,
|
|
171
|
+
spec: S,
|
|
172
|
+
options: RequestOptions & { model?: string } = {},
|
|
173
|
+
): Promise<Answers<S>> {
|
|
174
|
+
if (spec === null || typeof spec !== "object" || Array.isArray(spec))
|
|
175
|
+
throw new TypeSafeError(
|
|
176
|
+
`Questions must be an object keyed by answer name; got ${describe(spec)}.`,
|
|
177
|
+
);
|
|
178
|
+
if ("$" in spec)
|
|
179
|
+
throw new TypeSafeError(
|
|
180
|
+
'"$" is reserved for the full result; rename that question.',
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
const { model, ...request } = options;
|
|
184
|
+
if (model !== undefined && model.trim() === "")
|
|
185
|
+
throw new TypeSafeError("The model override is blank. Pass a model name, or omit `model`.");
|
|
186
|
+
const questions = Object.fromEntries(
|
|
187
|
+
Object.entries(spec).map(([name, value]) => [name, toQuestion(name, value)]),
|
|
188
|
+
) as Questions;
|
|
189
|
+
const { data, requestId } = await client()
|
|
190
|
+
.systemOne({ state, questions, model }, request)
|
|
191
|
+
.withResponse();
|
|
192
|
+
|
|
193
|
+
// Object.fromEntries defines own properties, so a question named "__proto__" lands as
|
|
194
|
+
// data rather than silently reassigning the prototype.
|
|
195
|
+
const answers = Object.fromEntries(
|
|
196
|
+
Object.keys(spec).map((name) => {
|
|
197
|
+
const answer = data.answers[name];
|
|
198
|
+
if (answer === undefined)
|
|
199
|
+
throw new TypeSafeError(
|
|
200
|
+
`The API returned no answer for question "${name}". Request ID: ${requestId ?? "unknown"}.`,
|
|
201
|
+
);
|
|
202
|
+
return [name, unwrap(name, questions[name]!, answer)];
|
|
203
|
+
}),
|
|
204
|
+
);
|
|
205
|
+
return { ...answers, $: { ...data, requestId } } as Answers<S>;
|
|
206
|
+
}
|