mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-08-10 17:43:04 -07:00
921 lines
27 KiB
JavaScript
921 lines
27 KiB
JavaScript
import { AskRefs } from "../refs.js";
|
|
import {
|
|
ROUTE_INSTRUCTION,
|
|
actionInstruction,
|
|
actionTool,
|
|
apiArgumentTool,
|
|
availableActions,
|
|
capabilityMetrics,
|
|
directAction,
|
|
generalCapabilities,
|
|
requestedChartStyles,
|
|
routeTools,
|
|
} from "./capabilities.js";
|
|
import { loadSessionContext } from "./context.js";
|
|
import { collectEvidence, collectSourceOptions } from "./evidence.js";
|
|
import { CapabilityExecutor } from "./executor.js";
|
|
import { normalize, relevance, tokenAffinity } from "../text.js";
|
|
import { recordArguments, selectApiRecord } from "../api/records.js";
|
|
import {
|
|
explicitArguments,
|
|
hasRequiredArguments,
|
|
reusableArguments,
|
|
validatedArguments,
|
|
} from "../api/routing.js";
|
|
|
|
const RETRIEVAL_TERMS = new Set([
|
|
"display",
|
|
"fetch",
|
|
"get",
|
|
"inspect",
|
|
"lookup",
|
|
"read",
|
|
"retrieve",
|
|
"show",
|
|
]);
|
|
|
|
/** @param {unknown[]} values */
|
|
function schemaTokens(values) {
|
|
return new Set(
|
|
normalize(values.filter(Boolean).join(" "))
|
|
.split(" ")
|
|
.filter((token) => token.length >= 3),
|
|
);
|
|
}
|
|
|
|
/** @param {Set<string>} query @param {Set<string>} document */
|
|
function overlapCount(query, document) {
|
|
return [...query].filter((token) => document.has(token)).length;
|
|
}
|
|
|
|
/** @param {Set<string>} query @param {Set<string>} document */
|
|
function semanticOverlapCount(query, document) {
|
|
return [...query].filter((token) =>
|
|
[...document].some((candidate) =>
|
|
tokenAffinity(token, candidate) >= 0.75
|
|
)
|
|
).length;
|
|
}
|
|
|
|
/** @param {string} question */
|
|
function explicitPosition(question) {
|
|
const date = question.match(
|
|
/(?<![A-Za-z0-9])\d{4}-\d{2}-\d{2}(?![A-Za-z0-9])/,
|
|
)?.[0];
|
|
if (date) return date;
|
|
const values = [...new Set(
|
|
question.match(
|
|
/(?<![A-Za-z0-9])[-+]?\d[\d,]*(?:\.\d+)?(?![A-Za-z0-9])/g,
|
|
) ?? [],
|
|
)];
|
|
return values.length === 1 ? values[0].replaceAll(",", "") : undefined;
|
|
}
|
|
|
|
/** @param {any} evidence @param {string} question @param {boolean} [requiresExample] */
|
|
function preferredGuide(evidence, question, requiresExample = false) {
|
|
return evidence.guideOptions
|
|
.filter(({ guide }) => !requiresExample || guide.example)
|
|
.map((option) => ({
|
|
option,
|
|
relevance: Math.max(
|
|
relevance(question, option.guide.title),
|
|
relevance(evidence.context.knowledge?.title, option.guide.title),
|
|
),
|
|
}))
|
|
.sort((left, right) => right.relevance - left.relevance)[0]?.option;
|
|
}
|
|
|
|
export class AskToolSession {
|
|
/** @param {import("../source/index.js").AskSource} source */
|
|
constructor(source) {
|
|
this.source = source;
|
|
}
|
|
|
|
/** @param {string} question @param {import("../../storage.js").StoredMessage[]} history @param {(status: string) => void} onStatus */
|
|
async begin(question, history, onStatus) {
|
|
const refs = new AskRefs();
|
|
this.refs = refs;
|
|
const context = await loadSessionContext(
|
|
history,
|
|
() => onStatus("Indexing context…"),
|
|
);
|
|
if (context.api) {
|
|
const record = selectApiRecord(context.api.records, question);
|
|
if (record !== undefined) {
|
|
context.api.arguments = {
|
|
...context.api.arguments,
|
|
...recordArguments(record, context.api.operation.response.type),
|
|
};
|
|
}
|
|
}
|
|
onStatus("Searching tools…");
|
|
const evidence = await collectEvidence({
|
|
question,
|
|
context,
|
|
refs,
|
|
onStatus,
|
|
});
|
|
this.question = question;
|
|
this.evidence = evidence;
|
|
this.executor = new CapabilityExecutor({
|
|
question,
|
|
evidence,
|
|
refs,
|
|
source: this.source,
|
|
});
|
|
}
|
|
|
|
question = "";
|
|
/** @type {Awaited<ReturnType<typeof collectEvidence>> | undefined} */
|
|
evidence;
|
|
/** @type {CapabilityExecutor | undefined} */
|
|
executor;
|
|
/** @type {AskRefs | undefined} */
|
|
refs;
|
|
|
|
routeTools() {
|
|
if (!this.evidence) throw new Error("Tool session is not ready");
|
|
return routeTools(this.evidence);
|
|
}
|
|
|
|
/** @param {unknown} subject */
|
|
continuesKnowledge(subject) {
|
|
const title = this.evidence?.context.knowledge?.title;
|
|
if (typeof subject !== "string" || !title) return false;
|
|
return Math.max(
|
|
relevance(subject, title),
|
|
relevance(title, subject),
|
|
) >= 10;
|
|
}
|
|
|
|
/** @param {unknown} explicitSubject @param {unknown} topic */
|
|
continuesGeneral(explicitSubject, topic) {
|
|
if (!this.evidence?.context.knowledge) return false;
|
|
if (this.continuesKnowledge(topic)) return true;
|
|
if (typeof explicitSubject === "string") {
|
|
return !explicitSubject.trim() ||
|
|
this.continuesKnowledge(explicitSubject);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
routeMessages() {
|
|
if (!this.evidence) throw new Error("Tool session is not ready");
|
|
const { context, apiOptions } = this.evidence;
|
|
const messages = [
|
|
{
|
|
role: /** @type {const} */ ("system"),
|
|
content: ROUTE_INSTRUCTION,
|
|
},
|
|
];
|
|
if (context.knowledge?.description) {
|
|
messages.push({
|
|
role: /** @type {const} */ ("assistant"),
|
|
content: context.knowledge.description,
|
|
});
|
|
}
|
|
messages.push(
|
|
{
|
|
role: /** @type {const} */ ("user"),
|
|
content: JSON.stringify({
|
|
request: this.question,
|
|
context: {
|
|
activeCapability: context.chart
|
|
? "chart"
|
|
: context.api
|
|
? "api"
|
|
: context.metrics.length
|
|
? "metric"
|
|
: context.source.length
|
|
? "source"
|
|
: context.knowledge
|
|
? "general"
|
|
: undefined,
|
|
...(context.chart
|
|
? {
|
|
activeChart: {
|
|
title: context.chart.chart.title,
|
|
series: context.chart.chart.series.map(
|
|
(/** @type {any} */ { path, label }) => ({ path, label }),
|
|
),
|
|
},
|
|
}
|
|
: {}),
|
|
...(context.metrics.length
|
|
? {
|
|
metrics: context.metrics.map(
|
|
(/** @type {any} */ metric) => ({
|
|
name: metric.name,
|
|
path: metric.path,
|
|
}),
|
|
),
|
|
}
|
|
: {}),
|
|
...(context.recentMetrics.length
|
|
? {
|
|
recentMetrics: context.recentMetrics.map(
|
|
(/** @type {any} */ metric) => ({
|
|
name: metric.name,
|
|
path: metric.path,
|
|
}),
|
|
),
|
|
}
|
|
: {}),
|
|
...(context.api
|
|
? {
|
|
api: {
|
|
operation: context.api.operation.label,
|
|
arguments: context.api.arguments,
|
|
},
|
|
}
|
|
: {}),
|
|
...(context.source.length
|
|
? {
|
|
source: context.source.map(
|
|
(/** @type {any} */ source) => source.path,
|
|
),
|
|
sourceSubjects: context.knowledge?.subjects ?? [],
|
|
}
|
|
: {}),
|
|
...(context.knowledge
|
|
? {
|
|
concept: {
|
|
title: context.knowledge.title,
|
|
subjects: context.knowledge.subjects ?? [],
|
|
},
|
|
}
|
|
: {}),
|
|
},
|
|
apiMatches: apiOptions.map(({ label, operation }) => ({
|
|
label,
|
|
required: operation.parameters
|
|
.filter((/** @type {any} */ parameter) => parameter.required)
|
|
.map((/** @type {any} */ parameter) => parameter.description),
|
|
})),
|
|
}),
|
|
},
|
|
);
|
|
return messages;
|
|
}
|
|
|
|
directRoute() {
|
|
if (!this.evidence) return undefined;
|
|
if (this.evidence.variantMiss) {
|
|
const metric = this.evidence.context.metrics[0];
|
|
return {
|
|
action: "clarify",
|
|
call: {
|
|
name: "clarify",
|
|
arguments: {
|
|
question: `I could not find a matching variant of ${
|
|
normalize(metric?.name ?? "the active metric").replaceAll("_", " ")
|
|
}. Which available cohort or variant should I use?`,
|
|
},
|
|
},
|
|
};
|
|
}
|
|
let action = directAction(this.evidence, this.question);
|
|
if (
|
|
action === "explain_metric_calculation" &&
|
|
!this.evidence.metricOptions.some(
|
|
(/** @type {any} */ { origin }) => origin === "mentioned",
|
|
) &&
|
|
this.evidence.apiOptions.some(({ operation }) =>
|
|
operation.parameters.some((parameter) => parameter.required) &&
|
|
hasRequiredArguments(
|
|
operation,
|
|
explicitArguments(operation, this.question),
|
|
)
|
|
)
|
|
) {
|
|
action = undefined;
|
|
}
|
|
if (action === "search_source") {
|
|
if (
|
|
this.evidence.context.source.length &&
|
|
!this.evidence.context.knowledge?.subjects?.length
|
|
) {
|
|
return undefined;
|
|
}
|
|
return {
|
|
action,
|
|
call: {
|
|
name: action,
|
|
arguments: { query: this.question },
|
|
},
|
|
};
|
|
}
|
|
if (action) {
|
|
return {
|
|
action,
|
|
call: this.directCall(action),
|
|
};
|
|
}
|
|
const at = explicitPosition(this.question);
|
|
const metricAt = at
|
|
? capabilityMetrics(this.evidence, "read_metric_at")
|
|
: [];
|
|
if (at && metricAt.length === 1) {
|
|
return {
|
|
action: "read_metric_at",
|
|
call: {
|
|
name: "read_metric_at",
|
|
arguments: {
|
|
refs: [metricAt[0].ref],
|
|
at,
|
|
},
|
|
},
|
|
};
|
|
}
|
|
const supplied = this.evidence.apiOptions
|
|
.map(({ ref, operation }) => ({
|
|
ref,
|
|
operation,
|
|
arguments: explicitArguments(operation, this.question),
|
|
}))
|
|
.filter(({ operation, arguments: arguments_ }) =>
|
|
hasRequiredArguments(operation, arguments_) &&
|
|
(
|
|
operation.parameters.some((parameter) => parameter.required) ||
|
|
Number(operation.titleMatchedTerms ?? 0) >= 2
|
|
)
|
|
)
|
|
.sort(({ operation: left }, { operation: right }) =>
|
|
Number(right.titleMatchedTerms ?? 0) -
|
|
Number(left.titleMatchedTerms ?? 0) ||
|
|
right.response.fields.length - left.response.fields.length ||
|
|
Number(right.specificity ?? 0) - Number(left.specificity ?? 0) ||
|
|
Number(right.score ?? 0) - Number(left.score ?? 0)
|
|
);
|
|
const priorArguments = new Set(
|
|
Object.values(this.evidence.context.api?.arguments ?? {}).map(String),
|
|
);
|
|
const newResource = !this.evidence.context.api ||
|
|
supplied.some(({ arguments: arguments_ }) =>
|
|
Object.values(arguments_).some((value) =>
|
|
!priorArguments.has(String(value))
|
|
)
|
|
);
|
|
if (newResource && supplied[0]) {
|
|
return {
|
|
action: "call_api",
|
|
call: {
|
|
name: "call_api",
|
|
arguments: { ref: supplied[0].ref },
|
|
},
|
|
};
|
|
}
|
|
if (
|
|
this.evidence.context.metrics.length ||
|
|
this.evidence.context.chart
|
|
) {
|
|
return undefined;
|
|
}
|
|
|
|
const questionTokens = schemaTokens([this.question]);
|
|
if (
|
|
!this.evidence.context.api &&
|
|
!this.evidence.metricOptions.some(
|
|
(/** @type {any} */ { origin }) => origin === "mentioned",
|
|
) &&
|
|
[...questionTokens].some((token) => RETRIEVAL_TERMS.has(token))
|
|
) {
|
|
const records = this.evidence.apiOptions
|
|
.map(({ ref, operation }) => ({
|
|
ref,
|
|
operation,
|
|
score: overlapCount(
|
|
questionTokens,
|
|
schemaTokens([operation.label]),
|
|
),
|
|
}))
|
|
.filter(({ score }) => score > 0)
|
|
.sort((left, right) =>
|
|
right.score - left.score ||
|
|
right.operation.response.fields.length -
|
|
left.operation.response.fields.length
|
|
);
|
|
if (
|
|
records[0] &&
|
|
records[0].score > (records[1]?.score ?? 0)
|
|
) {
|
|
return {
|
|
action: "call_api",
|
|
call: {
|
|
name: "call_api",
|
|
arguments: { ref: records[0].ref },
|
|
},
|
|
};
|
|
}
|
|
}
|
|
|
|
const query = questionTokens;
|
|
const contextKey = this.evidence.context.api?.operation.key;
|
|
const contextOperation = this.evidence.context.api?.operation;
|
|
const currentFieldNames = schemaTokens(
|
|
contextOperation?.response.fields.map(
|
|
(/** @type {any} */ field) => field.name,
|
|
) ?? [],
|
|
);
|
|
const detail = contextOperation?.response.type.endsWith("[]") &&
|
|
semanticOverlapCount(query, currentFieldNames) === 0
|
|
? this.evidence.apiOptions
|
|
.filter(({ operation }) =>
|
|
operation.key !== contextKey &&
|
|
!operation.response.type.endsWith("[]") &&
|
|
operation.response.fields.length >
|
|
contextOperation.response.fields.length &&
|
|
Boolean(reusableArguments(operation, this.evidence.context.api))
|
|
)
|
|
.sort(({ operation: left }, { operation: right }) =>
|
|
Number(right.titleMatchedTerms ?? 0) -
|
|
Number(left.titleMatchedTerms ?? 0) ||
|
|
Number(right.specificity ?? 0) - Number(left.specificity ?? 0) ||
|
|
right.response.fields.length - left.response.fields.length ||
|
|
Number(right.score ?? 0) - Number(left.score ?? 0)
|
|
)[0]
|
|
: undefined;
|
|
if (detail) {
|
|
return {
|
|
action: "call_api",
|
|
call: {
|
|
name: "call_api",
|
|
arguments: { ref: detail.ref },
|
|
},
|
|
};
|
|
}
|
|
|
|
const apiMatches = [];
|
|
for (const { ref, operation } of this.evidence.apiOptions) {
|
|
const returned = schemaTokens(
|
|
operation.response.fields.flatMap((/** @type {any} */ field) => [
|
|
field.name,
|
|
field.description,
|
|
]),
|
|
);
|
|
const fieldMatches = semanticOverlapCount(query, returned);
|
|
const suppliedResource =
|
|
operation.parameters.some((parameter) => parameter.required) &&
|
|
hasRequiredArguments(
|
|
operation,
|
|
explicitArguments(operation, this.question),
|
|
);
|
|
const inheritedResource = reusableArguments(
|
|
operation,
|
|
this.evidence.context.api,
|
|
);
|
|
if (
|
|
fieldMatches > 0 &&
|
|
(suppliedResource || inheritedResource || operation.key === contextKey)
|
|
) {
|
|
apiMatches.push({
|
|
score: fieldMatches,
|
|
context: operation.key === contextKey,
|
|
action: "call_api",
|
|
call: {
|
|
name: "call_api",
|
|
arguments: { ref },
|
|
},
|
|
});
|
|
}
|
|
}
|
|
apiMatches.sort((left, right) => right.score - left.score);
|
|
const activeMatch = apiMatches.find(({ context }) => context);
|
|
if (activeMatch) return activeMatch;
|
|
if (
|
|
apiMatches[0] &&
|
|
apiMatches[0].score > (apiMatches[1]?.score ?? 0)
|
|
) {
|
|
return apiMatches[0];
|
|
}
|
|
|
|
if (supplied.length) {
|
|
return {
|
|
action: "call_api",
|
|
call: {
|
|
name: "call_api",
|
|
arguments: { ref: supplied[0].ref },
|
|
},
|
|
};
|
|
}
|
|
if (
|
|
this.evidence.context.knowledge &&
|
|
!this.evidence.context.knowledge.subjects?.length &&
|
|
(
|
|
this.evidence.context.source.length ||
|
|
this.evidence.guideOptions.some(
|
|
(/** @type {any} */ { origin }) => origin === "context",
|
|
)
|
|
)
|
|
) {
|
|
return { action: "answer_general" };
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
/** @param {string} action @param {(status: string) => void} onStatus */
|
|
async prepareAction(action, onStatus) {
|
|
if (
|
|
action !== "explain_metric_calculation" ||
|
|
!this.evidence ||
|
|
!this.refs
|
|
) return;
|
|
this.evidence.sourceOptions = await collectSourceOptions({
|
|
question: this.question,
|
|
evidence: this.evidence,
|
|
source: this.source,
|
|
refs: this.refs,
|
|
onStatus,
|
|
});
|
|
}
|
|
|
|
/** @param {string} action */
|
|
actionTool(action) {
|
|
const evidence = this.evidence;
|
|
if (!evidence) throw new Error("Tool session is not ready");
|
|
if (!availableActions(evidence).includes(action)) {
|
|
throw new Error(`Unavailable AI capability: ${action}`);
|
|
}
|
|
return actionTool(evidence, action);
|
|
}
|
|
|
|
/** @param {string} action */
|
|
directCall(action) {
|
|
const evidence = this.evidence;
|
|
if (!evidence) return undefined;
|
|
|
|
const metrics = capabilityMetrics(evidence, action);
|
|
if (action === "search_source") {
|
|
return {
|
|
name: action,
|
|
arguments: { query: this.question },
|
|
};
|
|
}
|
|
if (action === "describe_capabilities") {
|
|
return {
|
|
name: action,
|
|
arguments: { capabilities: generalCapabilities() },
|
|
};
|
|
}
|
|
if (action === "show_guide_example") {
|
|
const guide = preferredGuide(evidence, this.question, true);
|
|
return guide
|
|
? {
|
|
name: action,
|
|
arguments: { refs: [guide.ref] },
|
|
}
|
|
: undefined;
|
|
}
|
|
if (
|
|
action === "add_chart_series" ||
|
|
action === "remove_chart_series" ||
|
|
action === "replace_chart_series"
|
|
) {
|
|
const activePaths = new Set(
|
|
evidence.context.chart?.chart.series.map(
|
|
(/** @type {{ path: string }} */ { path }) => path,
|
|
) ?? [],
|
|
);
|
|
const mentioned = evidence.metricOptions.filter(
|
|
({ metric, origin }) =>
|
|
origin === "mentioned" &&
|
|
(
|
|
action === "add_chart_series"
|
|
? !activePaths.has(metric.path)
|
|
: activePaths.has(metric.path)
|
|
),
|
|
);
|
|
if (mentioned.length === 1) {
|
|
return {
|
|
name: action,
|
|
arguments: { refs: [mentioned[0].ref] },
|
|
};
|
|
}
|
|
}
|
|
if (
|
|
(
|
|
action === "read_latest_metric" ||
|
|
action === "select_metric_variant" ||
|
|
action === "build_metric_chart" ||
|
|
action === "add_chart_series" ||
|
|
action === "remove_chart_series" ||
|
|
action === "replace_chart_series"
|
|
) &&
|
|
metrics.length === 1
|
|
) {
|
|
return {
|
|
name: action,
|
|
arguments: { refs: [metrics[0].ref] },
|
|
};
|
|
}
|
|
|
|
if (action === "list_metric_cohorts_variants") {
|
|
const variants = evidence.metricOptions.filter(
|
|
({ origin }) => origin === "variant",
|
|
);
|
|
if (variants.length === 1) {
|
|
return {
|
|
name: action,
|
|
arguments: { refs: [variants[0].ref] },
|
|
};
|
|
}
|
|
const bases = evidence.metricOptions.filter(
|
|
({ origin }) => origin === "mentioned" || origin === "context",
|
|
);
|
|
if (!variants.length && bases.length === 1) {
|
|
return {
|
|
name: action,
|
|
arguments: { refs: [bases[0].ref] },
|
|
};
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
if (action === "set_chart_view_scale") {
|
|
const styles = requestedChartStyles(this.question);
|
|
if (styles.length) {
|
|
return {
|
|
name: action,
|
|
arguments: { styles },
|
|
};
|
|
}
|
|
}
|
|
|
|
if (action !== "explain_metric_calculation") return undefined;
|
|
const { context, metricOptions, sourceOptions, guideOptions } = evidence;
|
|
const contextual = context.metrics[0]
|
|
? metricOptions.find(({ metric }) =>
|
|
metric.path === context.metrics[0].path
|
|
)
|
|
: undefined;
|
|
const mentioned = metricOptions.find(({ origin }) => origin === "mentioned");
|
|
const currentGuide = guideOptions.some(
|
|
({ origin }) => origin === "current",
|
|
);
|
|
const metric = mentioned ?? (currentGuide ? undefined : contextual);
|
|
const grounding = sourceOptions[0] ?? guideOptions[0];
|
|
if (!grounding) return undefined;
|
|
return {
|
|
name: action,
|
|
arguments: {
|
|
refs: [grounding.ref],
|
|
...(metric ? { metrics: [metric.ref] } : {}),
|
|
},
|
|
};
|
|
}
|
|
|
|
contextualGeneralAction(continueContext) {
|
|
if (!continueContext || !this.evidence) return undefined;
|
|
if (
|
|
this.evidence.context.source.length &&
|
|
this.evidence.context.knowledge?.subjects?.length
|
|
) {
|
|
return "search_source";
|
|
}
|
|
return directAction(this.evidence, this.question, true);
|
|
}
|
|
|
|
/** @param {string} action @param {boolean} [continueContext] */
|
|
actionMessages(action, continueContext = true) {
|
|
const scopedEvidence = this.evidence;
|
|
if (!scopedEvidence) throw new Error("Tool session is not ready");
|
|
const { context, apiOptions, sourceOptions, guideOptions } =
|
|
scopedEvidence;
|
|
const actionMetrics = capabilityMetrics(scopedEvidence, action);
|
|
/** @type {Record<string, unknown>} */
|
|
const evidence = {
|
|
request: this.question,
|
|
};
|
|
if (
|
|
action === "set_chart_view_scale" ||
|
|
action === "add_chart_series" ||
|
|
action === "remove_chart_series" ||
|
|
action === "replace_chart_series"
|
|
) {
|
|
evidence.activeChart = context.chart?.chart;
|
|
}
|
|
if ([
|
|
"read_latest_metric",
|
|
"read_metric_at",
|
|
"read_metric_range",
|
|
"build_metric_chart",
|
|
"add_chart_series",
|
|
"remove_chart_series",
|
|
"replace_chart_series",
|
|
"list_metric_cohorts_variants",
|
|
"select_metric_variant",
|
|
"explain_metric_calculation",
|
|
].includes(action)) {
|
|
evidence.metrics = actionMetrics.map(
|
|
(/** @type {any} */ { ref, label, metric, origin }) => ({
|
|
ref,
|
|
label,
|
|
origin,
|
|
path: metric.path,
|
|
indexes: action.startsWith("read_metric") ||
|
|
action === "read_latest_metric"
|
|
? metric.indexes
|
|
: undefined,
|
|
unit: metric.suggestedUnit,
|
|
}),
|
|
);
|
|
}
|
|
if (
|
|
action === "explain_metric_calculation" ||
|
|
action === "search_source"
|
|
) {
|
|
evidence.source = sourceOptions.map(
|
|
(/** @type {any} */ { ref, source }) => ({
|
|
ref,
|
|
path: source.path,
|
|
startLine: source.startLine,
|
|
content: source.content.slice(0, 220),
|
|
}),
|
|
);
|
|
evidence.guides = guideOptions.map(({ ref, guide }) => ({
|
|
ref,
|
|
title: guide.title,
|
|
description: guide.description,
|
|
}));
|
|
if (context.source.length) {
|
|
evidence.previousSource = context.source.map(
|
|
(/** @type {any} */ source) => ({
|
|
path: source.path,
|
|
startLine: source.startLine,
|
|
content: source.content,
|
|
}),
|
|
);
|
|
}
|
|
if (context.knowledge) {
|
|
evidence.previousContext = context.knowledge;
|
|
}
|
|
}
|
|
if (action === "call_api") {
|
|
evidence.api = apiOptions.map(({ ref, operation }) => ({
|
|
ref,
|
|
operation: `${operation.method} ${operation.path}`,
|
|
summary: operation.summary || operation.label,
|
|
description: operation.description,
|
|
parameters: operation.parameters,
|
|
response: {
|
|
type: operation.response.type,
|
|
description: operation.response.description,
|
|
fields: operation.response.fields
|
|
.slice(0, 12)
|
|
.map((/** @type {any} */ { name, type, description }) => ({
|
|
name,
|
|
type,
|
|
description,
|
|
})),
|
|
},
|
|
}));
|
|
if (context.api) {
|
|
evidence.previousApi = {
|
|
operation: context.api.operation.label,
|
|
arguments: context.api.arguments,
|
|
};
|
|
}
|
|
}
|
|
if (
|
|
(action === "answer_general" || action === "find_chart_metrics") &&
|
|
context.knowledge
|
|
) {
|
|
evidence.context = context.knowledge;
|
|
}
|
|
if (action === "answer_general") {
|
|
if (!context.knowledge || !continueContext) {
|
|
return [
|
|
{
|
|
role: /** @type {const} */ ("system"),
|
|
content: actionInstruction(action),
|
|
},
|
|
{
|
|
role: /** @type {const} */ ("user"),
|
|
content: this.question,
|
|
},
|
|
];
|
|
}
|
|
const verified = Boolean(
|
|
context.source.length ||
|
|
guideOptions.some(({ origin }) => origin === "context"),
|
|
);
|
|
const verifiedGuideFacts = guideOptions
|
|
.filter(({ origin, guide }) => origin === "context" && guide.description)
|
|
.map(({ guide }) => guide.description);
|
|
return [
|
|
{
|
|
role: /** @type {const} */ ("system"),
|
|
content: `${actionInstruction(action)}
|
|
The immediately preceding assistant message is the active conversation context. Its topic is "${
|
|
context.knowledge.title
|
|
}" and it is ${verified ? "verified" : "not verified"}. ${
|
|
verified
|
|
? "Use only that answer and its direct logical consequences when the new request continues it."
|
|
: "Do not treat it as verified evidence."
|
|
}${
|
|
verifiedGuideFacts.length
|
|
? `\nVerified supporting facts:\n${
|
|
verifiedGuideFacts.map((fact) => `- ${fact}`).join("\n")
|
|
}`
|
|
: ""
|
|
}`,
|
|
},
|
|
{
|
|
role: /** @type {const} */ ("assistant"),
|
|
content: context.knowledge.description,
|
|
},
|
|
{
|
|
role: /** @type {const} */ ("user"),
|
|
content: this.question,
|
|
},
|
|
];
|
|
}
|
|
return [
|
|
{
|
|
role: /** @type {const} */ ("system"),
|
|
content: actionInstruction(action),
|
|
},
|
|
{
|
|
role: /** @type {const} */ ("user"),
|
|
content: JSON.stringify(evidence),
|
|
},
|
|
];
|
|
}
|
|
|
|
/** @param {string} ref */
|
|
apiArgumentTool(ref) {
|
|
if (!this.refs) throw new Error("Tool session is not ready");
|
|
return apiArgumentTool(this.refs.get(ref, "api"));
|
|
}
|
|
|
|
/** @param {string} ref */
|
|
apiArgumentMessages(ref) {
|
|
if (!this.refs) throw new Error("Tool session is not ready");
|
|
const operation = this.refs.get(ref, "api");
|
|
return [
|
|
{
|
|
role: /** @type {const} */ ("system"),
|
|
content: "Call provide_api_arguments exactly once. Copy only parameter values explicitly supplied by the newest request or verified API context. Omit everything else.",
|
|
},
|
|
{
|
|
role: /** @type {const} */ ("user"),
|
|
content: JSON.stringify({
|
|
request: this.question,
|
|
operation: {
|
|
method: operation.method,
|
|
path: operation.path,
|
|
parameters: operation.parameters,
|
|
},
|
|
...(this.evidence?.context.api
|
|
? {
|
|
previousArguments: this.evidence.context.api.arguments,
|
|
}
|
|
: {}),
|
|
}),
|
|
},
|
|
];
|
|
}
|
|
|
|
/** @param {string} ref */
|
|
apiArguments(ref) {
|
|
if (!this.refs) throw new Error("Tool session is not ready");
|
|
const operation = this.refs.get(ref, "api");
|
|
return {
|
|
...(reusableArguments(operation, this.evidence?.context.api) ?? {}),
|
|
...explicitArguments(operation, this.question),
|
|
};
|
|
}
|
|
|
|
/** @param {string} ref @param {Record<string, unknown>} arguments_ */
|
|
hasApiArguments(ref, arguments_) {
|
|
if (!this.refs) throw new Error("Tool session is not ready");
|
|
return hasRequiredArguments(this.refs.get(ref, "api"), arguments_);
|
|
}
|
|
|
|
/** @param {string} ref @param {Record<string, unknown>} arguments_ */
|
|
validateApiArguments(ref, arguments_) {
|
|
if (!this.refs) throw new Error("Tool session is not ready");
|
|
return validatedArguments(
|
|
this.refs.get(ref, "api"),
|
|
arguments_,
|
|
this.question,
|
|
this.evidence?.context.api,
|
|
);
|
|
}
|
|
|
|
/** @param {string} ref @param {Record<string, unknown>} arguments_ */
|
|
missingApiArguments(ref, arguments_) {
|
|
if (!this.refs) throw new Error("Tool session is not ready");
|
|
return this.refs.get(ref, "api").parameters
|
|
.filter((/** @type {any} */ parameter) =>
|
|
parameter.required && !Object.hasOwn(arguments_, parameter.name)
|
|
);
|
|
}
|
|
|
|
/** @param {{ name: string, arguments: Record<string, unknown> }} call @param {(status: string) => void} onStatus @param {AbortSignal} signal */
|
|
execute(call, onStatus, signal) {
|
|
if (!this.executor) throw new Error("Tool session is not ready");
|
|
return this.executor.execute(call, onStatus, signal);
|
|
}
|
|
}
|