@butlerbot/sdk 0.0.45 → 0.0.47
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/dist/config.d.ts +4 -0
- package/dist/config.js +5 -1
- package/dist/index.d.ts +12 -0
- package/dist/index.js +15 -1
- package/dist/link/protocol.d.ts +10 -0
- package/dist/modules/conversation.d.ts +23 -0
- package/dist/modules/judge.d.ts +32 -0
- package/dist/modules/judge.js +29 -0
- package/dist/modules/transport.d.ts +20 -0
- package/dist/modules/transport.js +48 -0
- package/dist/modules/transport_link.js +6 -0
- package/dist/modules/transport_sse.js +7 -1
- package/dist/types/conversation/context.d.ts +28 -0
- package/dist/types/conversation/context.js +2 -0
- package/dist/types/conversation/v3/conversation_v3.d.ts +8 -0
- package/dist/types/judge/index.d.ts +1 -0
- package/dist/types/judge/index.js +17 -0
- package/dist/types/judge/judge.d.ts +103 -0
- package/dist/types/judge/judge.js +12 -0
- package/dist/types/response/v5/ai_response_v5.d.ts +8 -0
- package/dist/types/type_registry.d.ts +2 -0
- package/dist/types/type_registry.js +2 -0
- package/package.json +1 -1
- package/readme.md +55 -2
package/dist/config.d.ts
CHANGED
|
@@ -69,6 +69,10 @@ export declare const CONFIG: {
|
|
|
69
69
|
/** The inbox. One delivery's answer is `${base}/${deliveryId}/answer`. */
|
|
70
70
|
base: string;
|
|
71
71
|
};
|
|
72
|
+
judge: {
|
|
73
|
+
/** A decision on stated facts, by a decision model. */
|
|
74
|
+
base: string;
|
|
75
|
+
};
|
|
72
76
|
};
|
|
73
77
|
};
|
|
74
78
|
export type APIPath = keyof typeof CONFIG.paths.conversation;
|
package/dist/config.js
CHANGED
|
@@ -60,7 +60,11 @@ exports.CONFIG = {
|
|
|
60
60
|
outreach: {
|
|
61
61
|
/** The inbox. One delivery's answer is `${base}/${deliveryId}/answer`. */
|
|
62
62
|
base: "/api/outreach",
|
|
63
|
-
}
|
|
63
|
+
},
|
|
64
|
+
judge: {
|
|
65
|
+
/** A decision on stated facts, by a decision model. */
|
|
66
|
+
base: "/api/judge",
|
|
67
|
+
},
|
|
64
68
|
}
|
|
65
69
|
};
|
|
66
70
|
const withoutTrailingSlash = (url) => url.replace(/\/+$/, "");
|
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ import { Conversation, ConversationOptions } from "./modules/conversation";
|
|
|
4
4
|
import { UsagePolicyDataOptions } from "./modules/usage";
|
|
5
5
|
import { type CancelJobOptions, type GetJobJournalOptions, type GetJobOptions, type SetPhaseModelOptions, type ListJobsOptions, type ResumeJobOptions, type UpdateJobOptions, type UpdateJobSettingsOptions } from "./modules/jobs";
|
|
6
6
|
import { type AnswerDeliveryOptions, type ListDeliveriesOptions } from "./modules/outreach";
|
|
7
|
+
import { type JudgeOptions } from "./modules/judge";
|
|
8
|
+
import type { JudgeQuestions } from "./types/judge";
|
|
7
9
|
type OptionalApiKey<T> = Omit<T, "apiKey"> & {
|
|
8
10
|
/** Optional API key, defaults to API key specified in client */
|
|
9
11
|
apiKey?: string;
|
|
@@ -64,6 +66,13 @@ export declare class ButlerBotClient {
|
|
|
64
66
|
listDeliveries(config?: OptionalApiKey<ListDeliveriesOptions>): Promise<import("./types/type_registry").DeliveryListResponse>;
|
|
65
67
|
/** Answers a delivery. Throws a `ButlerBotAPIError` with `isConflict` when it was already answered */
|
|
66
68
|
answerDelivery(config: OptionalApiKey<AnswerDeliveryOptions>): Promise<import("./types/type_registry").DeliveryAnswerResponse>;
|
|
69
|
+
/**
|
|
70
|
+
* Asks the judge: a yes/no, pick-one or score decision on facts you state, by a decision
|
|
71
|
+
* model, in well under a second for a fraction of a cent. Answers come back keyed by
|
|
72
|
+
* question id and typed by the question. Throws a `ButlerBotAPIError` with `isBadRequest`
|
|
73
|
+
* on a question the judge cannot ask, carrying its message
|
|
74
|
+
*/
|
|
75
|
+
judge<Q extends JudgeQuestions>(config: OptionalApiKey<JudgeOptions<Q>>): Promise<import("./types/judge").JudgeVerdict<Q>>;
|
|
67
76
|
/** The client's own server and key underneath whatever the call named itself. */
|
|
68
77
|
private forRequest;
|
|
69
78
|
}
|
|
@@ -78,6 +87,9 @@ export { listJobs, getJob, cancelJob, resumeJob, updateJob, updateJobSettings }
|
|
|
78
87
|
export type { JobsRequestOptions, ListJobsOptions, GetJobOptions, CancelJobOptions, ResumeJobOptions, UpdateJobOptions, UpdateJobSettingsOptions, } from "./modules/jobs";
|
|
79
88
|
export { listDeliveries, answerDelivery } from "./modules/outreach";
|
|
80
89
|
export type { OutreachRequestOptions, ListDeliveriesOptions, AnswerDeliveryOptions, } from "./modules/outreach";
|
|
90
|
+
export { judge } from "./modules/judge";
|
|
91
|
+
export type { JudgeRequestOptions, JudgeOptions } from "./modules/judge";
|
|
81
92
|
export { LinkConversationTransport } from "./modules/transport_link";
|
|
82
93
|
export { SSEConversationTransport } from "./modules/transport_sse";
|
|
83
94
|
export type { SteerResult, TurnStopMode, TurnStopped } from "./modules/transport";
|
|
95
|
+
export { RESERVED_TURN_FIELDS } from "./modules/transport";
|
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.SSEConversationTransport = exports.LinkConversationTransport = exports.answerDelivery = exports.listDeliveries = exports.updateJobSettings = exports.updateJob = exports.resumeJob = exports.cancelJob = exports.getJob = exports.listJobs = exports.ButlerBotAPIError = exports.Conversation = exports.ButlerBotClient = void 0;
|
|
17
|
+
exports.RESERVED_TURN_FIELDS = exports.SSEConversationTransport = exports.LinkConversationTransport = exports.judge = exports.answerDelivery = exports.listDeliveries = exports.updateJobSettings = exports.updateJob = exports.resumeJob = exports.cancelJob = exports.getJob = exports.listJobs = exports.ButlerBotAPIError = exports.Conversation = exports.ButlerBotClient = void 0;
|
|
18
18
|
const config_1 = require("./config");
|
|
19
19
|
const link_1 = require("./link");
|
|
20
20
|
const conversation_1 = require("./modules/conversation");
|
|
@@ -22,6 +22,7 @@ Object.defineProperty(exports, "Conversation", { enumerable: true, get: function
|
|
|
22
22
|
const usage_1 = require("./modules/usage");
|
|
23
23
|
const jobs_1 = require("./modules/jobs");
|
|
24
24
|
const outreach_1 = require("./modules/outreach");
|
|
25
|
+
const judge_1 = require("./modules/judge");
|
|
25
26
|
/**
|
|
26
27
|
* The options a caller actually gave, with the keys they left out removed.
|
|
27
28
|
*
|
|
@@ -112,6 +113,15 @@ class ButlerBotClient {
|
|
|
112
113
|
answerDelivery(config) {
|
|
113
114
|
return (0, outreach_1.answerDelivery)(this.forRequest(config));
|
|
114
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* Asks the judge: a yes/no, pick-one or score decision on facts you state, by a decision
|
|
118
|
+
* model, in well under a second for a fraction of a cent. Answers come back keyed by
|
|
119
|
+
* question id and typed by the question. Throws a `ButlerBotAPIError` with `isBadRequest`
|
|
120
|
+
* on a question the judge cannot ask, carrying its message
|
|
121
|
+
*/
|
|
122
|
+
judge(config) {
|
|
123
|
+
return (0, judge_1.judge)(this.forRequest(config));
|
|
124
|
+
}
|
|
115
125
|
/** The client's own server and key underneath whatever the call named itself. */
|
|
116
126
|
forRequest(config) {
|
|
117
127
|
return { serverURL: this.serverUrl, apiKey: this.apiKey, debug: this.debug, ...given(config) };
|
|
@@ -133,7 +143,11 @@ Object.defineProperty(exports, "updateJobSettings", { enumerable: true, get: fun
|
|
|
133
143
|
var outreach_2 = require("./modules/outreach");
|
|
134
144
|
Object.defineProperty(exports, "listDeliveries", { enumerable: true, get: function () { return outreach_2.listDeliveries; } });
|
|
135
145
|
Object.defineProperty(exports, "answerDelivery", { enumerable: true, get: function () { return outreach_2.answerDelivery; } });
|
|
146
|
+
var judge_2 = require("./modules/judge");
|
|
147
|
+
Object.defineProperty(exports, "judge", { enumerable: true, get: function () { return judge_2.judge; } });
|
|
136
148
|
var transport_link_1 = require("./modules/transport_link");
|
|
137
149
|
Object.defineProperty(exports, "LinkConversationTransport", { enumerable: true, get: function () { return transport_link_1.LinkConversationTransport; } });
|
|
138
150
|
var transport_sse_1 = require("./modules/transport_sse");
|
|
139
151
|
Object.defineProperty(exports, "SSEConversationTransport", { enumerable: true, get: function () { return transport_sse_1.SSEConversationTransport; } });
|
|
152
|
+
var transport_1 = require("./modules/transport");
|
|
153
|
+
Object.defineProperty(exports, "RESERVED_TURN_FIELDS", { enumerable: true, get: function () { return transport_1.RESERVED_TURN_FIELDS; } });
|
package/dist/link/protocol.d.ts
CHANGED
|
@@ -122,6 +122,16 @@ export type LinkClientPayloads = {
|
|
|
122
122
|
personality?: string;
|
|
123
123
|
/** Why Alfred is speaking on this one turn: a line for its system prompt, at most 500 characters. */
|
|
124
124
|
wake?: string;
|
|
125
|
+
/**
|
|
126
|
+
* What the platform attaches beside this turn's message: a `MessageContextItem[]`,
|
|
127
|
+
* JSON-encoded, passed to the server as it is. Absent when there are no items.
|
|
128
|
+
*/
|
|
129
|
+
context?: string;
|
|
130
|
+
/**
|
|
131
|
+
* Further turn parameters, which the Link service spreads into the server's query.
|
|
132
|
+
* Never one of the fields above or the credential; absent when there are none.
|
|
133
|
+
*/
|
|
134
|
+
extra?: Record<string, string>;
|
|
125
135
|
};
|
|
126
136
|
"conversation.end": {
|
|
127
137
|
sessionId: string;
|
|
@@ -6,6 +6,7 @@ import { RequestResponseV3, RequestResponseV4 } from "../types/type_registry";
|
|
|
6
6
|
import { RequestResponseV5 } from "../types/response/v5/dialogue_response_v5";
|
|
7
7
|
import { ConversationStateResponse } from "../types/state/convo_state_response";
|
|
8
8
|
import { ConversationAddress } from "../types/conversation/address";
|
|
9
|
+
import { MessageContextItem } from "../types/conversation/context";
|
|
9
10
|
import { TurnProgressEntry } from "../types/response/v4/turn_registry_v4";
|
|
10
11
|
import { TurnProgressEntryV5 } from "../types/response/v5/turn_registry_v5";
|
|
11
12
|
export type DialogueRequestParams = {
|
|
@@ -35,6 +36,28 @@ export type DialogueRequestParams = {
|
|
|
35
36
|
* 500 characters; the server refuses a longer one. Left off when blank.
|
|
36
37
|
*/
|
|
37
38
|
wake?: string;
|
|
39
|
+
/**
|
|
40
|
+
* What the platform attaches beside this turn's message, never inside it: what it replies
|
|
41
|
+
* to, who it mentions, where it was sent. See `MessageContextItem` for the shape and the
|
|
42
|
+
* bounds (8 items, an 80-character title, a 4 000-character text, 12 000 characters in
|
|
43
|
+
* all), which the server enforces by refusing the turn. Passed per `send()`/`ask()`, since
|
|
44
|
+
* it belongs to the one message; left off when empty.
|
|
45
|
+
*/
|
|
46
|
+
context?: MessageContextItem[];
|
|
47
|
+
/**
|
|
48
|
+
* Further parameters for this one turn, forwarded to the server verbatim: each entry
|
|
49
|
+
* becomes a query parameter over SSE, and a field of `extra` on the `conversation.chat`
|
|
50
|
+
* frame over a Link, which the Link service spreads into the same query. The SDK does not
|
|
51
|
+
* read them; the server validates whatever arrives, as it does its named parameters.
|
|
52
|
+
*
|
|
53
|
+
* This is how a new turn parameter reaches the server without an SDK change: the server
|
|
54
|
+
* learns to read it and the client sends it here.
|
|
55
|
+
*
|
|
56
|
+
* A key the transport already sends (`RESERVED_TURN_FIELDS`: `message`, `chatId`,
|
|
57
|
+
* `api_key`, `model`, `instructions`, `platform`, `address`, `personality`, `wake`,
|
|
58
|
+
* `context`) is refused: `send()` throws, and `ask()` rejects, before anything is sent.
|
|
59
|
+
*/
|
|
60
|
+
extra?: Record<string, string>;
|
|
38
61
|
};
|
|
39
62
|
export type DialogueRequestOptions = Omit<Omit<DialogueRequestParams, "message">, "chatId">;
|
|
40
63
|
export interface RequestResponseByVersion {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { JudgeQuestions, JudgeState, JudgeVerdict } from "../types/judge";
|
|
2
|
+
/** What a judge call needs: where the server is, and who is asking. */
|
|
3
|
+
export type JudgeRequestOptions = {
|
|
4
|
+
serverURL?: string;
|
|
5
|
+
/** The path of the judge route, when it is not the default. */
|
|
6
|
+
path?: string;
|
|
7
|
+
apiKey: string;
|
|
8
|
+
debug?: boolean;
|
|
9
|
+
};
|
|
10
|
+
export type JudgeOptions<Q extends JudgeQuestions = JudgeQuestions> = JudgeRequestOptions & {
|
|
11
|
+
/**
|
|
12
|
+
* The facts the questions are answered from: the message, who sent it, the rules, numbers
|
|
13
|
+
* and dates already worked out in code. Never a transcript or an argument for an answer.
|
|
14
|
+
*/
|
|
15
|
+
state: JudgeState;
|
|
16
|
+
/** The questions, by the id the answer is read back under. All are judged against the same state in one call. */
|
|
17
|
+
questions: Q;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Asks the judge: a yes/no, pick-one or score decision on the facts given, made by a decision
|
|
21
|
+
* model in well under a second for a fraction of a cent.
|
|
22
|
+
*
|
|
23
|
+
* The answers come back keyed by question id and typed by the question: a boolean's `value` and
|
|
24
|
+
* `probability`, a choice's `choice` (one of its criteria's keys), `confidence` and
|
|
25
|
+
* `probabilities`, a score's `level`, `score` and `confidence`. The model writes no text and
|
|
26
|
+
* gives no reason.
|
|
27
|
+
*
|
|
28
|
+
* A question the judge cannot ask (a boolean missing one side, a choice with one label, a score
|
|
29
|
+
* with more than ten levels) is a 400 carrying the judge's own message, and a judgement not
|
|
30
|
+
* reached is a 503: the call throws a `ButlerBotAPIError` either way, never a guess.
|
|
31
|
+
*/
|
|
32
|
+
export declare function judge<Q extends JudgeQuestions>(options: JudgeOptions<Q>): Promise<JudgeVerdict<Q>>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.judge = judge;
|
|
4
|
+
const config_1 = require("../config");
|
|
5
|
+
const url_formatter_1 = require("../util/url_formatter");
|
|
6
|
+
const api_request_1 = require("./api_request");
|
|
7
|
+
/**
|
|
8
|
+
* Asks the judge: a yes/no, pick-one or score decision on the facts given, made by a decision
|
|
9
|
+
* model in well under a second for a fraction of a cent.
|
|
10
|
+
*
|
|
11
|
+
* The answers come back keyed by question id and typed by the question: a boolean's `value` and
|
|
12
|
+
* `probability`, a choice's `choice` (one of its criteria's keys), `confidence` and
|
|
13
|
+
* `probabilities`, a score's `level`, `score` and `confidence`. The model writes no text and
|
|
14
|
+
* gives no reason.
|
|
15
|
+
*
|
|
16
|
+
* A question the judge cannot ask (a boolean missing one side, a choice with one label, a score
|
|
17
|
+
* with more than ten levels) is a 400 carrying the judge's own message, and a judgement not
|
|
18
|
+
* reached is a 503: the call throws a `ButlerBotAPIError` either way, never a guess.
|
|
19
|
+
*/
|
|
20
|
+
async function judge(options) {
|
|
21
|
+
const url = (0, url_formatter_1.formatURL)((options.serverURL || config_1.CONFIG.server) + (options.path || config_1.CONFIG.paths.judge.base), {}, { apiKey: options.apiKey, debug: options.debug });
|
|
22
|
+
const response = await (0, api_request_1.requestAPI)({
|
|
23
|
+
url,
|
|
24
|
+
method: "POST",
|
|
25
|
+
body: { state: options.state, questions: options.questions },
|
|
26
|
+
action: "ask the judge",
|
|
27
|
+
});
|
|
28
|
+
return { answers: response.answers, model: response.model, costUsd: response.costUsd };
|
|
29
|
+
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* caller sees, including the payload shape, is identical either way.
|
|
8
8
|
*/
|
|
9
9
|
import type { ConversationAddress } from "../types/conversation/address";
|
|
10
|
+
import type { MessageContextItem } from "../types/conversation/context";
|
|
10
11
|
/** A turn in progress. */
|
|
11
12
|
export type ConversationStream = {
|
|
12
13
|
/**
|
|
@@ -60,7 +61,26 @@ export type TransportTurnRequest = {
|
|
|
60
61
|
personality?: string;
|
|
61
62
|
/** Why Alfred is speaking on this one turn: a line for its system prompt, at most 500 characters. */
|
|
62
63
|
wake?: string;
|
|
64
|
+
/** What the platform attaches beside this turn's message. Sent only when non-empty. */
|
|
65
|
+
context?: MessageContextItem[];
|
|
66
|
+
/** Further turn parameters, forwarded to the server verbatim. See `DialogueRequestParams.extra`. */
|
|
67
|
+
extra?: Record<string, string>;
|
|
63
68
|
};
|
|
69
|
+
/**
|
|
70
|
+
* The turn parameters the server reads by name, which a transport sends itself. An `extra`
|
|
71
|
+
* entry may not use one: it would replace what the transport sent, or, for `api_key`, the
|
|
72
|
+
* credential. Both transports refuse the same list, since over a Link the service spreads
|
|
73
|
+
* `extra` into the very query the SSE transport builds.
|
|
74
|
+
*/
|
|
75
|
+
export declare const RESERVED_TURN_FIELDS: readonly string[];
|
|
76
|
+
/**
|
|
77
|
+
* A turn's `extra`, checked, or nothing when there is none to send. Throws on a key that
|
|
78
|
+
* collides with a field the transport sends, and on a value that is not a string, before
|
|
79
|
+
* anything reaches the wire.
|
|
80
|
+
*/
|
|
81
|
+
export declare function turnExtra(extra: Record<string, string> | undefined): Record<string, string> | undefined;
|
|
82
|
+
/** A turn's context as it travels, JSON-encoded, or nothing when there is none. */
|
|
83
|
+
export declare function turnContext(context: MessageContextItem[] | undefined): string | undefined;
|
|
64
84
|
export type TransportHandlers = {
|
|
65
85
|
/** One payload of the stream, already in the shape callers expect. */
|
|
66
86
|
payload(payload: unknown): void;
|
|
@@ -8,10 +8,58 @@
|
|
|
8
8
|
* caller sees, including the payload shape, is identical either way.
|
|
9
9
|
*/
|
|
10
10
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.RESERVED_TURN_FIELDS = void 0;
|
|
12
|
+
exports.turnExtra = turnExtra;
|
|
13
|
+
exports.turnContext = turnContext;
|
|
11
14
|
exports.noticePayload = noticePayload;
|
|
12
15
|
exports.convoStartedPayload = convoStartedPayload;
|
|
13
16
|
exports.completedPayload = completedPayload;
|
|
14
17
|
exports.failurePayload = failurePayload;
|
|
18
|
+
/**
|
|
19
|
+
* The turn parameters the server reads by name, which a transport sends itself. An `extra`
|
|
20
|
+
* entry may not use one: it would replace what the transport sent, or, for `api_key`, the
|
|
21
|
+
* credential. Both transports refuse the same list, since over a Link the service spreads
|
|
22
|
+
* `extra` into the very query the SSE transport builds.
|
|
23
|
+
*/
|
|
24
|
+
exports.RESERVED_TURN_FIELDS = [
|
|
25
|
+
"api_key",
|
|
26
|
+
"message",
|
|
27
|
+
"chatId",
|
|
28
|
+
"model",
|
|
29
|
+
"instructions",
|
|
30
|
+
"platform",
|
|
31
|
+
"address",
|
|
32
|
+
"personality",
|
|
33
|
+
"wake",
|
|
34
|
+
"context",
|
|
35
|
+
];
|
|
36
|
+
/**
|
|
37
|
+
* A turn's `extra`, checked, or nothing when there is none to send. Throws on a key that
|
|
38
|
+
* collides with a field the transport sends, and on a value that is not a string, before
|
|
39
|
+
* anything reaches the wire.
|
|
40
|
+
*/
|
|
41
|
+
function turnExtra(extra) {
|
|
42
|
+
if (!extra)
|
|
43
|
+
return undefined;
|
|
44
|
+
const entries = Object.entries(extra);
|
|
45
|
+
if (entries.length === 0)
|
|
46
|
+
return undefined;
|
|
47
|
+
for (const [key, value] of entries) {
|
|
48
|
+
if (!key)
|
|
49
|
+
throw new Error("A turn's `extra` may not have an empty key.");
|
|
50
|
+
if (exports.RESERVED_TURN_FIELDS.includes(key)) {
|
|
51
|
+
throw new Error(`A turn's \`extra\` may not set "${key}": the transport already sends it. Reserved: ${exports.RESERVED_TURN_FIELDS.join(", ")}.`);
|
|
52
|
+
}
|
|
53
|
+
if (typeof value !== "string") {
|
|
54
|
+
throw new Error(`A turn's \`extra\` values must be strings; "${key}" is ${typeof value}.`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return Object.fromEntries(entries);
|
|
58
|
+
}
|
|
59
|
+
/** A turn's context as it travels, JSON-encoded, or nothing when there is none. */
|
|
60
|
+
function turnContext(context) {
|
|
61
|
+
return context && context.length > 0 ? JSON.stringify(context) : undefined;
|
|
62
|
+
}
|
|
15
63
|
// =============================================
|
|
16
64
|
// PAYLOAD SHAPES
|
|
17
65
|
// =============================================
|
|
@@ -29,6 +29,8 @@ class LinkConversationTransport {
|
|
|
29
29
|
link.on("disconnect", () => { this.sessionId = undefined; });
|
|
30
30
|
}
|
|
31
31
|
send(request, handlers) {
|
|
32
|
+
// Refused here, synchronously, as the SSE transport refuses it: before anything is sent.
|
|
33
|
+
(0, transport_1.turnExtra)(request.extra);
|
|
32
34
|
const listening = { closed: false };
|
|
33
35
|
const deliver = {
|
|
34
36
|
payload: (payload) => { if (!listening.closed)
|
|
@@ -176,6 +178,8 @@ class LinkConversationTransport {
|
|
|
176
178
|
}
|
|
177
179
|
};
|
|
178
180
|
learnChatId(progress.chatId);
|
|
181
|
+
const context = (0, transport_1.turnContext)(request.context);
|
|
182
|
+
const extra = (0, transport_1.turnExtra)(request.extra);
|
|
179
183
|
let done;
|
|
180
184
|
try {
|
|
181
185
|
done = await this.link.exchange("conversation.chat", {
|
|
@@ -186,6 +190,8 @@ class LinkConversationTransport {
|
|
|
186
190
|
...(request.personality ? { personality: request.personality } : {}),
|
|
187
191
|
// Per turn, never on the session: it says why Alfred was woken for this one.
|
|
188
192
|
...(request.wake?.trim() ? { wake: request.wake.trim() } : {}),
|
|
193
|
+
...(context ? { context } : {}),
|
|
194
|
+
...(extra ? { extra } : {}),
|
|
189
195
|
}, {
|
|
190
196
|
// A turn takes as long as it takes; only the transport dying ends it early.
|
|
191
197
|
timeoutMs: 0,
|
|
@@ -4,6 +4,7 @@ exports.SSEConversationTransport = void 0;
|
|
|
4
4
|
exports.streamSSE = streamSSE;
|
|
5
5
|
const eventsource_1 = require("eventsource");
|
|
6
6
|
const url_formatter_1 = require("../util/url_formatter");
|
|
7
|
+
const transport_1 = require("./transport");
|
|
7
8
|
/**
|
|
8
9
|
* Streams a server-sent-events endpoint, closing when the server says the stream is
|
|
9
10
|
* done. Shared by turns and by the progress stream, which is SSE-only.
|
|
@@ -123,5 +124,10 @@ function asQuery(request) {
|
|
|
123
124
|
const wake = request.wake?.trim();
|
|
124
125
|
if (wake)
|
|
125
126
|
query.wake = wake;
|
|
126
|
-
|
|
127
|
+
const context = (0, transport_1.turnContext)(request.context);
|
|
128
|
+
if (context)
|
|
129
|
+
query.context = context;
|
|
130
|
+
// Checked before anything else is added, so an entry can never replace a field above.
|
|
131
|
+
const extra = (0, transport_1.turnExtra)(request.extra);
|
|
132
|
+
return extra ? { ...extra, ...query } : query;
|
|
127
133
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Something a platform attaches beside a user's message, never inside it: what the message
|
|
3
|
+
* replies to, who it mentions, where it was sent, the conversation around it.
|
|
4
|
+
*
|
|
5
|
+
* The text the user typed stays exactly what they typed. The model reads the items as a
|
|
6
|
+
* labelled block after it; a person sees them as chips under the message, titled by `title`
|
|
7
|
+
* and expandable to `text`.
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* { kind: "reply", title: "Replying to", text: "Sam: what time does the store close?" }
|
|
11
|
+
* { kind: "mentions", title: "Mentions", text: "Sam (@sam) = <@123>, Alfred = <@456> (this is you)" }
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* Bounds, enforced by the server, which refuses a request over them with what is wrong rather
|
|
15
|
+
* than clipping it: at most 8 items on a message, a `title` of at most 80 characters, a `text`
|
|
16
|
+
* of at most 4 000, and 12 000 characters in all.
|
|
17
|
+
*/
|
|
18
|
+
export type MessageContextItem = {
|
|
19
|
+
/**
|
|
20
|
+
* What the item is: `reply`, `mentions`, `place` or `channel` today. The server holds the
|
|
21
|
+
* list and refuses an unknown kind with the real ones, so a new kind needs no SDK change.
|
|
22
|
+
*/
|
|
23
|
+
kind: string;
|
|
24
|
+
/** What a person sees on the chip and the model on the block's line. At most 80 characters. */
|
|
25
|
+
title: string;
|
|
26
|
+
/** The content, as plain text. At most 4 000 characters. */
|
|
27
|
+
text: string;
|
|
28
|
+
};
|
|
@@ -52,6 +52,14 @@ export type ToolStatus = {
|
|
|
52
52
|
content?: ToolStatusContent[];
|
|
53
53
|
/** Whether this tool call is ending the conversation */
|
|
54
54
|
endingConvo?: boolean;
|
|
55
|
+
/** The tool that ran: its id, or a raw (MCP) tool's key. A fact for clients to present, never a label. */
|
|
56
|
+
toolId?: string;
|
|
57
|
+
/** Epoch ms of the first status emitted for this id; the same on every later status for it. */
|
|
58
|
+
startedAt?: number;
|
|
59
|
+
/** Epoch ms at which this id reached `completed` or `failed`. */
|
|
60
|
+
endedAt?: number;
|
|
61
|
+
/** The id of an earlier failed call of the same tool, in the same turn, that this call retries. */
|
|
62
|
+
retryOf?: string;
|
|
55
63
|
};
|
|
56
64
|
export type TokenUsage = {
|
|
57
65
|
/**
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./judge";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./judge"), exports);
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The judge: a yes/no, pick-one or score decision made by a decision model, on facts the caller
|
|
3
|
+
* states.
|
|
4
|
+
*
|
|
5
|
+
* Not a conversation. It writes no text and gives no reason: one call, every question judged
|
|
6
|
+
* against the same state, and typed answers back with probabilities. The state is facts (the
|
|
7
|
+
* message, who sent it, the rules, numbers and dates already worked out in code), never a
|
|
8
|
+
* transcript or an argument for an answer. State and the longest question together fit in about
|
|
9
|
+
* 32,000 tokens.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* A yes/no question.
|
|
13
|
+
*
|
|
14
|
+
* Both sides are described, because the model weighs the state against each description rather
|
|
15
|
+
* than against the question alone, and the server refuses a question that leaves one out.
|
|
16
|
+
*/
|
|
17
|
+
export type JudgeBooleanQuestion = {
|
|
18
|
+
type: "boolean";
|
|
19
|
+
/** What is being decided about the state, in one or two sentences. */
|
|
20
|
+
instructions: string;
|
|
21
|
+
criteria: {
|
|
22
|
+
/** What a yes looks like in the state. */
|
|
23
|
+
true: string;
|
|
24
|
+
/** What a no looks like in the state. */
|
|
25
|
+
false: string;
|
|
26
|
+
};
|
|
27
|
+
/** The probability of yes (between 0 and 1, exclusive) at which `value` is true. 0.5 when unset. */
|
|
28
|
+
threshold?: number;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Pick one label.
|
|
32
|
+
*
|
|
33
|
+
* Every label the answer may be is a key of `criteria`, described by its value; at least two.
|
|
34
|
+
* There is no automatic "none": when the state may fit no label, add one ("none": "Nothing above
|
|
35
|
+
* fits"), or the nearest label is picked however poor the fit.
|
|
36
|
+
*/
|
|
37
|
+
export type JudgeChoiceQuestion<L extends string = string> = {
|
|
38
|
+
type: "choice";
|
|
39
|
+
instructions: string;
|
|
40
|
+
criteria: Record<L, string>;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* A place on an ordered scale.
|
|
44
|
+
*
|
|
45
|
+
* `criteria[0]` describes the bottom of the scale and each entry after it one level up; at most
|
|
46
|
+
* ten levels.
|
|
47
|
+
*/
|
|
48
|
+
export type JudgeScoreQuestion = {
|
|
49
|
+
type: "score";
|
|
50
|
+
instructions: string;
|
|
51
|
+
criteria: string[];
|
|
52
|
+
};
|
|
53
|
+
export type JudgeQuestion = JudgeBooleanQuestion | JudgeChoiceQuestion | JudgeScoreQuestion;
|
|
54
|
+
/** The questions of one call, by an id (1-64 letters, digits, `_` or `-`) the answer is read back under. */
|
|
55
|
+
export type JudgeQuestions = Record<string, JudgeQuestion>;
|
|
56
|
+
/** The facts the questions are answered from: text, or an object the server renders. */
|
|
57
|
+
export type JudgeState = string | Record<string, unknown>;
|
|
58
|
+
export type JudgeBooleanAnswer = {
|
|
59
|
+
type: "boolean";
|
|
60
|
+
/** True when `probability` reached the question's threshold. */
|
|
61
|
+
value: boolean;
|
|
62
|
+
/** How likely yes is, 0 to 1. */
|
|
63
|
+
probability: number;
|
|
64
|
+
};
|
|
65
|
+
export type JudgeChoiceAnswer<L extends string = string> = {
|
|
66
|
+
type: "choice";
|
|
67
|
+
/** The label picked: one of the question's criteria keys. */
|
|
68
|
+
choice: L;
|
|
69
|
+
/** How sure, 0 to 1. */
|
|
70
|
+
confidence: number;
|
|
71
|
+
/** Every label's probability, keyed by label. */
|
|
72
|
+
probabilities?: Partial<Record<L, number>>;
|
|
73
|
+
};
|
|
74
|
+
export type JudgeScoreAnswer = {
|
|
75
|
+
type: "score";
|
|
76
|
+
/** The probability-weighted mean of the levels, 0-based; a fraction when the model is torn. */
|
|
77
|
+
score: number;
|
|
78
|
+
/** The single most likely level, 0-based: an index into the question's `criteria`. */
|
|
79
|
+
level: number;
|
|
80
|
+
/** How sure of `level`, 0 to 1. */
|
|
81
|
+
confidence: number;
|
|
82
|
+
/** Each level's probability, keyed by its index as a string. */
|
|
83
|
+
probabilities?: Record<string, number>;
|
|
84
|
+
};
|
|
85
|
+
export type JudgeAnswer = JudgeBooleanAnswer | JudgeChoiceAnswer | JudgeScoreAnswer;
|
|
86
|
+
/** The answer a question gets, typed by the question: a choice's label is one of its criteria's keys. */
|
|
87
|
+
export type JudgeAnswerFor<Q extends JudgeQuestion> = Q extends JudgeBooleanQuestion ? JudgeBooleanAnswer : Q extends JudgeChoiceQuestion ? JudgeChoiceAnswer<Extract<keyof Q["criteria"], string>> : Q extends JudgeScoreQuestion ? JudgeScoreAnswer : never;
|
|
88
|
+
export type JudgeAnswers<Q extends JudgeQuestions> = {
|
|
89
|
+
[K in keyof Q]: JudgeAnswerFor<Q[K]>;
|
|
90
|
+
};
|
|
91
|
+
/** What a decision comes back as. */
|
|
92
|
+
export type JudgeVerdict<Q extends JudgeQuestions = JudgeQuestions> = {
|
|
93
|
+
/** One answer per question, under the question's id. */
|
|
94
|
+
answers: JudgeAnswers<Q>;
|
|
95
|
+
/** The decision model that answered. */
|
|
96
|
+
model: string;
|
|
97
|
+
/** What the judgement cost, USD. It is already on the account's ledger. */
|
|
98
|
+
costUsd: number;
|
|
99
|
+
};
|
|
100
|
+
/** The route's whole answer. */
|
|
101
|
+
export type JudgeResponse<Q extends JudgeQuestions = JudgeQuestions> = JudgeVerdict<Q> & {
|
|
102
|
+
success: true;
|
|
103
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The judge: a yes/no, pick-one or score decision made by a decision model, on facts the caller
|
|
4
|
+
* states.
|
|
5
|
+
*
|
|
6
|
+
* Not a conversation. It writes no text and gives no reason: one call, every question judged
|
|
7
|
+
* against the same state, and typed answers back with probabilities. The state is facts (the
|
|
8
|
+
* message, who sent it, the rules, numbers and dates already worked out in code), never a
|
|
9
|
+
* transcript or an argument for an answer. State and the longest question together fit in about
|
|
10
|
+
* 32,000 tokens.
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -39,6 +39,14 @@ export type ToolStatus = {
|
|
|
39
39
|
content?: ToolStatusContent[];
|
|
40
40
|
/** Whether this tool call is ending the conversation */
|
|
41
41
|
endingConvo?: boolean;
|
|
42
|
+
/** The tool that ran: its id, or a raw (MCP) tool's key. A fact for clients to present, never a label. */
|
|
43
|
+
toolId?: string;
|
|
44
|
+
/** Epoch ms of the first status emitted for this id; the same on every later status for it. */
|
|
45
|
+
startedAt?: number;
|
|
46
|
+
/** Epoch ms at which this id reached `completed` or `failed`. */
|
|
47
|
+
endedAt?: number;
|
|
48
|
+
/** The id of an earlier failed call of the same tool, in the same turn, that this call retries. */
|
|
49
|
+
retryOf?: string;
|
|
42
50
|
};
|
|
43
51
|
export type BaseResponseMetadata = {
|
|
44
52
|
/** Unique response ID */
|
|
@@ -4,6 +4,8 @@ export * from "./response/v5";
|
|
|
4
4
|
export * from "./state/convo_state_response";
|
|
5
5
|
export * from "./conversation/v4/conversation_v4";
|
|
6
6
|
export * from "./conversation/address";
|
|
7
|
+
export * from "./conversation/context";
|
|
7
8
|
export * from "./jobs";
|
|
8
9
|
export * from "./outreach";
|
|
10
|
+
export * from "./judge";
|
|
9
11
|
export * from "./error";
|
|
@@ -20,6 +20,8 @@ __exportStar(require("./response/v5"), exports);
|
|
|
20
20
|
__exportStar(require("./state/convo_state_response"), exports);
|
|
21
21
|
__exportStar(require("./conversation/v4/conversation_v4"), exports);
|
|
22
22
|
__exportStar(require("./conversation/address"), exports);
|
|
23
|
+
__exportStar(require("./conversation/context"), exports);
|
|
23
24
|
__exportStar(require("./jobs"), exports);
|
|
24
25
|
__exportStar(require("./outreach"), exports);
|
|
26
|
+
__exportStar(require("./judge"), exports);
|
|
25
27
|
__exportStar(require("./error"), exports);
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -387,10 +387,63 @@ one, and answering it is refused with a 409 just as one already answered is. `so
|
|
|
387
387
|
wrote a delivery: `shift` for a question a job's shift asked, `runtime` for the job's own
|
|
388
388
|
status tells, `gate` for an approval the autonomy gate asked for.
|
|
389
389
|
|
|
390
|
+
## Judge
|
|
391
|
+
|
|
392
|
+
A yes/no, pick-one or score decision on facts you state, made by a decision model: well under
|
|
393
|
+
a second and a fraction of a cent, so an app can ask it per event. It is Alfred's own judge,
|
|
394
|
+
the one his jobs and reflexes decide with, for your code to decide with too.
|
|
395
|
+
|
|
396
|
+
```typescript
|
|
397
|
+
const { answers, model, costUsd } = await client.judge({
|
|
398
|
+
state: { message: text, author: "bot: false", channel: "general", rules },
|
|
399
|
+
questions: {
|
|
400
|
+
hostile: {
|
|
401
|
+
type: "boolean",
|
|
402
|
+
instructions: "Is the message hostile?",
|
|
403
|
+
criteria: { true: "It attacks or insults someone", false: "It is civil, however blunt" },
|
|
404
|
+
},
|
|
405
|
+
rule: {
|
|
406
|
+
type: "choice",
|
|
407
|
+
instructions: "Which server rule does the message break?",
|
|
408
|
+
criteria: { spam: "Promotional or repeated", abuse: "Insults a person", none: "No rule is broken" },
|
|
409
|
+
},
|
|
410
|
+
heat: {
|
|
411
|
+
type: "score",
|
|
412
|
+
instructions: "How heated is the message?",
|
|
413
|
+
criteria: ["Calm", "Annoyed", "Furious"],
|
|
414
|
+
},
|
|
415
|
+
},
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
answers.hostile.value; // boolean: value, probability
|
|
419
|
+
answers.rule.choice; // "spam" | "abuse" | "none", with confidence and every label's probability
|
|
420
|
+
answers.heat.level; // 0..2, with score (the weighted mean) and confidence
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
Every question is judged against the same `state` in one call, and the answers come back keyed
|
|
424
|
+
by question id and typed by the question: a choice's `choice` is one of its own criteria keys.
|
|
425
|
+
It is a decision model, not a chat model: it writes no text and does no reasoning, so
|
|
426
|
+
|
|
427
|
+
- state facts, never a transcript or an argument for an answer;
|
|
428
|
+
- work out numbers and dates in code and state the result;
|
|
429
|
+
- describe both sides of a boolean, since the model weighs the state against each;
|
|
430
|
+
- add a `none` label to a choice when nothing may fit, or the nearest label is picked however
|
|
431
|
+
poor the fit;
|
|
432
|
+
- keep a score to ten levels, `criteria[0]` the bottom.
|
|
433
|
+
|
|
434
|
+
State and the longest question together fit in about 32,000 tokens. A question the judge cannot
|
|
435
|
+
ask is a 400 carrying its message, and a judgement it could not reach is a 503: both throw
|
|
436
|
+
`ButlerBotAPIError`, never a guess. Needs `judge.run`.
|
|
437
|
+
|
|
438
|
+
The judge is for a decision your app makes itself. For Alfred to react to something, do not
|
|
439
|
+
judge first: report it as a [hook event](#hooks-emit-or-report-what-matched) and the user's
|
|
440
|
+
reflex decides, with the judge inside it when it needs one. That keeps what Alfred reacts to,
|
|
441
|
+
and what it costs, in the user's hands.
|
|
442
|
+
|
|
390
443
|
### When a call fails
|
|
391
444
|
|
|
392
|
-
Every jobs and
|
|
393
|
-
success. The status is on the error, so the cases worth branching on are told apart without
|
|
445
|
+
Every jobs, outreach and judge call throws `ButlerBotAPIError` when the server does not answer
|
|
446
|
+
with a success. The status is on the error, so the cases worth branching on are told apart without
|
|
394
447
|
reading a message, and the parsed body is kept — a rejected cancel still carries the job, a
|
|
395
448
|
rejected answer still carries the delivery:
|
|
396
449
|
|