@juspay/neurolink
Version:
The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002
229 lines (228 loc) • 8.53 kB
JavaScript
import { asNumber, isRecord } from "../guards.js";
const PROBABILITY_SUM_TOLERANCE = 0.01;
// A sum one tolerance away (0.99, 1.01) lands a rounding error past the
// tolerance in floating point; it is within it.
const SUM_ROUNDING_SLACK = 1e-9;
const text = (value) => typeof value === "string" ? value : JSON.stringify(value);
const described = (value) => value === null || value === undefined ? {} : { description: text(value) };
function encodeQuestion(question) {
if (question.type === "boolean") {
// The wire has no per-side text, so each side joins the instructions.
const { true: whenTrue, false: whenFalse } = question.criteria ?? {};
return {
type: "predicate",
instructions: text(question.instructions) +
(whenTrue === null || whenTrue === undefined
? ""
: `\n\nAnswer true when: ${text(whenTrue)}`) +
(whenFalse === null || whenFalse === undefined
? ""
: `\nAnswer false when: ${text(whenFalse)}`),
};
}
if (question.type === "choice") {
return {
type: "choice",
instructions: text(question.instructions),
choices: Object.entries(question.criteria).map(([value, rubric]) => ({
value,
...described(rubric),
})),
};
}
// The index is the label so a returned `value` and `label` can never
// disagree about which level it is; the rubric text rides in `description`.
return {
type: "score",
instructions: text(question.instructions),
levels: question.criteria.map((rubric, index) => ({
label: String(index),
...described(rubric),
})),
};
}
const inUnitRange = (value) => value >= 0 && value <= 1;
/**
* The answer's distribution keyed by the keys that were asked, or null unless
* it names each asked key exactly once, with probabilities that sum to one.
* The result is in asked order, not the server's.
*/
function readDistribution(raw, asked, keyOf) {
if (!Array.isArray(raw) || raw.length !== asked.length) {
return null;
}
const expected = new Set(asked);
const seen = new Map();
for (const entry of raw) {
if (!isRecord(entry)) {
return null;
}
const key = keyOf(entry.value);
const probability = asNumber(entry.probability);
if (key === undefined ||
probability === undefined ||
!inUnitRange(probability) ||
!expected.has(key) ||
seen.has(key)) {
return null;
}
seen.set(key, probability);
}
let sum = 0;
for (const probability of seen.values()) {
sum += probability;
}
if (Math.abs(sum - 1) > PROBABILITY_SUM_TOLERANCE + SUM_ROUNDING_SLACK) {
return null;
}
return Object.fromEntries(asked.map((key) => [key, seen.get(key) ?? 0]));
}
const peak = (probabilities) => Math.max(...Object.values(probabilities));
/** The answer's own confidence, else the peak; null when its own is outside 0 to 1. */
function readConfidence(raw, probabilities) {
const own = asNumber(raw);
if (own === undefined) {
return peak(probabilities);
}
return inUnitRange(own) ? own : null;
}
function readChoice(question, raw) {
const options = Object.keys(question.criteria);
if (typeof raw.choice !== "string" || !options.includes(raw.choice)) {
return null;
}
const probabilities = readDistribution(raw.probabilities, options, (value) => typeof value === "string" ? value : undefined);
if (!probabilities) {
return null;
}
const confidence = readConfidence(raw.confidence, probabilities);
return confidence === null
? null
: { type: "choice", choice: raw.choice, confidence, probabilities };
}
function readScore(question, raw) {
const score = asNumber(raw.score);
const levels = question.criteria.map((_rubric, index) => String(index));
const probabilities = readDistribution(raw.probabilities, levels, (value) => typeof value === "number" && Number.isInteger(value)
? String(value)
: undefined);
// The probability-weighted mean of the level indices cannot leave 0 to n-1.
if (score === undefined ||
score < 0 ||
score > levels.length - 1 ||
!probabilities) {
return null;
}
const confidence = readConfidence(raw.confidence, probabilities);
if (confidence === null) {
return null;
}
return {
type: "score",
score,
confidence,
legend: Object.fromEntries(question.criteria.map((rubric, index) => [
String(index),
rubric === null || rubric === undefined ? "" : text(rubric),
])),
probabilities,
};
}
function readAnswer(question, raw) {
if (question.type === "boolean") {
const probability = asNumber(raw.probability);
return raw.type === "predicate" &&
probability !== undefined &&
inUnitRange(probability)
? { type: "boolean", probability }
: null;
}
if (question.type === "choice") {
return raw.type === "choice" ? readChoice(question, raw) : null;
}
return raw.type === "score" ? readScore(question, raw) : null;
}
/** Number of answers carrying each asked name, so a repeated one can be refused. */
function countAskedNames(raw, asked) {
const counts = new Map();
for (const item of raw) {
const name = isRecord(item) ? item.name : undefined;
if (typeof name === "string" && Object.hasOwn(asked, name)) {
counts.set(name, (counts.get(name) ?? 0) + 1);
}
}
return counts;
}
export const openaiDecisionsDialect = {
name: "openai-decisions",
encodeQuestion,
buildBody: ({ state, questions, model, media }) => {
const images = media?.images ?? [];
const stateText = text(state);
return {
...(model === undefined ? {} : { model }),
input: images.length === 0
? stateText
: [
{
role: "user",
content: [
...(stateText === ""
? []
: [{ type: "input_text", text: stateText }]),
...images.map((url) => ({
type: "input_image",
image_url: url,
})),
],
},
],
questions: Object.entries(questions).map(([name, question]) => ({
...question,
name,
})),
};
},
// The wire echoes each question's `name`, and the server's answer is checked
// against the question asked: one that fails any check is dropped and named,
// never repaired, so the other answers in the batch survive.
readAnswers: (decoded, _reportedConfidence, asked) => {
const raw = decoded.answers;
if (!Array.isArray(raw)) {
return undefined;
}
const counts = countAskedNames(raw, asked);
const answers = new Map();
const dropped = [];
const refused = [];
raw.forEach((item, index) => {
const record = isRecord(item) ? item : undefined;
const name = typeof record?.name === "string" ? record.name : undefined;
const question = name !== undefined && Object.hasOwn(asked, name)
? asked[name]
: undefined;
if (!record || name === undefined || !question) {
dropped.push(`#${index}`);
return;
}
if ((counts.get(name) ?? 0) > 1) {
if (!dropped.includes(name)) {
dropped.push(name);
}
return;
}
if (record.type === "refusal") {
refused.push(name);
return;
}
const answer = readAnswer(question, record);
if (answer) {
answers.set(name, answer);
}
else {
dropped.push(name);
}
});
return { answers: Object.fromEntries(answers), dropped, refused };
},
};