import { executeApi } from "../api/execute.js"; import { reusableArguments } from "../api/routing.js"; import { createChartArtifact } from "../chart.js"; import { resolveChartUnit } from "../chart/units.js"; import { readMetric } from "../data.js"; import { metricVariants, searchMetrics } from "../metrics/index.js"; import { renderData } from "../render.js"; import { normalize } from "../text.js"; import { schemaSourceQueries } from "./evidence.js"; const CHART_VIEWS = new Set(["line", "area", "stacked", "bar", "dots"]); const CHART_SCALES = new Set(["linear", "log"]); /** @param {unknown} value @param {string} name */ function requiredString(value, name) { if (typeof value !== "string" || !value.trim()) { throw new Error(`The AI did not provide ${name}`); } return value.trim(); } /** @param {unknown} value */ function uniqueRefs(value) { if ( !Array.isArray(value) || !value.length || value.some((ref) => typeof ref !== "string") ) { throw new Error("The AI did not select verified evidence"); } return [...new Set(value)]; } /** @param {string} value */ function label(value) { return normalize(value); } /** @param {string | undefined} value */ function inlineCode(value) { if (!value) return []; return value .split("`") .filter((_, index) => index % 2 === 1) .map((term) => term.trim()) .filter(Boolean); } /** @param {string | undefined} value */ function sourceSubject(value) { return inlineCode(value).find((term) => !term.includes("/") && !term.includes("\\") ); } /** @param {string} content @param {string} field */ function computesField(content, field) { return content.split("\n").some((line) => { const normalized = normalize(line); return normalized.includes(field) && ["+=", "-=", "*=", "/=", " + ", " - ", " * ", " / "] .some((operator) => line.includes(operator)); }); } /** @param {any} result @param {string} query */ function rankedSchemaMatches(result, query) { const parts = query.trim().split(/\s+/); const field = normalize(parts.at(-1) ?? ""); const owner = normalize(parts.slice(0, -1).join(" ")); const terms = normalize(query).split(" ").filter(Boolean); return [...result.matches].sort((left, right) => { const leftContent = normalize(left.content); const rightContent = normalize(right.content); const leftOwner = owner && leftContent.includes(owner) ? 1 : 0; const rightOwner = owner && rightContent.includes(owner) ? 1 : 0; const leftComputes = leftOwner && computesField(left.content, field) ? 1 : 0; const rightComputes = rightOwner && computesField(right.content, field) ? 1 : 0; const leftPath = terms.filter((term) => normalize(left.path).split(" ").includes(term) ).length; const rightPath = terms.filter((term) => normalize(right.path).split(" ").includes(term) ).length; return rightComputes - leftComputes || rightOwner - leftOwner || rightPath - leftPath || Number(right.score ?? 0) - Number(left.score ?? 0); }); } export class CapabilityExecutor { /** * @param {Object} options * @param {string} options.question * @param {any} options.evidence * @param {import("../refs.js").AskRefs} options.refs * @param {import("../source/index.js").AskSource} options.source */ constructor({ question, evidence, refs, source }) { this.question = question; this.evidence = evidence; this.refs = refs; this.source = source; } /** @param {unknown} value @param {"metric" | "source" | "guide"} kind */ selected(value, kind) { return uniqueRefs(value).map((ref) => ({ ref, value: this.refs.get(ref, kind), })); } /** @param {Record} arguments_ */ answerGeneral(arguments_) { const answer = requiredString(arguments_.answer, "an answer"); return { done: true, output: answer, knowledgeContext: { title: this.question.slice(0, 160), description: answer, }, }; } /** @param {Record} arguments_ */ clarify(arguments_) { return { done: true, output: requiredString(arguments_.question, "a clarification question"), }; } /** @param {Record} arguments_ */ describeCapabilities(arguments_) { const capabilities = Array.isArray(arguments_.capabilities) ? arguments_.capabilities.filter((value) => typeof value === "string") : []; if (!capabilities.length) { throw new Error("The assistant capabilities are unavailable"); } return { done: true, output: `I can:\n\n${capabilities.map((value) => `- ${value}`).join("\n")}`, }; } /** @param {Record} arguments_ */ async explain(arguments_) { const selected = [ ...uniqueRefs(arguments_.refs), ...(Array.isArray(arguments_.metrics) ? uniqueRefs(arguments_.metrics) : []), ]; const sources = []; const metrics = []; const guides = []; for (const ref of selected) { const kind = this.refs.kind(ref); if (kind === "source") sources.push(this.refs.get(ref, "source")); else if (kind === "metric") metrics.push(this.refs.get(ref, "metric")); else if (kind === "guide") guides.push(this.refs.get(ref, "guide")); else throw new Error(`${ref} cannot ground an explanation`); } return { done: true, metricPaths: metrics.map((metric) => metric.path), sourceContext: sources, grounding: { question: this.question, metrics: metrics.map((metric) => ({ name: label(metric.name), path: metric.path, unit: metric.suggestedUnit, })), facts: guides.map((guide) => guide.description).filter(Boolean), excerpts: sources, }, }; } /** @param {Record} arguments_ @param {(status: string) => void} onStatus */ async searchSource(arguments_, onStatus) { const query = requiredString(arguments_.query, "a source search query"); const schemaQueries = schemaSourceQueries( this.question, this.evidence.apiCandidates ?? [], ); const subject = sourceSubject( this.evidence.context.knowledge?.description, ); const context = this.evidence.context.source; const subjectContext = subject ? context.filter((/** @type {{ content: string }} */ source) => source.content.includes(subject) ) : []; const scopedContext = subjectContext.length ? subjectContext : context; const paths = [...new Set(scopedContext.map( (/** @type {{ path: string }} */ { path }) => path, ))].slice(0, 2); const contextualQuery = subject ? `${query} ${subject}` : query; const searches = [ { query, path: undefined, focus: undefined }, ...schemaQueries.map((schemaQuery) => ({ query: schemaQuery, path: undefined, focus: /** @type {const} */ ("implementation"), })), ...(contextualQuery === query ? [] : [{ query: contextualQuery, path: undefined, focus: undefined }]), ...(subject ? [{ query: subject, path: undefined, focus: undefined }] : []), ...paths.map((path) => ({ query: contextualQuery, path, focus: undefined, })), ]; onStatus("Searching source…"); const results = await Promise.all( searches.map(({ query: scopedQuery, path, focus }) => this.source.search( scopedQuery, path, focus, ({ loaded, total }) => onStatus(`Indexing source · ${loaded} / ${total}`), ) ), ); const scopedResults = results.filter((_, index) => searches[index].path); const schemaResults = results.slice(1, 1 + schemaQueries.length); const contextualIndex = 1 + schemaQueries.length; const contextualResult = contextualQuery === query ? undefined : results[contextualIndex]; const subjectResult = subject ? results[ contextualIndex + (contextualQuery === query ? 0 : 1) ] : undefined; const rawResult = results[0]; const seeded = this.evidence.sourceOptions.map( (/** @type {{ source: any }} */ { source }) => source, ); const excerpts = [...new Map([ ...(paths.length ? [] : seeded), ...schemaResults.flatMap((result, index) => rankedSchemaMatches(result, schemaQueries[index]).slice(0, 2) .map((/** @type {any} */ match) => ({ ...match, revision: result.revision, })) ), ...scopedResults.flatMap((result) => result.matches.slice(0, 1).map((/** @type {any} */ match) => ({ ...match, revision: result.revision, })) ), ...(subjectResult?.matches.slice(0, 3).map( (/** @type {any} */ match) => ({ ...match, revision: subjectResult.revision, }), ) ?? []), ...(contextualResult?.matches.slice(0, 3).map( (/** @type {any} */ match) => ({ ...match, revision: contextualResult.revision, }), ) ?? []), ...(paths.length ? seeded : []), ...rawResult.matches.slice(0, 3).map((/** @type {any} */ match) => ({ ...match, revision: rawResult.revision, })), ].map((excerpt) => [ `${excerpt.revision}:${excerpt.path}:${excerpt.startLine}`, excerpt, ])).values()].slice(0, 3); if (!excerpts.length) { return { done: true, output: "I could not find enough verified source evidence to answer that.", }; } return { done: true, sourceContext: excerpts, grounding: { question: this.question, metrics: [], facts: [], excerpts, }, }; } /** @param {Record} arguments_ */ async listVariants(arguments_) { const [{ value: metric }] = this.selected(arguments_.refs, "metric"); if (metric.origin === "variant") { return this.selectVariant(arguments_); } const variants = await metricVariants(metric, this.question); if (!variants) { return { done: true, output: `I found one **${label(metric.name)}** series and no cohort variants.`, metricPaths: [metric.path], }; } const groups = variants.groups .map((group) => group.examples.length === 1 && group.examples[0] === group.family ? label(group.family) : `${label(group.family)}: ${group.examples.map(label).join(", ")}` ) .join("; "); return { done: true, output: `**${label(metric.name)}** has ${variants.totalSeries} available series. Variant groups: ${groups}.`, metricPaths: [metric.path], }; } /** @param {Record} arguments_ @param {(status: string) => void} onStatus */ async findChartMetrics(arguments_, onStatus) { const query = requiredString(arguments_.query, "a metric search query"); const context = this.evidence.context.knowledge; const queries = [...new Set([ query, this.question, context?.title, context?.description, ].filter((value) => typeof value === "string" && value.trim()))]; onStatus("Searching metrics…"); const metrics = (await searchMetrics(queries, 12)) .filter(({ matchedTerms }) => Number(matchedTerms ?? 0) > 0) .filter((metric, index, values) => values.findIndex(({ name }) => name === metric.name) === index ) .slice(0, 6); if (!metrics.length) { return { done: true, output: "I could not find a chart metric matching that description.", }; } return { done: true, output: `I found these chart metrics:\n\n${ metrics.map((metric) => `- **${label(metric.name)}**${ metric.suggestedUnit ? ` · ${metric.suggestedUnit}` : "" }` ).join("\n") }`, metricPaths: metrics.map(({ path }) => path), }; } /** @param {Record} arguments_ */ selectVariant(arguments_) { const [{ value: metric }] = this.selected(arguments_.refs, "metric"); return { done: true, output: `Selected **${metric.label ?? label(metric.name)}**.`, metricPaths: [metric.path], }; } /** @param {Record} arguments_ @param {"latest" | "at" | "range"} mode */ async read(arguments_, mode) { const selected = this.selected(arguments_.refs, "metric"); if (mode === "at") requiredString(arguments_.at, "a block height or date"); const results = await Promise.all( selected.map(({ value: metric }) => readMetric(metric, { ...arguments_, mode }) ), ); return { done: true, output: renderData(results), metricPaths: selected.map(({ value }) => value.path), }; } /** @param {any[]} chosen @param {any} [existing] @param {"add" | "remove" | "replace"} [operation] */ chart(chosen, existing, operation) { const prior = /** @type {any[]} */ (existing?.chart.series ?? []); const added = chosen.map((metric) => ({ path: metric.path, label: metric.label ?? label(metric.name), })); const selectedPaths = new Set(added.map(({ path }) => path)); const series = !existing || operation === "replace" ? added : operation === "remove" ? prior.filter(({ path }) => !selectedPaths.has(path)) : [ ...prior, ...added.filter(({ path }) => !prior.some((item) => item.path === path) ), ]; if (!series.length) throw new Error("A chart needs at least one series"); const { unit, conflicts } = resolveChartUnit( chosen, existing?.chart.unit, operation ?? "replace", ); if (conflicts.length) { return { done: true, output: `Those metrics use different units (${conflicts.map((value) => value === "number" ? "**unitless**" : `**${value.toUpperCase()}**`).join(" and ")}), so they need separate charts. Which one should I chart?`, artifacts: [], }; } const artifact = createChartArtifact({ title: series.map((item) => item.label).join(" and "), unit, view: existing?.chart.view, scale: existing?.chart.scale, series, }); const known = new Map( [...this.evidence.context.metrics, ...chosen] .map((metric) => [metric.path, metric]), ); return { done: true, output: `${existing ? "Updated" : "Built"} **${artifact.chart.title}** with ${artifact.chart.series.map((item) => item.label).join(", ")}.`, artifacts: [artifact], metricPaths: [...new Set([ ...artifact.chart.series.map(({ path }) => path), ...(operation === "remove" || operation === "replace" ? chosen.map(({ path }) => path) : []), ])].filter((path) => known.has(path)), }; } /** @param {Record} arguments_ */ buildChart(arguments_) { const selected = this.selected(arguments_.refs, "metric") .map(({ value }) => value); const chosen = [...new Map([ ...(arguments_.includeContext === true ? this.evidence.context.metrics : []), ...selected, ].map((metric) => [metric.path, metric])).values()]; return this.chart(chosen); } /** * @param {Record} arguments_ * @param {"add" | "remove" | "replace"} operation */ editChart(arguments_, operation) { const active = this.evidence.context.chart; if (!active) throw new Error("There is no active chart to update"); const chosen = this.selected(arguments_.refs, "metric") .map(({ value }) => value); return this.chart(chosen, active, operation); } /** @param {Record} arguments_ */ styleChart(arguments_) { const active = this.evidence.context.chart; if (!active) throw new Error("There is no active chart to update"); const styles = Array.isArray(arguments_.styles) ? arguments_.styles.filter((style) => typeof style === "string") : []; const view = styles.find((style) => CHART_VIEWS.has(style)); const scale = styles.find((style) => CHART_SCALES.has(style)); if (!view && !scale) throw new Error("The AI did not select a chart style"); const artifact = createChartArtifact({ title: active.chart.title, unit: active.chart.unit, view: view ?? active.chart.view, scale: scale ?? active.chart.scale, series: active.chart.series, }); return { done: true, output: `Updated **${artifact.chart.title}** to use ${[ view ? `${view} view` : "", scale ? `${scale} scale` : "", ].filter(Boolean).join(" and ")}.`, artifacts: [artifact], metricPaths: artifact.chart.series.map(({ path }) => path), }; } /** * @param {Record} arguments_ * @param {(status: string) => void} onStatus * @param {AbortSignal} signal */ async callApi(arguments_, onStatus, signal) { const ref = requiredString(arguments_.ref, "an API reference"); const operation = this.refs.get(ref, "api"); const supplied = arguments_.arguments && typeof arguments_.arguments === "object" && !Array.isArray(arguments_.arguments) ? /** @type {Record} */ (arguments_.arguments) : {}; const reused = reusableArguments(operation, this.evidence.context.api); onStatus("Reading API…"); const result = await executeApi( operation, { ...(reused ?? {}), ...supplied }, signal, ); return { done: true, apiContext: { key: operation.key, arguments: result.arguments, }, apiGrounding: { question: this.question, previousFields: this.evidence.context.api?.fields ?? [], ...result, }, }; } /** * @param {{ name: string, arguments: Record }} call * @param {(status: string) => void} onStatus * @param {AbortSignal} signal */ async execute(call, onStatus, signal) { signal.throwIfAborted(); if (call.name === "answer_general") { return this.answerGeneral(call.arguments); } if (call.name === "describe_capabilities") { return this.describeCapabilities(call.arguments); } if (call.name === "clarify") return this.clarify(call.arguments); if (call.name === "explain_metric_calculation") { return await this.explain(call.arguments); } if (call.name === "search_source") { return await this.searchSource(call.arguments, onStatus); } if (call.name === "list_metric_cohorts_variants") { return await this.listVariants(call.arguments); } if (call.name === "find_chart_metrics") { return await this.findChartMetrics(call.arguments, onStatus); } if (call.name === "select_metric_variant") { return this.selectVariant(call.arguments); } if (call.name === "read_latest_metric") { onStatus("Reading data…"); return await this.read(call.arguments, "latest"); } if (call.name === "read_metric_at") { onStatus("Reading data…"); return await this.read(call.arguments, "at"); } if (call.name === "read_metric_range") { onStatus("Reading data…"); return await this.read(call.arguments, "range"); } if (call.name === "build_metric_chart") { onStatus("Building chart…"); return this.buildChart(call.arguments); } if (call.name === "add_chart_series") { onStatus("Updating chart…"); return this.editChart(call.arguments, "add"); } if (call.name === "remove_chart_series") { onStatus("Updating chart…"); return this.editChart(call.arguments, "remove"); } if (call.name === "replace_chart_series") { onStatus("Updating chart…"); return this.editChart(call.arguments, "replace"); } if (call.name === "set_chart_view_scale") { onStatus("Updating chart…"); return this.styleChart(call.arguments); } if (call.name === "call_api") { return await this.callApi(call.arguments, onStatus, signal); } throw new Error(`Unsupported AI capability: ${call.name}`); } }