Files
brk/website_next/ask/tools/api/answer.js
T
2026-07-28 21:35:02 +02:00

474 lines
16 KiB
JavaScript

import { renderApiAnswer } from "../render.js";
import { relevance } from "../text.js";
import { focusApiData } from "./result.js";
const MAX_FIELDS = 10;
/** @param {string} type */
function dimension(type) {
const value = type.toLowerCase();
if (value.includes("sats")) return "sats";
return value;
}
/** @param {string} type */
function displayedUnit(type) {
const value = dimension(type);
if (value === "sats") return " sats";
if (value === "number" || value === "integer" || value === "float") return "";
return ` ${type}`;
}
/**
* @typedef {Object} ApiAnswerField
* @property {string} ref
* @property {string} name
* @property {string} type
* @property {string} [description]
* @property {string} [ownDescription]
* @property {string | number | boolean} value
* @property {number} score
*
* @typedef {Object} ApiAnswerSpec
* @property {ApiAnswerField[]} fields
* @property {ApiAnswerField} [previous]
* @property {ApiAnswerField} [resolved]
* @property {ApiAnswerField} [direct]
* @property {ApiAnswerField[]} ambiguous
* @property {any[]} tools
*/
/** @param {unknown} value @param {string[]} path */
function valueAt(value, path) {
let current = value;
for (const key of path) {
if (!current || typeof current !== "object" || !Object.hasOwn(current, key)) {
return undefined;
}
current = /** @type {Record<string, unknown>} */ (current)[key];
}
return current;
}
/** @param {unknown} value */
function formattedValue(value) {
if (typeof value === "number") {
return new Intl.NumberFormat("en-US", {
maximumFractionDigits: 8,
}).format(value);
}
if (typeof value === "boolean") return value ? "yes" : "no";
return String(value);
}
/** @param {any} grounding */
export function summarizeApiAnswer(grounding) {
const data = focusApiData(grounding.data, grounding.arguments);
const responseFields = /** @type {{ name: string, type: string, description?: string, ownDescription?: string }[]} */ (
grounding.operation.response.fields ?? []
);
const fields = responseFields
.map((field) => ({
...field,
value: valueAt(data, field.name.split(".")),
}))
.filter((/** @type {any} */ { value }) =>
typeof value === "string" ||
typeof value === "number" ||
typeof value === "boolean"
)
.slice(0, 8);
if (!fields.length) {
return {
output: renderApiAnswer(
"The API returned no compact primitive fields to display.",
grounding.operation,
),
fields: [],
};
}
const output = fields
.map((/** @type {any} */ field) =>
`- **${field.name.replaceAll(".", " · ").replaceAll("_", " ")}**: ${
formattedValue(field.value)
}${typeof field.value === "number" ? displayedUnit(field.type) : ""}`
)
.join("\n");
return {
output: renderApiAnswer(output, grounding.operation),
fields: fields.map((/** @type {any} */ { name }) => name),
};
}
/** @param {any} grounding @returns {ApiAnswerSpec} */
export function createApiAnswerTool(grounding) {
const data = focusApiData(grounding.data, grounding.arguments);
const responseFields = /** @type {{ name: string, type: string, description?: string, ownDescription?: string }[]} */ (
grounding.operation.response.fields ?? []
);
const previousName = grounding.previousFields?.length === 1
? grounding.previousFields[0]
: undefined;
const previousParents = new Set(
(grounding.previousFields ?? []).map((/** @type {string} */ name) =>
name.split(".").slice(0, -1).join(".")
),
);
const previousParent = previousParents.size === 1
? [...previousParents][0]
: undefined;
const parameterNames = new Set(
grounding.operation.parameters.map(
(/** @type {{ name: string }} */ parameter) => parameter.name,
),
);
const primitive = responseFields
.map((field) => ({
...field,
value: valueAt(data, field.name.split(".")),
}))
.filter((field) =>
typeof field.value === "string" ||
typeof field.value === "number" ||
typeof field.value === "boolean"
)
.map((field, index) => ({
...field,
index,
score: relevance(
grounding.question,
`${field.name} ${field.description ?? ""}`,
) +
relevance(grounding.question, field.name) +
relevance(grounding.question, field.ownDescription ?? "") -
Math.max(0, field.name.split(".").length - 1) * 2,
}))
.sort((left, right) => {
const leftParent = left.name.split(".").slice(0, -1).join(".");
const rightParent = right.name.split(".").slice(0, -1).join(".");
const leftAffinity = previousParent && leftParent === previousParent ? 2 : 0;
const rightAffinity = previousParent && rightParent === previousParent ? 2 : 0;
return right.score + rightAffinity - left.score - leftAffinity ||
left.index - right.index;
});
const answerCandidates = primitive.filter(({ name }) =>
name !== previousName &&
!parameterNames.has(name.split(".").at(-1) ?? name)
);
const numericCandidates = answerCandidates.filter(
({ value }) => typeof value === "number",
);
const best = numericCandidates
.sort((left, right) => right.score - left.score || left.index - right.index)[0];
const runnerUp = numericCandidates
.filter(({ name }) => name !== best?.name)
.sort((left, right) => right.score - left.score || left.index - right.index)[0];
const direct = best && best.score >= 6 &&
best.score >= (runnerUp?.score ?? 0) + 2
? best
: undefined;
const siblings = best
? primitive.filter((field) =>
field.name.split(".").at(-1) === best.name.split(".").at(-1) &&
best.score - field.score < 1 &&
field.score > 0
)
: [];
const matchingParent = previousParent
? siblings.filter((field) =>
field.name.split(".").slice(0, -1).join(".") === previousParent
)
: [];
const ambiguousNames = new Set(
(matchingParent.length === 1 ? [] : siblings).map(({ name }) => name),
);
const current = primitive.filter(({ name }) => name !== previousName);
const previousField = previousName
? primitive.find(({ name }) => name === previousName)
: undefined;
const selected = [
...current.slice(0, MAX_FIELDS - (previousField ? 1 : 0)),
...(previousField ? [previousField] : []),
];
const fields = selected
.map((field, index) => ({
...field,
value: /** @type {string | number | boolean} */ (field.value),
ref: `n${index + 1}`,
}));
const previousChoices = fields
.filter(({ name }) => grounding.previousFields?.includes(name))
.sort((left, right) => right.score - left.score);
const resolved = previousChoices.length > 1 &&
previousChoices[0].score >= previousChoices[1].score + 5
? previousChoices[0]
: undefined;
const previous = previousName
? fields.find((field) =>
field.name === previousName && typeof field.value === "number"
)
: undefined;
const numericFields = fields.filter((field) => typeof field.value === "number");
const calculationSplit = numericFields.findIndex((field, index) =>
index >= 2 && numericFields[index - 1].score - field.score >= 3
);
const calculationFields = calculationSplit >= 2
? numericFields.slice(0, calculationSplit)
: numericFields;
/**
* @param {string} name
* @param {string} description
* @param {Record<string, any>} properties
* @param {string[]} required
*/
const functionTool = (name, description, properties, required) => ({
type: "function",
function: {
name,
description,
parameters: {
type: "object",
properties,
required,
additionalProperties: false,
},
},
});
const label = {
type: "string",
description: "Short user-facing name for the result.",
};
const operator = {
type: "string",
enum: ["add", "subtract", "multiply", "divide"],
description: "The arithmetic operation explicitly requested by the user.",
};
const reference = {
type: "string",
enum: fields.map(({ ref }) => ref),
description: "Verified field ref from the user message.",
};
const numericReference = {
type: "string",
enum: calculationFields.map(({ ref }) => ref),
description: "Verified numeric field ref from the user message.",
};
const tools = [
functionTool(
"answer_api",
[
"Choose select for one raw primitive field.",
"Choose calculate to derive the result from component fields, including a narrower concept than an aggregate.",
previous
? `Choose continue only to apply arithmetic to preceding ${previous.ref}=${previous.name}.`
: "",
"Choose text for a nonnumeric answer copied or summarized from verified data.",
].filter(Boolean).join(" "),
{
action: {
type: "string",
enum: [
...(fields.length ? ["select"] : []),
...(calculationFields.length >= 2 ? ["calculate"] : []),
...(previous ? ["continue"] : []),
"text",
],
},
...(fields.length
? {
field: reference,
...(calculationFields.length === 2
? {
operator,
left: {
...numericReference,
description: "Left arithmetic operand: the minuend or dividend for subtract or divide.",
},
right: {
...numericReference,
description: "Right arithmetic operand: the subtrahend or divisor for subtract or divide.",
},
}
: calculationFields.length > 2
? {
operator,
operands: {
type: "array",
minItems: 2,
maxItems: 10,
items: numericReference,
description: "Ordered verified numeric fields for calculate.",
},
}
: {}),
}
: {}),
...(previous
? {
operand: {
type: "string",
enum: numericFields
.filter(({ ref }) => ref !== previous.ref)
.map(({ ref }) => ref),
description: `Second operand after fixed ${previous.ref}.`,
},
}
: {}),
label,
text: {
type: "string",
description: "Concise nonnumeric answer containing no invented values.",
},
},
["action"],
),
];
return {
fields,
previous,
resolved,
direct: direct ? fields.find(({ name }) => name === direct.name) : undefined,
ambiguous: fields.filter(({ name }) => ambiguousNames.has(name)),
tools,
};
}
/** @param {string} name @param {Record<string, unknown>} action @param {ApiAnswerField[]} fields @param {any} grounding */
export function finishApiAnswer(name, action, fields, grounding) {
const byRef = new Map(fields.map((field) => [field.ref, field]));
if (name === "select_api_field") {
const field = byRef.get(String(action.field));
if (!field) throw new Error("The AI selected an unknown API field");
const label = typeof action.label === "string" && action.label.trim()
? action.label.trim().replaceAll("_", " ")
: field.name.replaceAll(".", " · ").replaceAll("_", " ");
const formatted = formattedValue(field.value);
return renderApiAnswer(
`**${label}**: ${formatted}${
typeof field.value === "number" ? displayedUnit(field.type) : ""
}`,
grounding.operation,
);
}
if (name === "answer_api_text") {
const text = typeof action.text === "string" ? action.text.trim() : "";
if (!text) throw new Error("The AI returned an empty API answer");
return renderApiAnswer(text, grounding.operation);
}
if (name === "continue_api_calculation") {
const previousNames = Array.isArray(grounding.previousFields)
? grounding.previousFields
: [];
const previous = previousNames.length === 1
? fields.find((field) => field.name === previousNames[0])
: undefined;
const operand = byRef.get(String(action.operand));
if (
!previous ||
!operand ||
typeof previous.value !== "number" ||
typeof operand.value !== "number" ||
previous.ref === operand.ref
) {
throw new Error("The AI returned an invalid API follow-up calculation");
}
if (
typeof action.operator !== "string" ||
!["add", "subtract", "multiply", "divide"].includes(action.operator)
) {
throw new Error("The AI returned an invalid API calculation operator");
}
if (action.operator === "divide" && operand.value === 0) {
throw new Error("Cannot divide by zero");
}
const value = action.operator === "add"
? previous.value + operand.value
: action.operator === "subtract"
? previous.value - operand.value
: action.operator === "multiply"
? previous.value * operand.value
: previous.value / operand.value;
const previousDimension = dimension(previous.type);
const operandDimension = dimension(operand.type);
const unit = action.operator === "divide"
? previousDimension === operandDimension
? ""
: ` ${previousDimension}/${operandDimension}`
: previousDimension === operandDimension
? displayedUnit(previous.type)
: "";
const label = typeof action.label === "string" && action.label.trim()
? action.label.trim().replaceAll("_", " ")
: "result";
const formatted = new Intl.NumberFormat("en-US", {
maximumFractionDigits: 8,
}).format(value);
return renderApiAnswer(`**${label}**: ${formatted}${unit}`, grounding.operation);
}
if (name !== "calculate_api_fields") {
throw new Error("The AI returned an invalid API calculation");
}
if (
typeof action.operator === "string" &&
["add", "subtract", "multiply", "divide"].includes(action.operator) &&
(
(
typeof action.left === "string" &&
typeof action.right === "string"
) ||
(
Array.isArray(action.operands) &&
action.operands.length >= 2
)
)
) {
const refs = typeof action.left === "string" &&
typeof action.right === "string"
? [action.left, action.right]
: /** @type {unknown[]} */ (action.operands);
const selected = refs.map((ref) => byRef.get(String(ref)));
if (selected.some((field) => !field)) throw new Error("Unknown calculation field");
if (selected.some((field) => typeof field?.value !== "number")) {
throw new Error("A calculation requires numeric fields");
}
const numeric = /** @type {(ApiAnswerField & { value: number })[]} */ (selected);
const [first, ...rest] = numeric;
const types = new Set(numeric.map(({ type }) => dimension(type)));
if (
(action.operator === "add" || action.operator === "subtract") &&
types.size > 1
) {
const choices = numeric
.map((field) =>
`**${field.name.replaceAll(".", " · ").replaceAll("_", " ")}** (${field.type})`
)
.join(", ");
return renderApiAnswer(
`Those fields use different units: ${choices}. Which one do you want?`,
grounding.operation,
);
}
const value = rest.reduce((result, field) => {
if (action.operator === "add") return result + field.value;
if (action.operator === "subtract") return result - field.value;
if (action.operator === "multiply") return result * field.value;
if (field.value === 0) throw new Error("Cannot divide by zero");
return result / field.value;
}, first.value);
const unit = action.operator === "divide" && numeric.length === 2
? dimension(first.type) === dimension(numeric[1].type)
? ""
: ` ${dimension(first.type)}/${dimension(numeric[1].type)}`
: types.size === 1
? displayedUnit(first.type)
: "";
const label = typeof action.label === "string" && action.label.trim()
? action.label.trim().replaceAll("_", " ")
: "result";
const formatted = new Intl.NumberFormat("en-US", { maximumFractionDigits: 8 }).format(value);
return renderApiAnswer(`**${label}**: ${formatted}${unit}`, grounding.operation);
}
throw new Error("The AI returned an invalid API calculation");
}