@tanstack/ai 0.55.0 → 0.57.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 +42 -16
- package/dist/esm/activities/chat/tools/tool-calls.js +1 -0
- package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
- package/dist/esm/activities/evaluate/adapter.d.ts +160 -0
- package/dist/esm/activities/evaluate/adapter.js +23 -0
- package/dist/esm/activities/evaluate/adapter.js.map +1 -0
- package/dist/esm/activities/evaluate/index.d.ts +255 -0
- package/dist/esm/activities/evaluate/index.js +317 -0
- package/dist/esm/activities/evaluate/index.js.map +1 -0
- package/dist/esm/activities/generateSpeech/adapter.d.ts +39 -1
- package/dist/esm/activities/generateSpeech/adapter.js.map +1 -1
- package/dist/esm/activities/generateSpeech/index.d.ts +55 -5
- package/dist/esm/activities/generateSpeech/index.js +53 -3
- package/dist/esm/activities/generateSpeech/index.js.map +1 -1
- package/dist/esm/activities/generateVoice/adapter.d.ts +62 -0
- package/dist/esm/activities/generateVoice/adapter.js +23 -0
- package/dist/esm/activities/generateVoice/adapter.js.map +1 -0
- package/dist/esm/activities/generateVoice/index.d.ts +133 -0
- package/dist/esm/activities/generateVoice/index.js +184 -0
- package/dist/esm/activities/generateVoice/index.js.map +1 -0
- package/dist/esm/activities/index.d.ts +10 -4
- package/dist/esm/activities/index.js +14 -10
- package/dist/esm/activities/middleware/types.d.ts +1 -1
- package/dist/esm/client.d.ts +3 -2
- package/dist/esm/client.js +21 -3
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/index.d.ts +4 -2
- package/dist/esm/index.js +5 -2
- package/dist/esm/middlewares/otel.js +2 -0
- package/dist/esm/middlewares/otel.js.map +1 -1
- package/dist/esm/realtime/index.d.ts +1 -1
- package/dist/esm/realtime/index.js +1 -1
- package/dist/esm/realtime/index.js.map +1 -1
- package/dist/esm/types.d.ts +225 -2
- package/package.json +3 -3
- package/skills/ai-core/media-generation/SKILL.md +132 -6
- package/src/activities/chat/tools/tool-calls.ts +9 -0
- package/src/activities/evaluate/adapter.ts +212 -0
- package/src/activities/evaluate/index.ts +614 -0
- package/src/activities/generateSpeech/adapter.ts +47 -1
- package/src/activities/generateSpeech/index.ts +149 -8
- package/src/activities/generateVoice/adapter.ts +89 -0
- package/src/activities/generateVoice/index.ts +371 -0
- package/src/activities/index.ts +69 -0
- package/src/activities/middleware/types.ts +2 -0
- package/src/client.ts +35 -8
- package/src/index.ts +21 -0
- package/src/middlewares/otel.ts +2 -0
- package/src/realtime/index.ts +1 -1
- package/src/types.ts +246 -2
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { DebugOption } from '../../logger/types.js';
|
|
2
|
+
import { TokenUsage } from '../../types.js';
|
|
3
|
+
import { GenerationMiddleware } from '../middleware/types.js';
|
|
4
|
+
import { EvaluateAdapter, EvaluateInstructions, EvaluateState, WireQuestion } from './adapter.js';
|
|
5
|
+
/** The adapter kind this activity handles */
|
|
6
|
+
export declare const kind: "evaluate";
|
|
7
|
+
/** Question key reserved for `result.meta`. */
|
|
8
|
+
declare const RESERVED_QUESTION_KEY: "meta";
|
|
9
|
+
/** Extract provider options from an EvaluateAdapter via ~types */
|
|
10
|
+
export type EvaluateProviderOptions<TAdapter> = TAdapter extends {
|
|
11
|
+
'~types': {
|
|
12
|
+
providerOptions: infer P extends object;
|
|
13
|
+
};
|
|
14
|
+
} ? P : object;
|
|
15
|
+
/**
|
|
16
|
+
* Public choice answer. `.value` is the selected option key.
|
|
17
|
+
*/
|
|
18
|
+
export interface ChoiceAnswer<TValue extends string = string> {
|
|
19
|
+
type: 'choice';
|
|
20
|
+
value: TValue;
|
|
21
|
+
/** P(selected option). */
|
|
22
|
+
probability: number;
|
|
23
|
+
confidence: number;
|
|
24
|
+
probabilities: Record<TValue, number>;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Public score answer. `.value` is the nearest level label.
|
|
28
|
+
* `.score` is the raw TypeSafe fraction.
|
|
29
|
+
*/
|
|
30
|
+
export interface ScoreAnswer<TLevel extends string = string> {
|
|
31
|
+
type: 'score';
|
|
32
|
+
value: TLevel;
|
|
33
|
+
/** P(nearest level). */
|
|
34
|
+
probability: number;
|
|
35
|
+
confidence: number;
|
|
36
|
+
score: number;
|
|
37
|
+
legend: Record<string, string>;
|
|
38
|
+
probabilities: Record<string, number>;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Public yes/no answer. `.value` is `true` when P(true) is 0.5 or more.
|
|
42
|
+
* There is no `.confidence`.
|
|
43
|
+
*/
|
|
44
|
+
export interface BooleanAnswer {
|
|
45
|
+
type: 'boolean';
|
|
46
|
+
value: boolean;
|
|
47
|
+
/** P(true), from the wire `noul` field. */
|
|
48
|
+
probability: number;
|
|
49
|
+
}
|
|
50
|
+
export interface EvaluateResultMeta {
|
|
51
|
+
/** Resolved model id from the provider. */
|
|
52
|
+
model: string;
|
|
53
|
+
usage: TokenUsage;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Map a helper question (or wire question) to its public answer type.
|
|
57
|
+
*/
|
|
58
|
+
export type InferEvaluateAnswer<TQuestion> = TQuestion extends {
|
|
59
|
+
type: 'choice';
|
|
60
|
+
criteria: infer TCriteria;
|
|
61
|
+
} ? TCriteria extends Record<string, string | null> ? ChoiceAnswer<Extract<keyof TCriteria, string>> : ChoiceAnswer : TQuestion extends {
|
|
62
|
+
type: 'score';
|
|
63
|
+
criteria: infer TLevels;
|
|
64
|
+
} ? TLevels extends ReadonlyArray<string> ? ScoreAnswer<TLevels[number] & string> : ScoreAnswer : TQuestion extends {
|
|
65
|
+
type: 'noul';
|
|
66
|
+
} ? BooleanAnswer : never;
|
|
67
|
+
/**
|
|
68
|
+
* Result of `decide()`. Each question key is a top-level answer.
|
|
69
|
+
* `meta` holds the resolved model id and usage.
|
|
70
|
+
*/
|
|
71
|
+
export type EvaluateResult<TQuestions extends Record<string, WireQuestion>> = {
|
|
72
|
+
[K in keyof TQuestions as K extends typeof RESERVED_QUESTION_KEY ? never : K]: InferEvaluateAnswer<TQuestions[K]>;
|
|
73
|
+
} & {
|
|
74
|
+
meta: EvaluateResultMeta;
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* Options for the evaluate activity. The model is extracted from the
|
|
78
|
+
* adapter's model property.
|
|
79
|
+
*
|
|
80
|
+
* @template TAdapter - The evaluate adapter type
|
|
81
|
+
* @template TQuestions - The questions object passed to `decide`
|
|
82
|
+
*/
|
|
83
|
+
export interface EvaluateActivityOptions<TAdapter extends EvaluateAdapter<string, EvaluateProviderOptions<TAdapter>>, TQuestions extends Record<string, WireQuestion>> {
|
|
84
|
+
/** The evaluate adapter to use (must be created with a model) */
|
|
85
|
+
adapter: TAdapter & {
|
|
86
|
+
kind: typeof kind;
|
|
87
|
+
};
|
|
88
|
+
/** Shared state every question judges. A JSON array is one state, not a batch. */
|
|
89
|
+
state: EvaluateState;
|
|
90
|
+
/**
|
|
91
|
+
* Questions built with `choice`, `score`, and `boolean`.
|
|
92
|
+
* The key `meta` is reserved.
|
|
93
|
+
*/
|
|
94
|
+
questions: TQuestions;
|
|
95
|
+
/** Provider-specific options */
|
|
96
|
+
modelOptions?: EvaluateProviderOptions<TAdapter>;
|
|
97
|
+
/** Forwarded to the provider request for cancellation. */
|
|
98
|
+
abortSignal?: AbortSignal;
|
|
99
|
+
/**
|
|
100
|
+
* Observe-only middleware notified on start, usage, success, abort, and
|
|
101
|
+
* error. Pass `otelMiddleware()` to emit OpenTelemetry spans, or implement
|
|
102
|
+
* the `GenerationMiddleware` contract for a custom backend.
|
|
103
|
+
*/
|
|
104
|
+
middleware?: Array<GenerationMiddleware>;
|
|
105
|
+
/**
|
|
106
|
+
* Enable debug logging. Pass `true` to enable all categories, `false` to
|
|
107
|
+
* silence everything including errors, or a `DebugConfig` object for granular
|
|
108
|
+
* control and/or a custom `Logger`.
|
|
109
|
+
*/
|
|
110
|
+
debug?: DebugOption;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Build a choice question. The model picks one key from `options`.
|
|
114
|
+
*
|
|
115
|
+
* Option keys become the union on `.value`. Use `null` when a key needs no
|
|
116
|
+
* extra description. On the wire, `options` is sent as TypeSafe `criteria`.
|
|
117
|
+
*
|
|
118
|
+
* @param options.instructions What the model should decide.
|
|
119
|
+
* @param options.options Map of option key to description, or `null`.
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* ```ts
|
|
123
|
+
* const queue = choice({
|
|
124
|
+
* instructions: 'Which team should handle this ticket?',
|
|
125
|
+
* options: {
|
|
126
|
+
* billing: 'Payments, invoices, refunds',
|
|
127
|
+
* tech: 'Bugs, outages, integrations',
|
|
128
|
+
* sales: 'Pricing, upgrades, new accounts',
|
|
129
|
+
* },
|
|
130
|
+
* })
|
|
131
|
+
* ```
|
|
132
|
+
*/
|
|
133
|
+
export declare function choice<const TOptions extends Record<string, string | null>>(options: {
|
|
134
|
+
instructions: EvaluateInstructions;
|
|
135
|
+
options: TOptions;
|
|
136
|
+
}): {
|
|
137
|
+
type: "choice";
|
|
138
|
+
instructions: import('./adapter.js').EvaluateJsonValue;
|
|
139
|
+
criteria: TOptions;
|
|
140
|
+
};
|
|
141
|
+
/**
|
|
142
|
+
* Build a score question. The model rates `state` on ordered `levels`.
|
|
143
|
+
*
|
|
144
|
+
* You must pass at least two levels. `.value` is the nearest level label.
|
|
145
|
+
* The raw fraction stays on `.score`. On the wire, `levels` is sent as
|
|
146
|
+
* TypeSafe `criteria`.
|
|
147
|
+
*
|
|
148
|
+
* @param options.instructions What the model should rate.
|
|
149
|
+
* @param options.levels Ordered labels, lowest first. At least two.
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* ```ts
|
|
153
|
+
* const urgency = score({
|
|
154
|
+
* instructions: 'How urgent is this ticket?',
|
|
155
|
+
* levels: ['low', 'medium', 'high'],
|
|
156
|
+
* })
|
|
157
|
+
* ```
|
|
158
|
+
*/
|
|
159
|
+
export declare function score<const TLevels extends ReadonlyArray<string>>(options: {
|
|
160
|
+
instructions: EvaluateInstructions;
|
|
161
|
+
levels: TLevels;
|
|
162
|
+
}): {
|
|
163
|
+
type: "score";
|
|
164
|
+
instructions: import('./adapter.js').EvaluateJsonValue;
|
|
165
|
+
criteria: TLevels;
|
|
166
|
+
};
|
|
167
|
+
/**
|
|
168
|
+
* Build a yes/no question.
|
|
169
|
+
*
|
|
170
|
+
* `.value` is `true` when P(true) is 0.5 or more. There is no `.confidence`.
|
|
171
|
+
* On the wire, the type is TypeSafe `noul`.
|
|
172
|
+
*
|
|
173
|
+
* @param options.instructions The yes/no question to judge.
|
|
174
|
+
* @param options.criteria Optional descriptions of yes and no.
|
|
175
|
+
*
|
|
176
|
+
* @example
|
|
177
|
+
* ```ts
|
|
178
|
+
* const refund = boolean({
|
|
179
|
+
* instructions: 'Is the customer asking for a refund?',
|
|
180
|
+
* })
|
|
181
|
+
* ```
|
|
182
|
+
*/
|
|
183
|
+
export declare function boolean(options: {
|
|
184
|
+
instructions: EvaluateInstructions;
|
|
185
|
+
criteria?: {
|
|
186
|
+
true?: string;
|
|
187
|
+
false?: string;
|
|
188
|
+
};
|
|
189
|
+
}): {
|
|
190
|
+
type: "noul";
|
|
191
|
+
instructions: import('./adapter.js').EvaluateJsonValue;
|
|
192
|
+
criteria?: undefined;
|
|
193
|
+
} | {
|
|
194
|
+
type: "noul";
|
|
195
|
+
instructions: import('./adapter.js').EvaluateJsonValue;
|
|
196
|
+
criteria: {
|
|
197
|
+
true?: string;
|
|
198
|
+
false?: string;
|
|
199
|
+
};
|
|
200
|
+
};
|
|
201
|
+
/**
|
|
202
|
+
* Ask typed questions about `state` and get answers your code can branch on.
|
|
203
|
+
*
|
|
204
|
+
* You have state (a ticket, a record, a log) and you need typed answers, not
|
|
205
|
+
* prose. Pass questions built with `choice`, `score`, and `boolean`. Then
|
|
206
|
+
* branch on `result.queue.value` in ordinary TypeScript.
|
|
207
|
+
*
|
|
208
|
+
* The question key `meta` is reserved. Throws if `questions` is empty or uses
|
|
209
|
+
* that key.
|
|
210
|
+
*
|
|
211
|
+
* @param options.adapter Evaluate adapter created with a model.
|
|
212
|
+
* @param options.state Shared state every question judges.
|
|
213
|
+
* @param options.questions Questions built with `choice`, `score`, `boolean`.
|
|
214
|
+
* @param options.modelOptions Provider-specific options.
|
|
215
|
+
* @param options.abortSignal Cancels the in-flight request.
|
|
216
|
+
* @param options.middleware Observe-only generation middleware.
|
|
217
|
+
* @param options.debug Debug logging option.
|
|
218
|
+
*
|
|
219
|
+
* @example Route a support ticket
|
|
220
|
+
* ```ts
|
|
221
|
+
* import { decide, choice, score, boolean } from '@tanstack/ai'
|
|
222
|
+
* import { typesafeDecider } from '@tanstack/ai-typesafe'
|
|
223
|
+
*
|
|
224
|
+
* const result = await decide({
|
|
225
|
+
* adapter: typesafeDecider('jev-latest'),
|
|
226
|
+
* state: ticket,
|
|
227
|
+
* questions: {
|
|
228
|
+
* queue: choice({
|
|
229
|
+
* instructions: 'Which team should handle this ticket?',
|
|
230
|
+
* options: {
|
|
231
|
+
* billing: 'Payments, invoices, refunds',
|
|
232
|
+
* tech: 'Bugs, outages, integrations',
|
|
233
|
+
* sales: 'Pricing, upgrades, new accounts',
|
|
234
|
+
* },
|
|
235
|
+
* }),
|
|
236
|
+
* urgency: score({
|
|
237
|
+
* instructions: 'How urgent is this ticket?',
|
|
238
|
+
* levels: ['low', 'medium', 'high'],
|
|
239
|
+
* }),
|
|
240
|
+
* refund: boolean({
|
|
241
|
+
* instructions: 'Is the customer asking for a refund?',
|
|
242
|
+
* }),
|
|
243
|
+
* },
|
|
244
|
+
* })
|
|
245
|
+
*
|
|
246
|
+
* result.queue.value
|
|
247
|
+
* result.meta.model
|
|
248
|
+
* result.meta.usage
|
|
249
|
+
* ```
|
|
250
|
+
*/
|
|
251
|
+
export declare function decide<TAdapter extends EvaluateAdapter<string, EvaluateProviderOptions<TAdapter>>, TQuestions extends Record<string, WireQuestion>>(options: EvaluateActivityOptions<TAdapter, TQuestions>): Promise<{ [K in keyof TQuestions]: InferEvaluateAnswer<TQuestions[K]>; } & {
|
|
252
|
+
meta: EvaluateResultMeta;
|
|
253
|
+
}>;
|
|
254
|
+
export type { EvaluateAdapter, EvaluateAdapterConfig, AnyEvaluateAdapter, EvaluateOptions, EvaluateAdapterResult, EvaluateState, EvaluateInstructions, EvaluateJsonValue, WireQuestion, WireAnswer, WireChoiceQuestion, WireScoreQuestion, WireNoulQuestion, WireChoiceAnswer, WireScoreAnswer, WireNoulAnswer, } from './adapter.js';
|
|
255
|
+
export { BaseEvaluateAdapter } from './adapter.js';
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { isAbortShapedError } from "../error-payload.js";
|
|
2
|
+
import { resolveDebugOption } from "../../logger/resolve.js";
|
|
3
|
+
import { createGenerationContext, runGenerationAbort, runGenerationError, runGenerationFinish, runGenerationStart, runGenerationUsage } from "../middleware/run.js";
|
|
4
|
+
import "./adapter.js";
|
|
5
|
+
import { aiEventClient } from "@tanstack/ai-event-client";
|
|
6
|
+
//#region src/activities/evaluate/index.ts
|
|
7
|
+
/**
|
|
8
|
+
* Evaluate Activity
|
|
9
|
+
*
|
|
10
|
+
* Asks typed questions about a shared state and returns values your code can
|
|
11
|
+
* branch on. This is a self-contained module with implementation, types, and
|
|
12
|
+
* JSDoc.
|
|
13
|
+
*/
|
|
14
|
+
/** The adapter kind this activity handles */
|
|
15
|
+
var kind = "evaluate";
|
|
16
|
+
/** Question key reserved for `result.meta`. */
|
|
17
|
+
var RESERVED_QUESTION_KEY = "meta";
|
|
18
|
+
function createId(prefix) {
|
|
19
|
+
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
20
|
+
}
|
|
21
|
+
function isAbortError(error, signal) {
|
|
22
|
+
if (isAbortShapedError(error)) return true;
|
|
23
|
+
return error instanceof Error ? false : signal?.aborted === true;
|
|
24
|
+
}
|
|
25
|
+
function questionKeys(questions) {
|
|
26
|
+
return Object.keys(questions);
|
|
27
|
+
}
|
|
28
|
+
function assertQuestions(questions) {
|
|
29
|
+
const keys = questionKeys(questions);
|
|
30
|
+
if (keys.length === 0) throw new Error("decide() requires at least one question");
|
|
31
|
+
if (Object.hasOwn(questions, RESERVED_QUESTION_KEY)) throw new Error("decide() reserves the question key \"meta\"");
|
|
32
|
+
return keys;
|
|
33
|
+
}
|
|
34
|
+
function mapChoiceAnswer(wire, key) {
|
|
35
|
+
const probability = wire.probabilities[wire.choice];
|
|
36
|
+
if (typeof probability !== "number") throw new Error(`decide(): missing probability for choice "${wire.choice}" on "${key}"`);
|
|
37
|
+
return {
|
|
38
|
+
type: "choice",
|
|
39
|
+
value: wire.choice,
|
|
40
|
+
probability,
|
|
41
|
+
confidence: wire.confidence,
|
|
42
|
+
probabilities: wire.probabilities
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function mapScoreAnswer(question, wire, key) {
|
|
46
|
+
const levels = question.criteria;
|
|
47
|
+
if (levels.length < 2) throw new Error(`decide(): score question "${key}" needs at least two levels`);
|
|
48
|
+
const lastIndex = levels.length - 1;
|
|
49
|
+
const rounded = Math.round(wire.score);
|
|
50
|
+
const nearestIndex = rounded < 0 ? 0 : rounded > lastIndex ? lastIndex : rounded;
|
|
51
|
+
const value = levels[nearestIndex];
|
|
52
|
+
if (value === void 0) throw new Error(`decide(): score question "${key}" has no level at index ${nearestIndex}`);
|
|
53
|
+
const probability = wire.probabilities[String(nearestIndex)];
|
|
54
|
+
if (typeof probability !== "number") throw new Error(`decide(): missing probability for score level ${nearestIndex} on "${key}"`);
|
|
55
|
+
return {
|
|
56
|
+
type: "score",
|
|
57
|
+
value,
|
|
58
|
+
probability,
|
|
59
|
+
confidence: wire.confidence,
|
|
60
|
+
score: wire.score,
|
|
61
|
+
legend: wire.legend,
|
|
62
|
+
probabilities: wire.probabilities
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function mapBooleanAnswer(wire) {
|
|
66
|
+
return {
|
|
67
|
+
type: "boolean",
|
|
68
|
+
value: wire.noul >= .5,
|
|
69
|
+
probability: wire.noul
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function mapWireAnswer(question, wire, key) {
|
|
73
|
+
switch (question.type) {
|
|
74
|
+
case "choice":
|
|
75
|
+
if (wire.type !== "choice") throw new Error(`decide(): expected choice answer for "${key}", got ${wire.type}`);
|
|
76
|
+
return mapChoiceAnswer(wire, key);
|
|
77
|
+
case "score":
|
|
78
|
+
if (wire.type !== "score") throw new Error(`decide(): expected score answer for "${key}", got ${wire.type}`);
|
|
79
|
+
return mapScoreAnswer(question, wire, key);
|
|
80
|
+
case "noul":
|
|
81
|
+
if (wire.type !== "noul") throw new Error(`decide(): expected noul answer for "${key}", got ${wire.type}`);
|
|
82
|
+
return mapBooleanAnswer(wire);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function mapAnswers(questions, wireAnswers) {
|
|
86
|
+
const answers = {};
|
|
87
|
+
const keys = Object.keys(questions);
|
|
88
|
+
for (const key of keys) {
|
|
89
|
+
const question = questions[key];
|
|
90
|
+
const wire = wireAnswers[String(key)];
|
|
91
|
+
if (question === void 0) throw new Error(`decide(): missing question "${String(key)}"`);
|
|
92
|
+
if (wire === void 0) throw new Error(`decide(): missing answer for question "${String(key)}"`);
|
|
93
|
+
answers[key] = mapWireAnswer(question, wire, String(key));
|
|
94
|
+
}
|
|
95
|
+
return answers;
|
|
96
|
+
}
|
|
97
|
+
function withMeta(answers, meta) {
|
|
98
|
+
return {
|
|
99
|
+
...answers,
|
|
100
|
+
meta
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Build a choice question. The model picks one key from `options`.
|
|
105
|
+
*
|
|
106
|
+
* Option keys become the union on `.value`. Use `null` when a key needs no
|
|
107
|
+
* extra description. On the wire, `options` is sent as TypeSafe `criteria`.
|
|
108
|
+
*
|
|
109
|
+
* @param options.instructions What the model should decide.
|
|
110
|
+
* @param options.options Map of option key to description, or `null`.
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* ```ts
|
|
114
|
+
* const queue = choice({
|
|
115
|
+
* instructions: 'Which team should handle this ticket?',
|
|
116
|
+
* options: {
|
|
117
|
+
* billing: 'Payments, invoices, refunds',
|
|
118
|
+
* tech: 'Bugs, outages, integrations',
|
|
119
|
+
* sales: 'Pricing, upgrades, new accounts',
|
|
120
|
+
* },
|
|
121
|
+
* })
|
|
122
|
+
* ```
|
|
123
|
+
*/
|
|
124
|
+
function choice(options) {
|
|
125
|
+
return {
|
|
126
|
+
type: "choice",
|
|
127
|
+
instructions: options.instructions,
|
|
128
|
+
criteria: options.options
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Build a score question. The model rates `state` on ordered `levels`.
|
|
133
|
+
*
|
|
134
|
+
* You must pass at least two levels. `.value` is the nearest level label.
|
|
135
|
+
* The raw fraction stays on `.score`. On the wire, `levels` is sent as
|
|
136
|
+
* TypeSafe `criteria`.
|
|
137
|
+
*
|
|
138
|
+
* @param options.instructions What the model should rate.
|
|
139
|
+
* @param options.levels Ordered labels, lowest first. At least two.
|
|
140
|
+
*
|
|
141
|
+
* @example
|
|
142
|
+
* ```ts
|
|
143
|
+
* const urgency = score({
|
|
144
|
+
* instructions: 'How urgent is this ticket?',
|
|
145
|
+
* levels: ['low', 'medium', 'high'],
|
|
146
|
+
* })
|
|
147
|
+
* ```
|
|
148
|
+
*/
|
|
149
|
+
function score(options) {
|
|
150
|
+
if (options.levels.length < 2) throw new Error("score() requires at least two levels");
|
|
151
|
+
return {
|
|
152
|
+
type: "score",
|
|
153
|
+
instructions: options.instructions,
|
|
154
|
+
criteria: options.levels
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Build a yes/no question.
|
|
159
|
+
*
|
|
160
|
+
* `.value` is `true` when P(true) is 0.5 or more. There is no `.confidence`.
|
|
161
|
+
* On the wire, the type is TypeSafe `noul`.
|
|
162
|
+
*
|
|
163
|
+
* @param options.instructions The yes/no question to judge.
|
|
164
|
+
* @param options.criteria Optional descriptions of yes and no.
|
|
165
|
+
*
|
|
166
|
+
* @example
|
|
167
|
+
* ```ts
|
|
168
|
+
* const refund = boolean({
|
|
169
|
+
* instructions: 'Is the customer asking for a refund?',
|
|
170
|
+
* })
|
|
171
|
+
* ```
|
|
172
|
+
*/
|
|
173
|
+
function boolean(options) {
|
|
174
|
+
if (options.criteria === void 0) return {
|
|
175
|
+
type: "noul",
|
|
176
|
+
instructions: options.instructions
|
|
177
|
+
};
|
|
178
|
+
return {
|
|
179
|
+
type: "noul",
|
|
180
|
+
instructions: options.instructions,
|
|
181
|
+
criteria: options.criteria
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Ask typed questions about `state` and get answers your code can branch on.
|
|
186
|
+
*
|
|
187
|
+
* You have state (a ticket, a record, a log) and you need typed answers, not
|
|
188
|
+
* prose. Pass questions built with `choice`, `score`, and `boolean`. Then
|
|
189
|
+
* branch on `result.queue.value` in ordinary TypeScript.
|
|
190
|
+
*
|
|
191
|
+
* The question key `meta` is reserved. Throws if `questions` is empty or uses
|
|
192
|
+
* that key.
|
|
193
|
+
*
|
|
194
|
+
* @param options.adapter Evaluate adapter created with a model.
|
|
195
|
+
* @param options.state Shared state every question judges.
|
|
196
|
+
* @param options.questions Questions built with `choice`, `score`, `boolean`.
|
|
197
|
+
* @param options.modelOptions Provider-specific options.
|
|
198
|
+
* @param options.abortSignal Cancels the in-flight request.
|
|
199
|
+
* @param options.middleware Observe-only generation middleware.
|
|
200
|
+
* @param options.debug Debug logging option.
|
|
201
|
+
*
|
|
202
|
+
* @example Route a support ticket
|
|
203
|
+
* ```ts
|
|
204
|
+
* import { decide, choice, score, boolean } from '@tanstack/ai'
|
|
205
|
+
* import { typesafeDecider } from '@tanstack/ai-typesafe'
|
|
206
|
+
*
|
|
207
|
+
* const result = await decide({
|
|
208
|
+
* adapter: typesafeDecider('jev-latest'),
|
|
209
|
+
* state: ticket,
|
|
210
|
+
* questions: {
|
|
211
|
+
* queue: choice({
|
|
212
|
+
* instructions: 'Which team should handle this ticket?',
|
|
213
|
+
* options: {
|
|
214
|
+
* billing: 'Payments, invoices, refunds',
|
|
215
|
+
* tech: 'Bugs, outages, integrations',
|
|
216
|
+
* sales: 'Pricing, upgrades, new accounts',
|
|
217
|
+
* },
|
|
218
|
+
* }),
|
|
219
|
+
* urgency: score({
|
|
220
|
+
* instructions: 'How urgent is this ticket?',
|
|
221
|
+
* levels: ['low', 'medium', 'high'],
|
|
222
|
+
* }),
|
|
223
|
+
* refund: boolean({
|
|
224
|
+
* instructions: 'Is the customer asking for a refund?',
|
|
225
|
+
* }),
|
|
226
|
+
* },
|
|
227
|
+
* })
|
|
228
|
+
*
|
|
229
|
+
* result.queue.value
|
|
230
|
+
* result.meta.model
|
|
231
|
+
* result.meta.usage
|
|
232
|
+
* ```
|
|
233
|
+
*/
|
|
234
|
+
async function decide(options) {
|
|
235
|
+
const { adapter, state, questions, modelOptions, abortSignal, middleware, debug } = options;
|
|
236
|
+
const model = adapter.model;
|
|
237
|
+
const keys = assertQuestions(questions);
|
|
238
|
+
const requestId = createId("evaluate");
|
|
239
|
+
const startTime = Date.now();
|
|
240
|
+
const logger = resolveDebugOption(debug);
|
|
241
|
+
const mwCtx = createGenerationContext({
|
|
242
|
+
requestId,
|
|
243
|
+
activity: "evaluate",
|
|
244
|
+
provider: adapter.name,
|
|
245
|
+
model,
|
|
246
|
+
modelOptions,
|
|
247
|
+
createId
|
|
248
|
+
});
|
|
249
|
+
await runGenerationStart(middleware, mwCtx);
|
|
250
|
+
aiEventClient.emit("evaluate:request:started", {
|
|
251
|
+
requestId,
|
|
252
|
+
provider: adapter.name,
|
|
253
|
+
model,
|
|
254
|
+
questionCount: keys.length,
|
|
255
|
+
timestamp: startTime
|
|
256
|
+
});
|
|
257
|
+
logger.request(`activity=evaluate provider=${adapter.name}`, {
|
|
258
|
+
provider: adapter.name,
|
|
259
|
+
model,
|
|
260
|
+
questionCount: keys.length
|
|
261
|
+
});
|
|
262
|
+
try {
|
|
263
|
+
const result = await adapter.evaluate({
|
|
264
|
+
model,
|
|
265
|
+
state,
|
|
266
|
+
questions,
|
|
267
|
+
modelOptions,
|
|
268
|
+
abortSignal,
|
|
269
|
+
logger
|
|
270
|
+
});
|
|
271
|
+
const answers = mapAnswers(questions, result.answers);
|
|
272
|
+
const duration = Date.now() - startTime;
|
|
273
|
+
aiEventClient.emit("evaluate:request:completed", {
|
|
274
|
+
requestId,
|
|
275
|
+
provider: adapter.name,
|
|
276
|
+
model: result.model,
|
|
277
|
+
questionCount: keys.length,
|
|
278
|
+
duration,
|
|
279
|
+
timestamp: Date.now()
|
|
280
|
+
});
|
|
281
|
+
aiEventClient.emit("evaluate:usage", {
|
|
282
|
+
requestId,
|
|
283
|
+
model: result.model,
|
|
284
|
+
usage: result.usage,
|
|
285
|
+
timestamp: Date.now()
|
|
286
|
+
});
|
|
287
|
+
logger.output(`activity=evaluate answers=${keys.length}`, { answerCount: keys.length });
|
|
288
|
+
await runGenerationUsage(middleware, mwCtx, result.usage);
|
|
289
|
+
await runGenerationFinish(middleware, mwCtx, {
|
|
290
|
+
duration,
|
|
291
|
+
usage: result.usage
|
|
292
|
+
});
|
|
293
|
+
return withMeta(answers, {
|
|
294
|
+
model: result.model,
|
|
295
|
+
usage: result.usage
|
|
296
|
+
});
|
|
297
|
+
} catch (error) {
|
|
298
|
+
const duration = Date.now() - startTime;
|
|
299
|
+
if (isAbortError(error, abortSignal)) await runGenerationAbort(middleware, mwCtx, {
|
|
300
|
+
reason: error instanceof Error ? error.message : void 0,
|
|
301
|
+
duration
|
|
302
|
+
});
|
|
303
|
+
else await runGenerationError(middleware, mwCtx, {
|
|
304
|
+
error,
|
|
305
|
+
duration
|
|
306
|
+
});
|
|
307
|
+
logger.errors("evaluate activity failed", {
|
|
308
|
+
error,
|
|
309
|
+
source: "evaluate"
|
|
310
|
+
});
|
|
311
|
+
throw error;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
//#endregion
|
|
315
|
+
export { boolean, choice, decide, kind, score };
|
|
316
|
+
|
|
317
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../../src/activities/evaluate/index.ts"],"sourcesContent":["/**\n * Evaluate Activity\n *\n * Asks typed questions about a shared state and returns values your code can\n * branch on. This is a self-contained module with implementation, types, and\n * JSDoc.\n */\n\nimport { aiEventClient } from '@tanstack/ai-event-client'\nimport { resolveDebugOption } from '../../logger/resolve'\nimport { isAbortShapedError } from '../error-payload'\nimport {\n createGenerationContext,\n runGenerationAbort,\n runGenerationError,\n runGenerationFinish,\n runGenerationStart,\n runGenerationUsage,\n} from '../middleware/run'\nimport type { InternalLogger } from '../../logger/internal-logger'\nimport type { DebugOption } from '../../logger/types'\nimport type { TokenUsage } from '../../types'\nimport type { GenerationMiddleware } from '../middleware/types'\nimport type {\n EvaluateAdapter,\n EvaluateInstructions,\n EvaluateState,\n WireAnswer,\n WireChoiceAnswer,\n WireNoulAnswer,\n WireQuestion,\n WireScoreAnswer,\n WireScoreQuestion,\n} from './adapter'\n\n// ===========================\n// Activity Kind\n// ===========================\n\n/** The adapter kind this activity handles */\nexport const kind = 'evaluate' as const\n\n/** Question key reserved for `result.meta`. */\nconst RESERVED_QUESTION_KEY = 'meta' as const\n\n// ===========================\n// Type Extraction Helpers\n// ===========================\n\n/** Extract provider options from an EvaluateAdapter via ~types */\nexport type EvaluateProviderOptions<TAdapter> = TAdapter extends {\n '~types': { providerOptions: infer P extends object }\n}\n ? P\n : object\n\n// ===========================\n// Unified answers\n// ===========================\n\n/**\n * Public choice answer. `.value` is the selected option key.\n */\nexport interface ChoiceAnswer<TValue extends string = string> {\n type: 'choice'\n value: TValue\n /** P(selected option). */\n probability: number\n confidence: number\n probabilities: Record<TValue, number>\n}\n\n/**\n * Public score answer. `.value` is the nearest level label.\n * `.score` is the raw TypeSafe fraction.\n */\nexport interface ScoreAnswer<TLevel extends string = string> {\n type: 'score'\n value: TLevel\n /** P(nearest level). */\n probability: number\n confidence: number\n score: number\n legend: Record<string, string>\n probabilities: Record<string, number>\n}\n\n/**\n * Public yes/no answer. `.value` is `true` when P(true) is 0.5 or more.\n * There is no `.confidence`.\n */\nexport interface BooleanAnswer {\n type: 'boolean'\n value: boolean\n /** P(true), from the wire `noul` field. */\n probability: number\n}\n\nexport interface EvaluateResultMeta {\n /** Resolved model id from the provider. */\n model: string\n usage: TokenUsage\n}\n\n/**\n * Map a helper question (or wire question) to its public answer type.\n */\nexport type InferEvaluateAnswer<TQuestion> = TQuestion extends {\n type: 'choice'\n criteria: infer TCriteria\n}\n ? TCriteria extends Record<string, string | null>\n ? ChoiceAnswer<Extract<keyof TCriteria, string>>\n : ChoiceAnswer\n : TQuestion extends { type: 'score'; criteria: infer TLevels }\n ? TLevels extends ReadonlyArray<string>\n ? ScoreAnswer<TLevels[number] & string>\n : ScoreAnswer\n : TQuestion extends { type: 'noul' }\n ? BooleanAnswer\n : never\n\n/**\n * Result of `decide()`. Each question key is a top-level answer.\n * `meta` holds the resolved model id and usage.\n */\nexport type EvaluateResult<TQuestions extends Record<string, WireQuestion>> = {\n [K in keyof TQuestions as K extends typeof RESERVED_QUESTION_KEY\n ? never\n : K]: InferEvaluateAnswer<TQuestions[K]>\n} & {\n meta: EvaluateResultMeta\n}\n\n// ===========================\n// Activity Options Types\n// ===========================\n\n/**\n * Options for the evaluate activity. The model is extracted from the\n * adapter's model property.\n *\n * @template TAdapter - The evaluate adapter type\n * @template TQuestions - The questions object passed to `decide`\n */\nexport interface EvaluateActivityOptions<\n TAdapter extends EvaluateAdapter<string, EvaluateProviderOptions<TAdapter>>,\n TQuestions extends Record<string, WireQuestion>,\n> {\n /** The evaluate adapter to use (must be created with a model) */\n adapter: TAdapter & { kind: typeof kind }\n /** Shared state every question judges. A JSON array is one state, not a batch. */\n state: EvaluateState\n /**\n * Questions built with `choice`, `score`, and `boolean`.\n * The key `meta` is reserved.\n */\n questions: TQuestions\n /** Provider-specific options */\n modelOptions?: EvaluateProviderOptions<TAdapter>\n /** Forwarded to the provider request for cancellation. */\n abortSignal?: AbortSignal\n /**\n * Observe-only middleware notified on start, usage, success, abort, and\n * error. Pass `otelMiddleware()` to emit OpenTelemetry spans, or implement\n * the `GenerationMiddleware` contract for a custom backend.\n */\n middleware?: Array<GenerationMiddleware>\n /**\n * Enable debug logging. Pass `true` to enable all categories, `false` to\n * silence everything including errors, or a `DebugConfig` object for granular\n * control and/or a custom `Logger`.\n */\n debug?: DebugOption\n}\n\n// ===========================\n// Helper Functions\n// ===========================\n\nfunction createId(prefix: string): string {\n return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`\n}\n\nfunction isAbortError(error: unknown, signal?: AbortSignal): boolean {\n // Prefer the error's own identity over the signal state. A genuine\n // cancellation throws an abort-shaped error (DOM `AbortError`, the OpenRouter\n // SDK's `RequestAbortedError`, ...). Classifying on `signal.aborted` alone\n // would misroute a real failure to the abort hook whenever a shared signal\n // happens to already be aborted, hiding it from `onError` observers.\n if (isAbortShapedError(error)) return true\n // Fall back to signal state only for non-Error throws we can't otherwise\n // identify; a real Error with a non-abort name is never an abort.\n return error instanceof Error ? false : signal?.aborted === true\n}\n\nfunction questionKeys(questions: Record<string, WireQuestion>) {\n return Object.keys(questions)\n}\n\nfunction assertQuestions(questions: Record<string, WireQuestion>) {\n const keys = questionKeys(questions)\n if (keys.length === 0) {\n throw new Error('decide() requires at least one question')\n }\n if (Object.hasOwn(questions, RESERVED_QUESTION_KEY)) {\n throw new Error('decide() reserves the question key \"meta\"')\n }\n return keys\n}\n\nfunction mapChoiceAnswer(wire: WireChoiceAnswer, key: string) {\n const probability = wire.probabilities[wire.choice]\n if (typeof probability !== 'number') {\n throw new Error(\n `decide(): missing probability for choice \"${wire.choice}\" on \"${key}\"`,\n )\n }\n return {\n type: 'choice' as const,\n value: wire.choice,\n probability,\n confidence: wire.confidence,\n probabilities: wire.probabilities,\n }\n}\n\nfunction mapScoreAnswer(\n question: WireScoreQuestion,\n wire: WireScoreAnswer,\n key: string,\n) {\n const levels = question.criteria\n if (levels.length < 2) {\n throw new Error(\n `decide(): score question \"${key}\" needs at least two levels`,\n )\n }\n const lastIndex = levels.length - 1\n const rounded = Math.round(wire.score)\n const nearestIndex =\n rounded < 0 ? 0 : rounded > lastIndex ? lastIndex : rounded\n const value = levels[nearestIndex]\n if (value === undefined) {\n throw new Error(\n `decide(): score question \"${key}\" has no level at index ${nearestIndex}`,\n )\n }\n const probability = wire.probabilities[String(nearestIndex)]\n if (typeof probability !== 'number') {\n throw new Error(\n `decide(): missing probability for score level ${nearestIndex} on \"${key}\"`,\n )\n }\n return {\n type: 'score' as const,\n value,\n probability,\n confidence: wire.confidence,\n score: wire.score,\n legend: wire.legend,\n probabilities: wire.probabilities,\n }\n}\n\nfunction mapBooleanAnswer(wire: WireNoulAnswer) {\n return {\n type: 'boolean' as const,\n value: wire.noul >= 0.5,\n probability: wire.noul,\n }\n}\n\nfunction mapWireAnswer(question: WireQuestion, wire: WireAnswer, key: string) {\n switch (question.type) {\n case 'choice': {\n if (wire.type !== 'choice') {\n throw new Error(\n `decide(): expected choice answer for \"${key}\", got ${wire.type}`,\n )\n }\n return mapChoiceAnswer(wire, key)\n }\n case 'score': {\n if (wire.type !== 'score') {\n throw new Error(\n `decide(): expected score answer for \"${key}\", got ${wire.type}`,\n )\n }\n return mapScoreAnswer(question, wire, key)\n }\n case 'noul': {\n if (wire.type !== 'noul') {\n throw new Error(\n `decide(): expected noul answer for \"${key}\", got ${wire.type}`,\n )\n }\n return mapBooleanAnswer(wire)\n }\n }\n}\n\nfunction mapAnswers<TQuestions extends Record<string, WireQuestion>>(\n questions: TQuestions,\n wireAnswers: Record<string, WireAnswer>,\n) {\n const answers = {} as {\n [K in keyof TQuestions]: InferEvaluateAnswer<TQuestions[K]>\n }\n const keys = Object.keys(questions) as Array<keyof TQuestions>\n for (const key of keys) {\n const question = questions[key]\n const wire = wireAnswers[String(key)]\n if (question === undefined) {\n throw new Error(`decide(): missing question \"${String(key)}\"`)\n }\n if (wire === undefined) {\n throw new Error(`decide(): missing answer for question \"${String(key)}\"`)\n }\n answers[key] = mapWireAnswer(\n question,\n wire,\n String(key),\n ) as InferEvaluateAnswer<TQuestions[typeof key]>\n }\n return answers\n}\n\nfunction withMeta<TAnswers extends object>(\n answers: TAnswers,\n meta: EvaluateResultMeta,\n) {\n return { ...answers, meta }\n}\n\n// ===========================\n// Question helpers\n// ===========================\n\n/**\n * Build a choice question. The model picks one key from `options`.\n *\n * Option keys become the union on `.value`. Use `null` when a key needs no\n * extra description. On the wire, `options` is sent as TypeSafe `criteria`.\n *\n * @param options.instructions What the model should decide.\n * @param options.options Map of option key to description, or `null`.\n *\n * @example\n * ```ts\n * const queue = choice({\n * instructions: 'Which team should handle this ticket?',\n * options: {\n * billing: 'Payments, invoices, refunds',\n * tech: 'Bugs, outages, integrations',\n * sales: 'Pricing, upgrades, new accounts',\n * },\n * })\n * ```\n */\nexport function choice<\n const TOptions extends Record<string, string | null>,\n>(options: { instructions: EvaluateInstructions; options: TOptions }) {\n return {\n type: 'choice' as const,\n instructions: options.instructions,\n criteria: options.options,\n }\n}\n\n/**\n * Build a score question. The model rates `state` on ordered `levels`.\n *\n * You must pass at least two levels. `.value` is the nearest level label.\n * The raw fraction stays on `.score`. On the wire, `levels` is sent as\n * TypeSafe `criteria`.\n *\n * @param options.instructions What the model should rate.\n * @param options.levels Ordered labels, lowest first. At least two.\n *\n * @example\n * ```ts\n * const urgency = score({\n * instructions: 'How urgent is this ticket?',\n * levels: ['low', 'medium', 'high'],\n * })\n * ```\n */\nexport function score<const TLevels extends ReadonlyArray<string>>(options: {\n instructions: EvaluateInstructions\n levels: TLevels\n}) {\n if (options.levels.length < 2) {\n throw new Error('score() requires at least two levels')\n }\n return {\n type: 'score' as const,\n instructions: options.instructions,\n criteria: options.levels,\n }\n}\n\n/**\n * Build a yes/no question.\n *\n * `.value` is `true` when P(true) is 0.5 or more. There is no `.confidence`.\n * On the wire, the type is TypeSafe `noul`.\n *\n * @param options.instructions The yes/no question to judge.\n * @param options.criteria Optional descriptions of yes and no.\n *\n * @example\n * ```ts\n * const refund = boolean({\n * instructions: 'Is the customer asking for a refund?',\n * })\n * ```\n */\nexport function boolean(options: {\n instructions: EvaluateInstructions\n criteria?: {\n true?: string\n false?: string\n }\n}) {\n if (options.criteria === undefined) {\n return {\n type: 'noul' as const,\n instructions: options.instructions,\n }\n }\n return {\n type: 'noul' as const,\n instructions: options.instructions,\n criteria: options.criteria,\n }\n}\n\n// ===========================\n// Activity Implementation\n// ===========================\n\n/**\n * Ask typed questions about `state` and get answers your code can branch on.\n *\n * You have state (a ticket, a record, a log) and you need typed answers, not\n * prose. Pass questions built with `choice`, `score`, and `boolean`. Then\n * branch on `result.queue.value` in ordinary TypeScript.\n *\n * The question key `meta` is reserved. Throws if `questions` is empty or uses\n * that key.\n *\n * @param options.adapter Evaluate adapter created with a model.\n * @param options.state Shared state every question judges.\n * @param options.questions Questions built with `choice`, `score`, `boolean`.\n * @param options.modelOptions Provider-specific options.\n * @param options.abortSignal Cancels the in-flight request.\n * @param options.middleware Observe-only generation middleware.\n * @param options.debug Debug logging option.\n *\n * @example Route a support ticket\n * ```ts\n * import { decide, choice, score, boolean } from '@tanstack/ai'\n * import { typesafeDecider } from '@tanstack/ai-typesafe'\n *\n * const result = await decide({\n * adapter: typesafeDecider('jev-latest'),\n * state: ticket,\n * questions: {\n * queue: choice({\n * instructions: 'Which team should handle this ticket?',\n * options: {\n * billing: 'Payments, invoices, refunds',\n * tech: 'Bugs, outages, integrations',\n * sales: 'Pricing, upgrades, new accounts',\n * },\n * }),\n * urgency: score({\n * instructions: 'How urgent is this ticket?',\n * levels: ['low', 'medium', 'high'],\n * }),\n * refund: boolean({\n * instructions: 'Is the customer asking for a refund?',\n * }),\n * },\n * })\n *\n * result.queue.value\n * result.meta.model\n * result.meta.usage\n * ```\n */\nexport async function decide<\n TAdapter extends EvaluateAdapter<string, EvaluateProviderOptions<TAdapter>>,\n TQuestions extends Record<string, WireQuestion>,\n>(options: EvaluateActivityOptions<TAdapter, TQuestions>) {\n const {\n adapter,\n state,\n questions,\n modelOptions,\n abortSignal,\n middleware,\n debug,\n } = options\n const model = adapter.model\n const keys = assertQuestions(questions)\n const requestId = createId('evaluate')\n const startTime = Date.now()\n const logger: InternalLogger = resolveDebugOption(debug)\n\n const mwCtx = createGenerationContext({\n requestId,\n activity: 'evaluate',\n provider: adapter.name,\n model,\n modelOptions,\n createId,\n })\n\n await runGenerationStart(middleware, mwCtx)\n\n aiEventClient.emit('evaluate:request:started', {\n requestId,\n provider: adapter.name,\n model,\n questionCount: keys.length,\n timestamp: startTime,\n })\n\n logger.request(`activity=evaluate provider=${adapter.name}`, {\n provider: adapter.name,\n model,\n questionCount: keys.length,\n })\n\n try {\n const result = await adapter.evaluate({\n model,\n state,\n questions,\n modelOptions,\n abortSignal,\n logger,\n })\n\n const answers = mapAnswers(questions, result.answers)\n const duration = Date.now() - startTime\n\n aiEventClient.emit('evaluate:request:completed', {\n requestId,\n provider: adapter.name,\n model: result.model,\n questionCount: keys.length,\n duration,\n timestamp: Date.now(),\n })\n\n aiEventClient.emit('evaluate:usage', {\n requestId,\n model: result.model,\n usage: result.usage,\n timestamp: Date.now(),\n })\n\n logger.output(`activity=evaluate answers=${keys.length}`, {\n answerCount: keys.length,\n })\n\n await runGenerationUsage(middleware, mwCtx, result.usage)\n await runGenerationFinish(middleware, mwCtx, {\n duration,\n usage: result.usage,\n })\n\n return withMeta(answers, {\n model: result.model,\n usage: result.usage,\n })\n } catch (error) {\n const duration = Date.now() - startTime\n if (isAbortError(error, abortSignal)) {\n await runGenerationAbort(middleware, mwCtx, {\n reason: error instanceof Error ? error.message : undefined,\n duration,\n })\n } else {\n await runGenerationError(middleware, mwCtx, { error, duration })\n }\n logger.errors('evaluate activity failed', { error, source: 'evaluate' })\n throw error\n }\n}\n\n// Re-export adapter types\nexport type {\n EvaluateAdapter,\n EvaluateAdapterConfig,\n AnyEvaluateAdapter,\n EvaluateOptions,\n EvaluateAdapterResult,\n EvaluateState,\n EvaluateInstructions,\n EvaluateJsonValue,\n WireQuestion,\n WireAnswer,\n WireChoiceQuestion,\n WireScoreQuestion,\n WireNoulQuestion,\n WireChoiceAnswer,\n WireScoreAnswer,\n WireNoulAnswer,\n} from './adapter'\nexport { BaseEvaluateAdapter } from './adapter'\n"],"mappings":";;;;;;;;;;;;;;AAwCA,IAAa,OAAO;;AAGpB,IAAM,wBAAwB;AAyI9B,SAAS,SAAS,QAAwB;CACxC,OAAO,GAAG,OAAO,GAAG,KAAK,IAAI,EAAE,GAAG,KAAK,OAAO,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,MAAM,GAAG,CAAC;AACzE;AAEA,SAAS,aAAa,OAAgB,QAA+B;CAMnE,IAAI,mBAAmB,KAAK,GAAG,OAAO;CAGtC,OAAO,iBAAiB,QAAQ,QAAQ,QAAQ,YAAY;AAC9D;AAEA,SAAS,aAAa,WAAyC;CAC7D,OAAO,OAAO,KAAK,SAAS;AAC9B;AAEA,SAAS,gBAAgB,WAAyC;CAChE,MAAM,OAAO,aAAa,SAAS;CACnC,IAAI,KAAK,WAAW,GAClB,MAAM,IAAI,MAAM,yCAAyC;CAE3D,IAAI,OAAO,OAAO,WAAW,qBAAqB,GAChD,MAAM,IAAI,MAAM,6CAA2C;CAE7D,OAAO;AACT;AAEA,SAAS,gBAAgB,MAAwB,KAAa;CAC5D,MAAM,cAAc,KAAK,cAAc,KAAK;CAC5C,IAAI,OAAO,gBAAgB,UACzB,MAAM,IAAI,MACR,6CAA6C,KAAK,OAAO,QAAQ,IAAI,EACvE;CAEF,OAAO;EACL,MAAM;EACN,OAAO,KAAK;EACZ;EACA,YAAY,KAAK;EACjB,eAAe,KAAK;CACtB;AACF;AAEA,SAAS,eACP,UACA,MACA,KACA;CACA,MAAM,SAAS,SAAS;CACxB,IAAI,OAAO,SAAS,GAClB,MAAM,IAAI,MACR,6BAA6B,IAAI,4BACnC;CAEF,MAAM,YAAY,OAAO,SAAS;CAClC,MAAM,UAAU,KAAK,MAAM,KAAK,KAAK;CACrC,MAAM,eACJ,UAAU,IAAI,IAAI,UAAU,YAAY,YAAY;CACtD,MAAM,QAAQ,OAAO;CACrB,IAAI,UAAU,KAAA,GACZ,MAAM,IAAI,MACR,6BAA6B,IAAI,0BAA0B,cAC7D;CAEF,MAAM,cAAc,KAAK,cAAc,OAAO,YAAY;CAC1D,IAAI,OAAO,gBAAgB,UACzB,MAAM,IAAI,MACR,iDAAiD,aAAa,OAAO,IAAI,EAC3E;CAEF,OAAO;EACL,MAAM;EACN;EACA;EACA,YAAY,KAAK;EACjB,OAAO,KAAK;EACZ,QAAQ,KAAK;EACb,eAAe,KAAK;CACtB;AACF;AAEA,SAAS,iBAAiB,MAAsB;CAC9C,OAAO;EACL,MAAM;EACN,OAAO,KAAK,QAAQ;EACpB,aAAa,KAAK;CACpB;AACF;AAEA,SAAS,cAAc,UAAwB,MAAkB,KAAa;CAC5E,QAAQ,SAAS,MAAjB;EACE,KAAK;GACH,IAAI,KAAK,SAAS,UAChB,MAAM,IAAI,MACR,yCAAyC,IAAI,SAAS,KAAK,MAC7D;GAEF,OAAO,gBAAgB,MAAM,GAAG;EAElC,KAAK;GACH,IAAI,KAAK,SAAS,SAChB,MAAM,IAAI,MACR,wCAAwC,IAAI,SAAS,KAAK,MAC5D;GAEF,OAAO,eAAe,UAAU,MAAM,GAAG;EAE3C,KAAK;GACH,IAAI,KAAK,SAAS,QAChB,MAAM,IAAI,MACR,uCAAuC,IAAI,SAAS,KAAK,MAC3D;GAEF,OAAO,iBAAiB,IAAI;CAEhC;AACF;AAEA,SAAS,WACP,WACA,aACA;CACA,MAAM,UAAU,CAAC;CAGjB,MAAM,OAAO,OAAO,KAAK,SAAS;CAClC,KAAK,MAAM,OAAO,MAAM;EACtB,MAAM,WAAW,UAAU;EAC3B,MAAM,OAAO,YAAY,OAAO,GAAG;EACnC,IAAI,aAAa,KAAA,GACf,MAAM,IAAI,MAAM,+BAA+B,OAAO,GAAG,EAAE,EAAE;EAE/D,IAAI,SAAS,KAAA,GACX,MAAM,IAAI,MAAM,0CAA0C,OAAO,GAAG,EAAE,EAAE;EAE1E,QAAQ,OAAO,cACb,UACA,MACA,OAAO,GAAG,CACZ;CACF;CACA,OAAO;AACT;AAEA,SAAS,SACP,SACA,MACA;CACA,OAAO;EAAE,GAAG;EAAS;CAAK;AAC5B;;;;;;;;;;;;;;;;;;;;;;AA2BA,SAAgB,OAEd,SAAoE;CACpE,OAAO;EACL,MAAM;EACN,cAAc,QAAQ;EACtB,UAAU,QAAQ;CACpB;AACF;;;;;;;;;;;;;;;;;;;AAoBA,SAAgB,MAAmD,SAGhE;CACD,IAAI,QAAQ,OAAO,SAAS,GAC1B,MAAM,IAAI,MAAM,sCAAsC;CAExD,OAAO;EACL,MAAM;EACN,cAAc,QAAQ;EACtB,UAAU,QAAQ;CACpB;AACF;;;;;;;;;;;;;;;;;AAkBA,SAAgB,QAAQ,SAMrB;CACD,IAAI,QAAQ,aAAa,KAAA,GACvB,OAAO;EACL,MAAM;EACN,cAAc,QAAQ;CACxB;CAEF,OAAO;EACL,MAAM;EACN,cAAc,QAAQ;EACtB,UAAU,QAAQ;CACpB;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwDA,eAAsB,OAGpB,SAAwD;CACxD,MAAM,EACJ,SACA,OACA,WACA,cACA,aACA,YACA,UACE;CACJ,MAAM,QAAQ,QAAQ;CACtB,MAAM,OAAO,gBAAgB,SAAS;CACtC,MAAM,YAAY,SAAS,UAAU;CACrC,MAAM,YAAY,KAAK,IAAI;CAC3B,MAAM,SAAyB,mBAAmB,KAAK;CAEvD,MAAM,QAAQ,wBAAwB;EACpC;EACA,UAAU;EACV,UAAU,QAAQ;EAClB;EACA;EACA;CACF,CAAC;CAED,MAAM,mBAAmB,YAAY,KAAK;CAE1C,cAAc,KAAK,4BAA4B;EAC7C;EACA,UAAU,QAAQ;EAClB;EACA,eAAe,KAAK;EACpB,WAAW;CACb,CAAC;CAED,OAAO,QAAQ,8BAA8B,QAAQ,QAAQ;EAC3D,UAAU,QAAQ;EAClB;EACA,eAAe,KAAK;CACtB,CAAC;CAED,IAAI;EACF,MAAM,SAAS,MAAM,QAAQ,SAAS;GACpC;GACA;GACA;GACA;GACA;GACA;EACF,CAAC;EAED,MAAM,UAAU,WAAW,WAAW,OAAO,OAAO;EACpD,MAAM,WAAW,KAAK,IAAI,IAAI;EAE9B,cAAc,KAAK,8BAA8B;GAC/C;GACA,UAAU,QAAQ;GAClB,OAAO,OAAO;GACd,eAAe,KAAK;GACpB;GACA,WAAW,KAAK,IAAI;EACtB,CAAC;EAED,cAAc,KAAK,kBAAkB;GACnC;GACA,OAAO,OAAO;GACd,OAAO,OAAO;GACd,WAAW,KAAK,IAAI;EACtB,CAAC;EAED,OAAO,OAAO,6BAA6B,KAAK,UAAU,EACxD,aAAa,KAAK,OACpB,CAAC;EAED,MAAM,mBAAmB,YAAY,OAAO,OAAO,KAAK;EACxD,MAAM,oBAAoB,YAAY,OAAO;GAC3C;GACA,OAAO,OAAO;EAChB,CAAC;EAED,OAAO,SAAS,SAAS;GACvB,OAAO,OAAO;GACd,OAAO,OAAO;EAChB,CAAC;CACH,SAAS,OAAO;EACd,MAAM,WAAW,KAAK,IAAI,IAAI;EAC9B,IAAI,aAAa,OAAO,WAAW,GACjC,MAAM,mBAAmB,YAAY,OAAO;GAC1C,QAAQ,iBAAiB,QAAQ,MAAM,UAAU,KAAA;GACjD;EACF,CAAC;OAED,MAAM,mBAAmB,YAAY,OAAO;GAAE;GAAO;EAAS,CAAC;EAEjE,OAAO,OAAO,4BAA4B;GAAE;GAAO,QAAQ;EAAW,CAAC;EACvE,MAAM;CACR;AACF"}
|
|
@@ -1,4 +1,20 @@
|
|
|
1
|
-
import { TTSOptions, TTSResult } from '../../types.js';
|
|
1
|
+
import { ListVoicesOptions, ListVoicesResult, TTSOptions, TTSResult } from '../../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* What a TTS adapter can do beyond a single voice reading a single string.
|
|
4
|
+
*
|
|
5
|
+
* Declared statically so `generateSpeech()` can reject an unsupported request
|
|
6
|
+
* before it reaches the provider, instead of surfacing a provider 422.
|
|
7
|
+
*/
|
|
8
|
+
export interface TTSCapabilities {
|
|
9
|
+
/**
|
|
10
|
+
* Maximum number of distinct voices accepted across `turns`
|
|
11
|
+
* (ElevenLabs 10, Gemini 2). Omit it when the adapter has no dialogue
|
|
12
|
+
* endpoint — then `turns` is rejected outright.
|
|
13
|
+
*/
|
|
14
|
+
maxSpeakers?: number;
|
|
15
|
+
/** Set when the adapter can honour `timestamps: true`. */
|
|
16
|
+
timestamps?: boolean;
|
|
17
|
+
}
|
|
2
18
|
/**
|
|
3
19
|
* Configuration for TTS adapter instances
|
|
4
20
|
*/
|
|
@@ -26,6 +42,11 @@ export interface TTSAdapter<TModel extends string = string, TProviderOptions ext
|
|
|
26
42
|
readonly name: string;
|
|
27
43
|
/** The model this adapter is configured for */
|
|
28
44
|
readonly model: TModel;
|
|
45
|
+
/**
|
|
46
|
+
* Optional static capability declaration. Absent means "single voice, no
|
|
47
|
+
* timestamps" — the contract every adapter had before dialogue existed.
|
|
48
|
+
*/
|
|
49
|
+
readonly capabilities?: TTSCapabilities;
|
|
29
50
|
/**
|
|
30
51
|
* @internal Type-only properties for inference. Not assigned at runtime.
|
|
31
52
|
*/
|
|
@@ -36,6 +57,17 @@ export interface TTSAdapter<TModel extends string = string, TProviderOptions ext
|
|
|
36
57
|
* Generate speech from text
|
|
37
58
|
*/
|
|
38
59
|
generateSpeech: (options: TTSOptions<TProviderOptions>) => Promise<TTSResult>;
|
|
60
|
+
/**
|
|
61
|
+
* List the voices this account can use.
|
|
62
|
+
*
|
|
63
|
+
* Optional, because only some providers have a catalog worth querying at
|
|
64
|
+
* runtime. A provider whose voices are a fixed list known at build time
|
|
65
|
+
* publishes that list from its own package instead (`GeminiTTSVoices`, or
|
|
66
|
+
* the `OpenAITTSVoice` union), which is strictly better than a network
|
|
67
|
+
* call. Implement this only when the catalog is per-account and can change,
|
|
68
|
+
* which is the case wherever `generateVoice()` can add to it.
|
|
69
|
+
*/
|
|
70
|
+
listVoices?: (options?: ListVoicesOptions) => Promise<ListVoicesResult>;
|
|
39
71
|
}
|
|
40
72
|
/**
|
|
41
73
|
* A TTSAdapter with any/unknown type parameters.
|
|
@@ -52,11 +84,17 @@ export declare abstract class BaseTTSAdapter<TModel extends string = string, TPr
|
|
|
52
84
|
readonly kind: "tts";
|
|
53
85
|
abstract readonly name: string;
|
|
54
86
|
readonly model: TModel;
|
|
87
|
+
readonly capabilities?: TTSCapabilities;
|
|
55
88
|
'~types': {
|
|
56
89
|
providerOptions: TProviderOptions;
|
|
57
90
|
};
|
|
58
91
|
protected config: TTSAdapterConfig;
|
|
59
92
|
constructor(model: TModel, config?: TTSAdapterConfig);
|
|
60
93
|
abstract generateSpeech(options: TTSOptions<TProviderOptions>): Promise<TTSResult>;
|
|
94
|
+
/**
|
|
95
|
+
* Not abstract: a provider with a fixed voice list has nothing to query and
|
|
96
|
+
* should not be forced to write a stub.
|
|
97
|
+
*/
|
|
98
|
+
listVoices?(options?: ListVoicesOptions): Promise<ListVoicesResult>;
|
|
61
99
|
protected generateId(): string;
|
|
62
100
|
}
|