UNPKG

@tanstack/ai

Version:

Type-safe TypeScript AI SDK for streaming chat, tool calling, agents, structured outputs, and multimodal generation.

317 lines (316 loc) 9.84 kB
import { isAbortShapedError } from "../error-payload.js"; import { resolveDebugOption } from "../../logger/resolve.js"; import { createGenerationContext, runGenerationAbort, runGenerationError, runGenerationFinish, runGenerationStart, runGenerationUsage } from "../middleware/run.js"; import "./adapter.js"; import { aiEventClient } from "@tanstack/ai-event-client"; //#region src/activities/evaluate/index.ts /** * Evaluate Activity * * Asks typed questions about a shared state and returns values your code can * branch on. This is a self-contained module with implementation, types, and * JSDoc. */ /** The adapter kind this activity handles */ var kind = "evaluate"; /** Question key reserved for `result.meta`. */ var RESERVED_QUESTION_KEY = "meta"; function createId(prefix) { return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`; } function isAbortError(error, signal) { if (isAbortShapedError(error)) return true; return error instanceof Error ? false : signal?.aborted === true; } function questionKeys(questions) { return Object.keys(questions); } function assertQuestions(questions) { const keys = questionKeys(questions); if (keys.length === 0) throw new Error("decide() requires at least one question"); if (Object.hasOwn(questions, RESERVED_QUESTION_KEY)) throw new Error("decide() reserves the question key \"meta\""); return keys; } function mapChoiceAnswer(wire, key) { const probability = wire.probabilities[wire.choice]; if (typeof probability !== "number") throw new Error(`decide(): missing probability for choice "${wire.choice}" on "${key}"`); return { type: "choice", value: wire.choice, probability, confidence: wire.confidence, probabilities: wire.probabilities }; } function mapScoreAnswer(question, wire, key) { const levels = question.criteria; if (levels.length < 2) throw new Error(`decide(): score question "${key}" needs at least two levels`); const lastIndex = levels.length - 1; const rounded = Math.round(wire.score); const nearestIndex = rounded < 0 ? 0 : rounded > lastIndex ? lastIndex : rounded; const value = levels[nearestIndex]; if (value === void 0) throw new Error(`decide(): score question "${key}" has no level at index ${nearestIndex}`); const probability = wire.probabilities[String(nearestIndex)]; if (typeof probability !== "number") throw new Error(`decide(): missing probability for score level ${nearestIndex} on "${key}"`); return { type: "score", value, probability, confidence: wire.confidence, score: wire.score, legend: wire.legend, probabilities: wire.probabilities }; } function mapBooleanAnswer(wire) { return { type: "boolean", value: wire.noul >= .5, probability: wire.noul }; } function mapWireAnswer(question, wire, key) { switch (question.type) { case "choice": if (wire.type !== "choice") throw new Error(`decide(): expected choice answer for "${key}", got ${wire.type}`); return mapChoiceAnswer(wire, key); case "score": if (wire.type !== "score") throw new Error(`decide(): expected score answer for "${key}", got ${wire.type}`); return mapScoreAnswer(question, wire, key); case "noul": if (wire.type !== "noul") throw new Error(`decide(): expected noul answer for "${key}", got ${wire.type}`); return mapBooleanAnswer(wire); } } function mapAnswers(questions, wireAnswers) { const answers = {}; const keys = Object.keys(questions); for (const key of keys) { const question = questions[key]; const wire = wireAnswers[String(key)]; if (question === void 0) throw new Error(`decide(): missing question "${String(key)}"`); if (wire === void 0) throw new Error(`decide(): missing answer for question "${String(key)}"`); answers[key] = mapWireAnswer(question, wire, String(key)); } return answers; } function withMeta(answers, meta) { return { ...answers, meta }; } /** * Build a choice question. The model picks one key from `options`. * * Option keys become the union on `.value`. Use `null` when a key needs no * extra description. On the wire, `options` is sent as TypeSafe `criteria`. * * @param options.instructions What the model should decide. * @param options.options Map of option key to description, or `null`. * * @example * ```ts * const queue = choice({ * instructions: 'Which team should handle this ticket?', * options: { * billing: 'Payments, invoices, refunds', * tech: 'Bugs, outages, integrations', * sales: 'Pricing, upgrades, new accounts', * }, * }) * ``` */ function choice(options) { return { type: "choice", instructions: options.instructions, criteria: options.options }; } /** * Build a score question. The model rates `state` on ordered `levels`. * * You must pass at least two levels. `.value` is the nearest level label. * The raw fraction stays on `.score`. On the wire, `levels` is sent as * TypeSafe `criteria`. * * @param options.instructions What the model should rate. * @param options.levels Ordered labels, lowest first. At least two. * * @example * ```ts * const urgency = score({ * instructions: 'How urgent is this ticket?', * levels: ['low', 'medium', 'high'], * }) * ``` */ function score(options) { if (options.levels.length < 2) throw new Error("score() requires at least two levels"); return { type: "score", instructions: options.instructions, criteria: options.levels }; } /** * Build a yes/no question. * * `.value` is `true` when P(true) is 0.5 or more. There is no `.confidence`. * On the wire, the type is TypeSafe `noul`. * * @param options.instructions The yes/no question to judge. * @param options.criteria Optional descriptions of yes and no. * * @example * ```ts * const refund = boolean({ * instructions: 'Is the customer asking for a refund?', * }) * ``` */ function boolean(options) { if (options.criteria === void 0) return { type: "noul", instructions: options.instructions }; return { type: "noul", instructions: options.instructions, criteria: options.criteria }; } /** * Ask typed questions about `state` and get answers your code can branch on. * * You have state (a ticket, a record, a log) and you need typed answers, not * prose. Pass questions built with `choice`, `score`, and `boolean`. Then * branch on `result.queue.value` in ordinary TypeScript. * * The question key `meta` is reserved. Throws if `questions` is empty or uses * that key. * * @param options.adapter Evaluate adapter created with a model. * @param options.state Shared state every question judges. * @param options.questions Questions built with `choice`, `score`, `boolean`. * @param options.modelOptions Provider-specific options. * @param options.abortSignal Cancels the in-flight request. * @param options.middleware Observe-only generation middleware. * @param options.debug Debug logging option. * * @example Route a support ticket * ```ts * import { decide, choice, score, boolean } from '@tanstack/ai' * import { typesafeDecider } from '@tanstack/ai-typesafe' * * const result = await decide({ * adapter: typesafeDecider('jev-latest'), * state: ticket, * questions: { * queue: choice({ * instructions: 'Which team should handle this ticket?', * options: { * billing: 'Payments, invoices, refunds', * tech: 'Bugs, outages, integrations', * sales: 'Pricing, upgrades, new accounts', * }, * }), * urgency: score({ * instructions: 'How urgent is this ticket?', * levels: ['low', 'medium', 'high'], * }), * refund: boolean({ * instructions: 'Is the customer asking for a refund?', * }), * }, * }) * * result.queue.value * result.meta.model * result.meta.usage * ``` */ async function decide(options) { const { adapter, state, questions, modelOptions, abortSignal, middleware, debug } = options; const model = adapter.model; const keys = assertQuestions(questions); const requestId = createId("evaluate"); const startTime = Date.now(); const logger = resolveDebugOption(debug); const mwCtx = createGenerationContext({ requestId, activity: "evaluate", provider: adapter.name, model, modelOptions, createId }); await runGenerationStart(middleware, mwCtx); aiEventClient.emit("evaluate:request:started", { requestId, provider: adapter.name, model, questionCount: keys.length, timestamp: startTime }); logger.request(`activity=evaluate provider=${adapter.name}`, { provider: adapter.name, model, questionCount: keys.length }); try { const result = await adapter.evaluate({ model, state, questions, modelOptions, abortSignal, logger }); const answers = mapAnswers(questions, result.answers); const duration = Date.now() - startTime; aiEventClient.emit("evaluate:request:completed", { requestId, provider: adapter.name, model: result.model, questionCount: keys.length, duration, timestamp: Date.now() }); aiEventClient.emit("evaluate:usage", { requestId, model: result.model, usage: result.usage, timestamp: Date.now() }); logger.output(`activity=evaluate answers=${keys.length}`, { answerCount: keys.length }); await runGenerationUsage(middleware, mwCtx, result.usage); await runGenerationFinish(middleware, mwCtx, { duration, usage: result.usage }); return withMeta(answers, { model: result.model, usage: result.usage }); } catch (error) { const duration = Date.now() - startTime; if (isAbortError(error, abortSignal)) await runGenerationAbort(middleware, mwCtx, { reason: error instanceof Error ? error.message : void 0, duration }); else await runGenerationError(middleware, mwCtx, { error, duration }); logger.errors("evaluate activity failed", { error, source: "evaluate" }); throw error; } } //#endregion export { boolean, choice, decide, kind, score }; //# sourceMappingURL=index.js.map