cerno-sdk 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/README.md +103 -0
- package/dist/cjs/answers.d.ts +43 -0
- package/dist/cjs/answers.js +91 -0
- package/dist/cjs/builder.d.ts +31 -0
- package/dist/cjs/builder.js +53 -0
- package/dist/cjs/client.d.ts +39 -0
- package/dist/cjs/client.js +98 -0
- package/dist/cjs/errors.d.ts +37 -0
- package/dist/cjs/errors.js +69 -0
- package/dist/cjs/index.d.ts +12 -0
- package/dist/cjs/index.js +23 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/types.d.ts +117 -0
- package/dist/cjs/types.js +7 -0
- package/dist/esm/answers.d.ts +43 -0
- package/dist/esm/answers.js +87 -0
- package/dist/esm/builder.d.ts +31 -0
- package/dist/esm/builder.js +49 -0
- package/dist/esm/client.d.ts +39 -0
- package/dist/esm/client.js +94 -0
- package/dist/esm/errors.d.ts +37 -0
- package/dist/esm/errors.js +60 -0
- package/dist/esm/index.d.ts +12 -0
- package/dist/esm/index.js +11 -0
- package/dist/esm/types.d.ts +117 -0
- package/dist/esm/types.js +6 -0
- package/package.json +37 -0
package/README.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# cerno-sdk
|
|
2
|
+
|
|
3
|
+
Three kinds of question about a piece of text, answered by a locally hosted model:
|
|
4
|
+
|
|
5
|
+
- **noul** — how likely is the answer yes
|
|
6
|
+
- **choice** — which one of up to 20 options
|
|
7
|
+
- **score** — where on a rubric of 2–10 levels
|
|
8
|
+
|
|
9
|
+
Each question is one forward pass, so answers come back in tens of milliseconds.
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install cerno-sdk
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { Client } from "cerno-sdk";
|
|
17
|
+
|
|
18
|
+
const client = new Client("http://localhost:3000");
|
|
19
|
+
|
|
20
|
+
const answers = await client
|
|
21
|
+
.systemone("Ticket: server room at 31C, rising, servers throttling.")
|
|
22
|
+
.noul("urgent", "Is this urgent?")
|
|
23
|
+
.choice("team", "Which team?", ["IT", "Facility", "HR"])
|
|
24
|
+
.score("sev", "How severe?", ["harmless", "minor", "moderate", "high", "critical"])
|
|
25
|
+
.send();
|
|
26
|
+
|
|
27
|
+
answers.noul("urgent"); // 0.991
|
|
28
|
+
answers.choice("team"); // "Facility"
|
|
29
|
+
answers.score("sev"); // 5
|
|
30
|
+
answers.legend("sev"); // "critical"
|
|
31
|
+
answers.confidence("team"); // 0.939
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Questions in one call share their state, so the text is sent and prefilled once.
|
|
35
|
+
|
|
36
|
+
A choice whose options speak for themselves takes two arguments:
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
.choice("mood", ["positive", "neutral", "negative"])
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Reading the answer honestly
|
|
43
|
+
|
|
44
|
+
Every answer carries the evidence it came from:
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
answers.truncated("team"); // a label fell outside the host's reporting window
|
|
48
|
+
answers.truncatedLabels("team"); // ["C"] — which ones; their raw_logprobs entry is a bound
|
|
49
|
+
answers.get("team").raw_logprobs; // { A: -4.54, B: -0.02, ... }
|
|
50
|
+
answers.get("team").probabilities; // per option, in request order
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`truncated` is worth checking when you act on a probability rather than on the winner: it means
|
|
54
|
+
at least one option ranked below everything the host reported, so its probability is an upper
|
|
55
|
+
bound, not an observation.
|
|
56
|
+
|
|
57
|
+
To flatten an overconfident model, scale the logits before they are normalised:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
await client.systemone(text).calibration(2.5).noul("urgent", "Is this urgent?").send();
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Above 1 flattens, below 1 sharpens, and `raw_logprobs` is unaffected either way.
|
|
64
|
+
|
|
65
|
+
`answers.get(id)` is a discriminated union, so narrowing on `type` gives you the full shape:
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
const answer = answers.get("team");
|
|
69
|
+
if (answer.type === "choice") {
|
|
70
|
+
answer.probabilities[0].option; // typed
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Errors
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
import { ApiError } from "cerno-sdk";
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
await client.systemone(text).choice("team", "Which?", options).send();
|
|
81
|
+
} catch (err) {
|
|
82
|
+
if (err instanceof ApiError) {
|
|
83
|
+
err.code; // "too_many_options" — branch on this, never on the message
|
|
84
|
+
err.questionId; // "team"
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`UnexpectedResponse` is thrown instead when a non-2xx body is not a cerno error at all, which
|
|
90
|
+
usually means a proxy between you and the service. `TransportError` means the service could not
|
|
91
|
+
be reached or did not answer within `timeoutMs`; the original error is its `cause`. All three
|
|
92
|
+
extend `CernoError`.
|
|
93
|
+
|
|
94
|
+
## Options
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
new Client({
|
|
98
|
+
baseUrl: "http://localhost:3000",
|
|
99
|
+
timeoutMs: 60_000,
|
|
100
|
+
headers: { "x-request-id": id },
|
|
101
|
+
fetch: customFetch, // injected for testing or custom transports
|
|
102
|
+
});
|
|
103
|
+
```
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Answer, SystemOneResponse, Usage } from "./types.ts";
|
|
2
|
+
/**
|
|
3
|
+
* The answers to one request, with accessors that fail loudly on the wrong id or type.
|
|
4
|
+
*
|
|
5
|
+
* Reaching for `noul("team")` when `team` was a choice is a programming mistake, not a runtime
|
|
6
|
+
* condition, so it throws rather than handing back a default.
|
|
7
|
+
*/
|
|
8
|
+
export declare class Answers {
|
|
9
|
+
readonly model: string;
|
|
10
|
+
readonly usage: Usage;
|
|
11
|
+
readonly timingMs: number;
|
|
12
|
+
private readonly response;
|
|
13
|
+
constructor(response: SystemOneResponse);
|
|
14
|
+
/** The raw answer for `id`. */
|
|
15
|
+
get(id: string): Answer;
|
|
16
|
+
ids(): string[];
|
|
17
|
+
/** Probability that the answer to `id` is yes. */
|
|
18
|
+
noul(id: string): number;
|
|
19
|
+
/** The winning option for `id`. */
|
|
20
|
+
choice(id: string): string;
|
|
21
|
+
/** The winning option's position in the request. */
|
|
22
|
+
index(id: string): number;
|
|
23
|
+
/** The winning level for `id`, 1-based. */
|
|
24
|
+
score(id: string): number;
|
|
25
|
+
/** The probability-weighted mean level for `id`. */
|
|
26
|
+
expectedScore(id: string): number;
|
|
27
|
+
legend(id: string): string;
|
|
28
|
+
/**
|
|
29
|
+
* How peaked the distribution behind `id` was, in 0..=1. A noul has none, as in JEV: its
|
|
30
|
+
* probability is already the whole answer.
|
|
31
|
+
*/
|
|
32
|
+
confidence(id: string): number;
|
|
33
|
+
/**
|
|
34
|
+
* Whether some label for `id` fell outside the host's reporting window. When true, that
|
|
35
|
+
* label's probability is an upper bound rather than an observation.
|
|
36
|
+
*/
|
|
37
|
+
truncated(id: string): boolean;
|
|
38
|
+
/** The labels for `id` whose logprob is an upper bound rather than an observation. */
|
|
39
|
+
truncatedLabels(id: string): string[];
|
|
40
|
+
/** The response exactly as the service sent it. */
|
|
41
|
+
raw(): SystemOneResponse;
|
|
42
|
+
private typed;
|
|
43
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Answers = void 0;
|
|
4
|
+
const errors_ts_1 = require("./errors.js");
|
|
5
|
+
/**
|
|
6
|
+
* The answers to one request, with accessors that fail loudly on the wrong id or type.
|
|
7
|
+
*
|
|
8
|
+
* Reaching for `noul("team")` when `team` was a choice is a programming mistake, not a runtime
|
|
9
|
+
* condition, so it throws rather than handing back a default.
|
|
10
|
+
*/
|
|
11
|
+
class Answers {
|
|
12
|
+
model;
|
|
13
|
+
usage;
|
|
14
|
+
timingMs;
|
|
15
|
+
// Declared explicitly rather than as a constructor parameter property: Node's native
|
|
16
|
+
// type-stripping does not support those, and the test suite runs the sources directly.
|
|
17
|
+
response;
|
|
18
|
+
constructor(response) {
|
|
19
|
+
this.response = response;
|
|
20
|
+
this.model = response.model;
|
|
21
|
+
this.usage = response.usage;
|
|
22
|
+
this.timingMs = response.timing_ms.total;
|
|
23
|
+
}
|
|
24
|
+
/** The raw answer for `id`. */
|
|
25
|
+
get(id) {
|
|
26
|
+
const answer = this.response.answers[id];
|
|
27
|
+
if (answer === undefined)
|
|
28
|
+
throw new errors_ts_1.MissingAnswer(id);
|
|
29
|
+
return answer;
|
|
30
|
+
}
|
|
31
|
+
ids() {
|
|
32
|
+
return Object.keys(this.response.answers);
|
|
33
|
+
}
|
|
34
|
+
/** Probability that the answer to `id` is yes. */
|
|
35
|
+
noul(id) {
|
|
36
|
+
return this.typed(id, "noul").noul;
|
|
37
|
+
}
|
|
38
|
+
/** The winning option for `id`. */
|
|
39
|
+
choice(id) {
|
|
40
|
+
return this.typed(id, "choice").choice;
|
|
41
|
+
}
|
|
42
|
+
/** The winning option's position in the request. */
|
|
43
|
+
index(id) {
|
|
44
|
+
return this.typed(id, "choice").index;
|
|
45
|
+
}
|
|
46
|
+
/** The winning level for `id`, 1-based. */
|
|
47
|
+
score(id) {
|
|
48
|
+
return this.typed(id, "score").score;
|
|
49
|
+
}
|
|
50
|
+
/** The probability-weighted mean level for `id`. */
|
|
51
|
+
expectedScore(id) {
|
|
52
|
+
return this.typed(id, "score").expected_score;
|
|
53
|
+
}
|
|
54
|
+
legend(id) {
|
|
55
|
+
return this.typed(id, "score").legend;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* How peaked the distribution behind `id` was, in 0..=1. A noul has none, as in JEV: its
|
|
59
|
+
* probability is already the whole answer.
|
|
60
|
+
*/
|
|
61
|
+
confidence(id) {
|
|
62
|
+
const answer = this.get(id);
|
|
63
|
+
if (answer.type === "noul") {
|
|
64
|
+
throw new errors_ts_1.WrongAnswerType(id, "choice or score", answer.type);
|
|
65
|
+
}
|
|
66
|
+
return answer.confidence;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Whether some label for `id` fell outside the host's reporting window. When true, that
|
|
70
|
+
* label's probability is an upper bound rather than an observation.
|
|
71
|
+
*/
|
|
72
|
+
truncated(id) {
|
|
73
|
+
return this.get(id).truncated;
|
|
74
|
+
}
|
|
75
|
+
/** The labels for `id` whose logprob is an upper bound rather than an observation. */
|
|
76
|
+
truncatedLabels(id) {
|
|
77
|
+
return this.get(id).truncated_labels ?? [];
|
|
78
|
+
}
|
|
79
|
+
/** The response exactly as the service sent it. */
|
|
80
|
+
raw() {
|
|
81
|
+
return this.response;
|
|
82
|
+
}
|
|
83
|
+
typed(id, expected) {
|
|
84
|
+
const answer = this.get(id);
|
|
85
|
+
if (answer.type !== expected) {
|
|
86
|
+
throw new errors_ts_1.WrongAnswerType(id, expected, answer.type);
|
|
87
|
+
}
|
|
88
|
+
return answer;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
exports.Answers = Answers;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Answers } from "./answers.ts";
|
|
2
|
+
import type { LevelSpec, SystemOneRequest } from "./types.ts";
|
|
3
|
+
/** A request under construction. Chain questions onto it, then `send()`. */
|
|
4
|
+
export declare class SystemOneBuilder {
|
|
5
|
+
private readonly request;
|
|
6
|
+
private readonly dispatch;
|
|
7
|
+
constructor(state: string, dispatch: (body: SystemOneRequest) => Promise<Answers>);
|
|
8
|
+
/** Name a model or a configured alias. The service's default applies otherwise. */
|
|
9
|
+
model(model: string): this;
|
|
10
|
+
/** Scale the label logits before normalising. Above 1 flattens, below 1 sharpens. */
|
|
11
|
+
calibration(temperature: number): this;
|
|
12
|
+
/** How likely the answer to `question` is yes. */
|
|
13
|
+
noul(id: string, question: string): this;
|
|
14
|
+
/**
|
|
15
|
+
* One of `options`.
|
|
16
|
+
*
|
|
17
|
+
* Called with two arguments the options come second and no question is sent, which is the
|
|
18
|
+
* right shape when the options speak for themselves.
|
|
19
|
+
*/
|
|
20
|
+
choice(id: string, question: string, options: string[]): this;
|
|
21
|
+
choice(id: string, options: string[]): this;
|
|
22
|
+
/** A position on a rubric: a number for generated levels, or the level texts. */
|
|
23
|
+
score(id: string, question: string, levels: LevelSpec): this;
|
|
24
|
+
/**
|
|
25
|
+
* The request as it will be sent. Useful for logging, and for testing a chain without a
|
|
26
|
+
* server.
|
|
27
|
+
*/
|
|
28
|
+
body(): SystemOneRequest;
|
|
29
|
+
send(): Promise<Answers>;
|
|
30
|
+
private push;
|
|
31
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SystemOneBuilder = void 0;
|
|
4
|
+
/** A request under construction. Chain questions onto it, then `send()`. */
|
|
5
|
+
class SystemOneBuilder {
|
|
6
|
+
request;
|
|
7
|
+
dispatch;
|
|
8
|
+
constructor(state, dispatch) {
|
|
9
|
+
this.request = { state, questions: [] };
|
|
10
|
+
this.dispatch = dispatch;
|
|
11
|
+
}
|
|
12
|
+
/** Name a model or a configured alias. The service's default applies otherwise. */
|
|
13
|
+
model(model) {
|
|
14
|
+
this.request.model = model;
|
|
15
|
+
return this;
|
|
16
|
+
}
|
|
17
|
+
/** Scale the label logits before normalising. Above 1 flattens, below 1 sharpens. */
|
|
18
|
+
calibration(temperature) {
|
|
19
|
+
this.request.calibration = { temperature };
|
|
20
|
+
return this;
|
|
21
|
+
}
|
|
22
|
+
/** How likely the answer to `question` is yes. */
|
|
23
|
+
noul(id, question) {
|
|
24
|
+
return this.push({ id, noul: question });
|
|
25
|
+
}
|
|
26
|
+
choice(id, second, third) {
|
|
27
|
+
const options = third ?? second;
|
|
28
|
+
const question = third === undefined ? undefined : second;
|
|
29
|
+
return this.push({
|
|
30
|
+
id,
|
|
31
|
+
choice: question === undefined ? { options } : { question, options },
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
/** A position on a rubric: a number for generated levels, or the level texts. */
|
|
35
|
+
score(id, question, levels) {
|
|
36
|
+
return this.push({ id, score: { question, levels } });
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The request as it will be sent. Useful for logging, and for testing a chain without a
|
|
40
|
+
* server.
|
|
41
|
+
*/
|
|
42
|
+
body() {
|
|
43
|
+
return this.request;
|
|
44
|
+
}
|
|
45
|
+
send() {
|
|
46
|
+
return this.dispatch(this.request);
|
|
47
|
+
}
|
|
48
|
+
push(question) {
|
|
49
|
+
this.request.questions.push(question);
|
|
50
|
+
return this;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
exports.SystemOneBuilder = SystemOneBuilder;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { SystemOneBuilder } from "./builder.ts";
|
|
2
|
+
import type { ModelsResponse, SystemOneRequest } from "./types.ts";
|
|
3
|
+
export interface ClientOptions {
|
|
4
|
+
/** Defaults to `http://localhost:3000`. */
|
|
5
|
+
baseUrl?: string;
|
|
6
|
+
/** Milliseconds before a request is aborted. Defaults to 60000. */
|
|
7
|
+
timeoutMs?: number;
|
|
8
|
+
/** Sent with every request. */
|
|
9
|
+
headers?: Record<string, string>;
|
|
10
|
+
/** Injected for testing, or to route through a custom transport. */
|
|
11
|
+
fetch?: typeof globalThis.fetch;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Client for the cerno service.
|
|
15
|
+
*
|
|
16
|
+
* ```ts
|
|
17
|
+
* const client = new Client({ baseUrl: "http://localhost:3000" });
|
|
18
|
+
* const answers = await client
|
|
19
|
+
* .systemone("Ticket: server room at 31C, rising.")
|
|
20
|
+
* .noul("urgent", "Is this urgent?")
|
|
21
|
+
* .choice("team", "Which team?", ["IT", "Facility", "HR"])
|
|
22
|
+
* .send();
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export declare class Client {
|
|
26
|
+
private readonly baseUrl;
|
|
27
|
+
private readonly timeoutMs;
|
|
28
|
+
private readonly headers;
|
|
29
|
+
private readonly doFetch;
|
|
30
|
+
constructor(options?: ClientOptions | string);
|
|
31
|
+
/** Start a request about `state`. */
|
|
32
|
+
systemone(state: string): SystemOneBuilder;
|
|
33
|
+
/** The models this service will answer for. */
|
|
34
|
+
models(): Promise<ModelsResponse>;
|
|
35
|
+
/** Whether the service is up. False, not an error, when it cannot be reached in time. */
|
|
36
|
+
health(): Promise<boolean>;
|
|
37
|
+
private request;
|
|
38
|
+
}
|
|
39
|
+
export type { SystemOneRequest };
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Client = void 0;
|
|
4
|
+
const answers_ts_1 = require("./answers.js");
|
|
5
|
+
const builder_ts_1 = require("./builder.js");
|
|
6
|
+
const errors_ts_1 = require("./errors.js");
|
|
7
|
+
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
8
|
+
/**
|
|
9
|
+
* Client for the cerno service.
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* const client = new Client({ baseUrl: "http://localhost:3000" });
|
|
13
|
+
* const answers = await client
|
|
14
|
+
* .systemone("Ticket: server room at 31C, rising.")
|
|
15
|
+
* .noul("urgent", "Is this urgent?")
|
|
16
|
+
* .choice("team", "Which team?", ["IT", "Facility", "HR"])
|
|
17
|
+
* .send();
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
class Client {
|
|
21
|
+
baseUrl;
|
|
22
|
+
timeoutMs;
|
|
23
|
+
headers;
|
|
24
|
+
doFetch;
|
|
25
|
+
constructor(options = {}) {
|
|
26
|
+
const opts = typeof options === "string" ? { baseUrl: options } : options;
|
|
27
|
+
this.baseUrl = (opts.baseUrl ?? "http://localhost:3000").replace(/\/+$/, "");
|
|
28
|
+
this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
29
|
+
this.headers = opts.headers ?? {};
|
|
30
|
+
this.doFetch = opts.fetch ?? globalThis.fetch;
|
|
31
|
+
}
|
|
32
|
+
/** Start a request about `state`. */
|
|
33
|
+
systemone(state) {
|
|
34
|
+
return new builder_ts_1.SystemOneBuilder(state, async (body) => {
|
|
35
|
+
const response = await this.request("POST", "/v1/systemone", body);
|
|
36
|
+
return new answers_ts_1.Answers(response);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
/** The models this service will answer for. */
|
|
40
|
+
models() {
|
|
41
|
+
return this.request("GET", "/v1/models");
|
|
42
|
+
}
|
|
43
|
+
/** Whether the service is up. False, not an error, when it cannot be reached in time. */
|
|
44
|
+
async health() {
|
|
45
|
+
try {
|
|
46
|
+
const response = await this.doFetch(`${this.baseUrl}/health`, {
|
|
47
|
+
signal: AbortSignal.timeout(this.timeoutMs),
|
|
48
|
+
});
|
|
49
|
+
return response.ok;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async request(method, path, body) {
|
|
56
|
+
// An explicit abort, so a wedged host surfaces as a timeout rather than hanging forever.
|
|
57
|
+
const abort = AbortSignal.timeout(this.timeoutMs);
|
|
58
|
+
let response;
|
|
59
|
+
let text;
|
|
60
|
+
try {
|
|
61
|
+
response = await this.doFetch(`${this.baseUrl}${path}`, {
|
|
62
|
+
method,
|
|
63
|
+
signal: abort,
|
|
64
|
+
headers: {
|
|
65
|
+
...(body === undefined ? {} : { "content-type": "application/json" }),
|
|
66
|
+
...this.headers,
|
|
67
|
+
},
|
|
68
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
69
|
+
});
|
|
70
|
+
text = await response.text();
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
throw new errors_ts_1.TransportError(`could not reach cerno: ${String(err)}`, err);
|
|
74
|
+
}
|
|
75
|
+
if (response.ok) {
|
|
76
|
+
try {
|
|
77
|
+
return JSON.parse(text);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
throw new errors_ts_1.UnexpectedResponse(response.status, text);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
let parsed;
|
|
84
|
+
try {
|
|
85
|
+
const candidate = JSON.parse(text);
|
|
86
|
+
if (candidate && typeof candidate.code === "string")
|
|
87
|
+
parsed = candidate;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
// Falls through to UnexpectedResponse below.
|
|
91
|
+
}
|
|
92
|
+
// Not our error shape, so do not pretend to know what went wrong.
|
|
93
|
+
if (!parsed)
|
|
94
|
+
throw new errors_ts_1.UnexpectedResponse(response.status, text);
|
|
95
|
+
throw new errors_ts_1.ApiError(response.status, parsed);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
exports.Client = Client;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { ErrorCode, ErrorResponse } from "./types.ts";
|
|
2
|
+
export declare class CernoError extends Error {
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* The service answered with a structured failure.
|
|
6
|
+
*
|
|
7
|
+
* Branch on `code`, never on `message` — the codes are the contract, the prose is not.
|
|
8
|
+
*/
|
|
9
|
+
export declare class ApiError extends CernoError {
|
|
10
|
+
readonly status: number;
|
|
11
|
+
readonly code: ErrorCode;
|
|
12
|
+
readonly questionId?: string;
|
|
13
|
+
constructor(status: number, body: ErrorResponse);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The service could not be reached, or did not answer within the timeout. The error `fetch`
|
|
17
|
+
* threw is kept as `cause`.
|
|
18
|
+
*/
|
|
19
|
+
export declare class TransportError extends CernoError {
|
|
20
|
+
constructor(message: string, cause: unknown);
|
|
21
|
+
}
|
|
22
|
+
/** A non-2xx response that was not shaped like a cerno error — a proxy, most likely. */
|
|
23
|
+
export declare class UnexpectedResponse extends CernoError {
|
|
24
|
+
readonly status: number;
|
|
25
|
+
readonly body: string;
|
|
26
|
+
constructor(status: number, body: string);
|
|
27
|
+
}
|
|
28
|
+
export declare class MissingAnswer extends CernoError {
|
|
29
|
+
readonly questionId: string;
|
|
30
|
+
constructor(questionId: string);
|
|
31
|
+
}
|
|
32
|
+
export declare class WrongAnswerType extends CernoError {
|
|
33
|
+
readonly questionId: string;
|
|
34
|
+
readonly expected: string;
|
|
35
|
+
readonly actual: string;
|
|
36
|
+
constructor(questionId: string, expected: string, actual: string);
|
|
37
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WrongAnswerType = exports.MissingAnswer = exports.UnexpectedResponse = exports.TransportError = exports.ApiError = exports.CernoError = void 0;
|
|
4
|
+
class CernoError extends Error {
|
|
5
|
+
}
|
|
6
|
+
exports.CernoError = CernoError;
|
|
7
|
+
/**
|
|
8
|
+
* The service answered with a structured failure.
|
|
9
|
+
*
|
|
10
|
+
* Branch on `code`, never on `message` — the codes are the contract, the prose is not.
|
|
11
|
+
*/
|
|
12
|
+
class ApiError extends CernoError {
|
|
13
|
+
status;
|
|
14
|
+
code;
|
|
15
|
+
questionId;
|
|
16
|
+
constructor(status, body) {
|
|
17
|
+
super(`cerno returned ${status} (${body.code}): ${body.message}`);
|
|
18
|
+
this.name = "ApiError";
|
|
19
|
+
this.status = status;
|
|
20
|
+
this.code = body.code;
|
|
21
|
+
this.questionId = body.question_id;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
exports.ApiError = ApiError;
|
|
25
|
+
/**
|
|
26
|
+
* The service could not be reached, or did not answer within the timeout. The error `fetch`
|
|
27
|
+
* threw is kept as `cause`.
|
|
28
|
+
*/
|
|
29
|
+
class TransportError extends CernoError {
|
|
30
|
+
constructor(message, cause) {
|
|
31
|
+
super(message, { cause });
|
|
32
|
+
this.name = "TransportError";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
exports.TransportError = TransportError;
|
|
36
|
+
/** A non-2xx response that was not shaped like a cerno error — a proxy, most likely. */
|
|
37
|
+
class UnexpectedResponse extends CernoError {
|
|
38
|
+
status;
|
|
39
|
+
body;
|
|
40
|
+
constructor(status, body) {
|
|
41
|
+
super(`cerno returned ${status}: ${body.slice(0, 200)}`);
|
|
42
|
+
this.name = "UnexpectedResponse";
|
|
43
|
+
this.status = status;
|
|
44
|
+
this.body = body;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
exports.UnexpectedResponse = UnexpectedResponse;
|
|
48
|
+
class MissingAnswer extends CernoError {
|
|
49
|
+
questionId;
|
|
50
|
+
constructor(questionId) {
|
|
51
|
+
super(`no answer for question ${JSON.stringify(questionId)}`);
|
|
52
|
+
this.name = "MissingAnswer";
|
|
53
|
+
this.questionId = questionId;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
exports.MissingAnswer = MissingAnswer;
|
|
57
|
+
class WrongAnswerType extends CernoError {
|
|
58
|
+
questionId;
|
|
59
|
+
expected;
|
|
60
|
+
actual;
|
|
61
|
+
constructor(questionId, expected, actual) {
|
|
62
|
+
super(`question ${JSON.stringify(questionId)} answered with a ${actual}, not a ${expected}`);
|
|
63
|
+
this.name = "WrongAnswerType";
|
|
64
|
+
this.questionId = questionId;
|
|
65
|
+
this.expected = expected;
|
|
66
|
+
this.actual = actual;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
exports.WrongAnswerType = WrongAnswerType;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript client for cerno.
|
|
3
|
+
*
|
|
4
|
+
* cerno answers three kinds of question about a piece of text — is it true (`noul`), which one
|
|
5
|
+
* is it (`choice`), where on a scale does it sit (`score`) — using a locally hosted model. Each
|
|
6
|
+
* question is one forward pass, so answers come back in tens of milliseconds.
|
|
7
|
+
*/
|
|
8
|
+
export { Answers } from "./answers.ts";
|
|
9
|
+
export { SystemOneBuilder } from "./builder.ts";
|
|
10
|
+
export { Client, type ClientOptions } from "./client.ts";
|
|
11
|
+
export { ApiError, CernoError, MissingAnswer, TransportError, UnexpectedResponse, WrongAnswerType, } from "./errors.ts";
|
|
12
|
+
export type * from "./types.ts";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* TypeScript client for cerno.
|
|
4
|
+
*
|
|
5
|
+
* cerno answers three kinds of question about a piece of text — is it true (`noul`), which one
|
|
6
|
+
* is it (`choice`), where on a scale does it sit (`score`) — using a locally hosted model. Each
|
|
7
|
+
* question is one forward pass, so answers come back in tens of milliseconds.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.WrongAnswerType = exports.UnexpectedResponse = exports.TransportError = exports.MissingAnswer = exports.CernoError = exports.ApiError = exports.Client = exports.SystemOneBuilder = exports.Answers = void 0;
|
|
11
|
+
var answers_ts_1 = require("./answers.js");
|
|
12
|
+
Object.defineProperty(exports, "Answers", { enumerable: true, get: function () { return answers_ts_1.Answers; } });
|
|
13
|
+
var builder_ts_1 = require("./builder.js");
|
|
14
|
+
Object.defineProperty(exports, "SystemOneBuilder", { enumerable: true, get: function () { return builder_ts_1.SystemOneBuilder; } });
|
|
15
|
+
var client_ts_1 = require("./client.js");
|
|
16
|
+
Object.defineProperty(exports, "Client", { enumerable: true, get: function () { return client_ts_1.Client; } });
|
|
17
|
+
var errors_ts_1 = require("./errors.js");
|
|
18
|
+
Object.defineProperty(exports, "ApiError", { enumerable: true, get: function () { return errors_ts_1.ApiError; } });
|
|
19
|
+
Object.defineProperty(exports, "CernoError", { enumerable: true, get: function () { return errors_ts_1.CernoError; } });
|
|
20
|
+
Object.defineProperty(exports, "MissingAnswer", { enumerable: true, get: function () { return errors_ts_1.MissingAnswer; } });
|
|
21
|
+
Object.defineProperty(exports, "TransportError", { enumerable: true, get: function () { return errors_ts_1.TransportError; } });
|
|
22
|
+
Object.defineProperty(exports, "UnexpectedResponse", { enumerable: true, get: function () { return errors_ts_1.UnexpectedResponse; } });
|
|
23
|
+
Object.defineProperty(exports, "WrongAnswerType", { enumerable: true, get: function () { return errors_ts_1.WrongAnswerType; } });
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"commonjs"}
|