UNPKG

@nytka/plugin-typesafe

Version:

TypeSafe connector for nytka projects. Asks Jev typed questions (noul, choice, score) about a file or an existing dataset and registers the judgements in datasets/ with the model, the questions hash and the usage.

130 lines (119 loc) • 5.91 kB
// Programmatic API. The CLI is a thin wrapper over exactly these. export { client, credentials, modelFromEnv, ask, classify, plan as planItems, prepare, validateQuestions, questionShape, estimateTokens, estimateCost, detectKeyField, datasetId, questionsKey, subjectKey, slugify, shortHash, canonical, explain, backoffMs, API_URL, API_KEY_ENV, MODEL_ENV, DEFAULT_MODEL, RATE_USD_PER_MTOK, RATE_AS_OF, STATE_TOKEN_LIMIT, QUESTION_TYPES, RETRIABLE_STATUS, UNRETRIABLE_CODES, MAX_RETRIES, MAX_RETRY_AFTER_MS, DEFAULT_CONCURRENCY, MAX_CONCURRENCY, TypeSafeError, TypeSafeHttpError, } from './typesafe.mjs' export { loadQuestions, loadState, resolveInput, ITEMS_KEYS } from './inputs.mjs' // Re-exported from @nytka/core so a caller that has this package installed does not need to // add core to reach the plumbing. Same courtesy every connector extends. export { findProjectRoot, loadEnv, writePayload, registerDataset, isoDate } from '@nytka/core' import { client, classify, plan as planItems, modelFromEnv, datasetId, shortHash, canonical, questionShape } from './typesafe.mjs' import { loadQuestions, resolveInput } from './inputs.mjs' import { findProjectRoot, loadEnv, writePayload, registerDataset, isoDate } from '@nytka/core' function projectRootOf (root) { const projectRoot = root || findProjectRoot() if (!projectRoot) throw new Error('no project.yaml found walking up from cwd — run inside a nytka project') return projectRoot } /** What run() would do, priced, with the input and questions resolved the same way run() * resolves them. Sends nothing, constructs no client, needs no credential — the plan is * what an agent reads instead of the payload, so it must be free. */ export async function plan ({ root, input, questions, field = null, limit = null, model, itemsKey = null } = {}) { const projectRoot = projectRootOf(root) loadEnv(projectRoot) const requested = model ?? modelFromEnv() const q = await loadQuestions(projectRoot, questions) const src = await resolveInput(projectRoot, input, { itemsKey }) const p = planItems({ items: src.items, questions: q.questions, model: requested, field, limit }) return { ...p, id: datasetId({ input: src, questions: q.questions, questionsName: q.name, model: requested }), input: { kind: src.kind, ref: src.ref, rawPath: src.rawPath, itemsKey: src.itemsKey, available: src.items.length }, questions: { name: q.name, path: q.path, hash: shortHash(canonical(q.questions)), ...questionShape(q.questions) }, } } /** Classify and store in one call: writes the payload into the project's datasets/ and * registers it in datasets/index.json. * * Returns { id, model, items, rowCount, calls, usage, estimatedCost, rawPath, registered } * — never the rows. Payloads must not enter agent context; that this connector read one * to produce its own is the whole reason it exists. * * run() never plans and never prompts. The plan step is the CLI's, because "show what * you would spend and stop" is a conversation with whoever typed the command, and a * programmatic caller has already had it. */ export async function run ({ root, input, questions, field = null, keyField = null, limit = null, concurrency, model, itemsKey = null, snapshot = false, register = true, ts, at = new Date(), } = {}) { const projectRoot = projectRootOf(root) // Before client(), which reads the credential straight out of process.env. loadEnv(projectRoot) const requested = model ?? modelFromEnv() const q = await loadQuestions(projectRoot, questions) const src = await resolveInput(projectRoot, input, { itemsKey }) const result = await classify({ ts: ts ?? client(), items: src.items, questions: q.questions, model: requested, field, keyField, limit, concurrency, input: { ref: src.ref, kind: src.kind, rawPath: src.rawPath, itemsKey: src.itemsKey }, questionsMeta: { path: q.path, name: q.name }, at, }) if (snapshot) result.id = datasetId({ input: src, questions: q.questions, questionsName: q.name, model: requested, collectedOn: isoDate(at) }) const rawPath = await writePayload(projectRoot, result.id, result) const base = { id: result.id, model: result.model, items: result.input.items, rowCount: result.rowCount, calls: result.apiMetadata.calls, usage: result.apiMetadata.usage, estimatedCost: result.apiMetadata.estimatedCost, rate: result.apiMetadata.rate, rawPath, } if (!register) return { ...base, registered: false } await registerDataset(projectRoot, { id: result.id, source: 'typesafe', operation: 'classify', // `subject`, as DataForSEO: the thing judged is not a property the project owns. subject: truncate(`${result.questions.name} over ${result.input.ref}`, 120), // The RESOLVED model — the provenance of the answers. The requested alias is in the id. model: result.model.resolved, // The first connector whose input is another dataset. Lineage is a PLG-001 item; until // the contract names the field, it is this one. inputs: [result.input.ref], inputHash: result.input.inputHash, questionsPath: result.questions.path, questionsHash: result.questions.hash, questionIds: result.questions.ids, questionTypes: result.questions.types, // Explicitly null, not absent: a judgement covers no date range. There is no `period` // either — staleness here is "the model changed", which `model` shows. dateRange: null, collectedAt: isoDate(at), validUntil: null, rawPath, rows: result.rowCount, schema: result.schema, summary: result.summary, status: 'current', producedBy: '@nytka/plugin-typesafe', }) return { ...base, registered: true } } function truncate (s, n) { const str = String(s ?? '') return str.length > n ? `${str.slice(0, n - 1)}…` : str }