mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-07-23 08:58:11 -07:00
next: ai part 4
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
import { normalize } from "../text.js";
|
||||
|
||||
const CHART_REQUEST = /\b(?:chart|graph|plot)\b/;
|
||||
const ADD_REQUEST = /\b(?:add|include|overlay)\b/;
|
||||
const REMOVE_REQUEST = /\b(?:remove|drop)\b/;
|
||||
|
||||
/**
|
||||
* @param {string} request
|
||||
* @param {boolean} hasActiveChart
|
||||
*/
|
||||
export function directChartCommand(request, hasActiveChart) {
|
||||
const text = normalize(request);
|
||||
|
||||
if (hasActiveChart && ADD_REQUEST.test(text)) {
|
||||
return { kind: /** @type {const} */ ("edit"), operation: /** @type {const} */ ("add") };
|
||||
}
|
||||
if (hasActiveChart && REMOVE_REQUEST.test(text)) {
|
||||
return { kind: /** @type {const} */ ("edit"), operation: /** @type {const} */ ("remove") };
|
||||
}
|
||||
if (CHART_REQUEST.test(text) && !ADD_REQUEST.test(text) && !REMOVE_REQUEST.test(text)) {
|
||||
return { kind: /** @type {const} */ ("build"), operation: /** @type {const} */ ("add") };
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@@ -64,7 +64,7 @@ class MetricIndex {
|
||||
/** @type {Map<string, { resolve: (value: any) => void, reject: (error: Error) => void, onProgress?: () => void }>} */
|
||||
#pending = new Map();
|
||||
|
||||
/** @param {"search" | "categories" | "byName" | "variants"} type @param {Record<string, unknown>} data @param {(() => void) | undefined} [onProgress] */
|
||||
/** @param {"search" | "mentions" | "categories" | "byName" | "variants"} type @param {Record<string, unknown>} data @param {(() => void) | undefined} [onProgress] */
|
||||
request(type, data, onProgress) {
|
||||
this.#ensureWorker();
|
||||
const id = crypto.randomUUID();
|
||||
@@ -123,6 +123,11 @@ export function searchMetrics(queries, limit = 16, prefixes = [], onProgress) {
|
||||
return index.request("search", { queries, limit, prefixes }, onProgress);
|
||||
}
|
||||
|
||||
/** @param {string} query @param {(() => void) | undefined} [onProgress] @returns {Promise<CatalogMetric[]>} */
|
||||
export function mentionedMetrics(query, onProgress) {
|
||||
return index.request("mentions", { query }, onProgress);
|
||||
}
|
||||
|
||||
/** @returns {Promise<{ path: string, label: string, count: number, examples: string[] }[]>} */
|
||||
export function metricCategories() {
|
||||
return index.request("categories", {});
|
||||
|
||||
@@ -56,6 +56,8 @@ function buildState() {
|
||||
const items = [];
|
||||
/** @type {Map<string, CatalogMetric>} */
|
||||
const byName = new Map();
|
||||
/** @type {Map<string, CatalogMetric[]>} */
|
||||
const bySearchableName = new Map();
|
||||
/** @type {Map<string, CatalogMetric>} */
|
||||
const byDocument = new Map();
|
||||
const seen = new WeakSet();
|
||||
@@ -76,6 +78,10 @@ function buildState() {
|
||||
const metric = { path, name, endpoint, document };
|
||||
items.push(metric);
|
||||
if (!byName.has(name)) byName.set(name, metric);
|
||||
const nameKey = searchable(name);
|
||||
const named = bySearchableName.get(nameKey) ?? [];
|
||||
named.push(metric);
|
||||
bySearchableName.set(nameKey, named);
|
||||
byDocument.set(document, metric);
|
||||
} else {
|
||||
for (const [key, child] of Object.entries(value)) {
|
||||
@@ -88,7 +94,7 @@ function buildState() {
|
||||
const matcher = new QuickMatch(items.map(({ document }) => document), config);
|
||||
/** @type {Map<string, { matcher: QuickMatch, config: QuickMatchConfig }>} */
|
||||
const scoped = new Map();
|
||||
return { items, byName, byDocument, matcher, config, scoped };
|
||||
return { items, byName, bySearchableName, byDocument, matcher, config, scoped };
|
||||
}
|
||||
|
||||
/** @type {Promise<ReturnType<typeof buildState>> | undefined} */
|
||||
@@ -199,6 +205,31 @@ function search(index, queries, limit, prefixes) {
|
||||
return output;
|
||||
}
|
||||
|
||||
/** @param {ReturnType<typeof buildState>} index @param {string} query */
|
||||
function mentions(index, query) {
|
||||
const words = searchable(query).match(/[a-z0-9]+/g) ?? [];
|
||||
/** @type {{ start: number, end: number, metric: CatalogMetric }[]} */
|
||||
const matches = [];
|
||||
|
||||
for (let start = 0; start < words.length; start += 1) {
|
||||
for (let end = start + 1; end <= words.length; end += 1) {
|
||||
const named = index.bySearchableName.get(words.slice(start, end).join(" "));
|
||||
if (named?.length === 1) matches.push({ start, end, metric: named[0] });
|
||||
}
|
||||
}
|
||||
|
||||
const maximal = matches.filter((match) =>
|
||||
!matches.some((candidate) =>
|
||||
candidate.start <= match.start &&
|
||||
candidate.end >= match.end &&
|
||||
candidate.end - candidate.start > match.end - match.start
|
||||
)
|
||||
);
|
||||
return [...new Map(
|
||||
maximal.map(({ metric }) => [metric.path, publicMetric(metric)]),
|
||||
).values()];
|
||||
}
|
||||
|
||||
/** @param {ReturnType<typeof buildState>} index */
|
||||
function categories(index) {
|
||||
/** @type {Map<string, { path: string, label: string, count: number, branches: Map<string, number>[] }>} */
|
||||
@@ -301,6 +332,8 @@ self.addEventListener("message", async (event) => {
|
||||
let result;
|
||||
if (type === "search") {
|
||||
result = search(index, data.queries, data.limit, data.prefixes);
|
||||
} else if (type === "mentions") {
|
||||
result = mentions(index, data.query);
|
||||
} else if (type === "categories") {
|
||||
result = categories(index);
|
||||
} else if (type === "byName") {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { createChartArtifact } from "./chart.js";
|
||||
import { readMetric } from "./data.js";
|
||||
import { directChartCommand } from "./direct/chart.js";
|
||||
import { searchLearn } from "./learn.js";
|
||||
import {
|
||||
metricByName,
|
||||
metricCategories,
|
||||
metricVariants,
|
||||
mentionedMetrics,
|
||||
searchMetrics,
|
||||
} from "./metrics/index.js";
|
||||
import { ASK_STAGE_PROMPTS } from "./prompts.js";
|
||||
@@ -101,6 +103,15 @@ function isDirectDefinition(request) {
|
||||
return asks && !needsRouting;
|
||||
}
|
||||
|
||||
/** @param {string[]} topics @param {() => void} onProgress */
|
||||
async function exactTopicMetrics(topics, onProgress) {
|
||||
if (!topics.length) return [];
|
||||
|
||||
const names = new Set(topics.map(normalize));
|
||||
const metrics = await searchMetrics(topics, MAX_OPTIONS, [], onProgress);
|
||||
return metrics.filter((metric) => names.has(normalize(metric.name)));
|
||||
}
|
||||
|
||||
/** @param {string} request */
|
||||
function directReadAction(request) {
|
||||
const block = request.match(/\bblock\s+(\d{4,})\b/i)?.[1];
|
||||
@@ -251,22 +262,50 @@ export class AskToolSession {
|
||||
|
||||
/** @param {(status: string) => void} onStatus */
|
||||
async tryDirect(onStatus) {
|
||||
const chart = directChartCommand(this.request, Boolean(this.activeChart));
|
||||
if (chart) {
|
||||
let metrics = await mentionedMetrics(
|
||||
this.request,
|
||||
() => onStatus("Indexing metrics…"),
|
||||
);
|
||||
if (!metrics.length) {
|
||||
metrics = await exactTopicMetrics(
|
||||
this.previousTopics,
|
||||
() => onStatus("Indexing metrics…"),
|
||||
);
|
||||
}
|
||||
|
||||
if (metrics.length) {
|
||||
const refs = metrics.map((metric) => this.refs.issue("metric", metric, metric.path));
|
||||
this.outcome = chart.kind === "edit"
|
||||
? "edit_existing_chart"
|
||||
: "build_requested_chart";
|
||||
this.rememberMetrics(refs);
|
||||
onStatus("Building chart…");
|
||||
const result = this.buildChart({ refs, operation: chart.operation });
|
||||
return { output: result.output, artifacts: result.artifacts };
|
||||
}
|
||||
}
|
||||
|
||||
if (this.previousTopics.length === 1 && isDirectValueFollowup(this.request)) {
|
||||
const action = directReadAction(this.request);
|
||||
if (action) {
|
||||
const topic = this.previousTopics[0];
|
||||
const metrics = (await searchMetrics(
|
||||
[topic],
|
||||
3,
|
||||
[],
|
||||
let metrics = await mentionedMetrics(
|
||||
this.request,
|
||||
() => onStatus("Indexing metrics…"),
|
||||
)).filter((metric) => normalize(metric.name) === normalize(topic));
|
||||
);
|
||||
if (!metrics.length) {
|
||||
metrics = await exactTopicMetrics(
|
||||
this.previousTopics,
|
||||
() => onStatus("Indexing metrics…"),
|
||||
);
|
||||
}
|
||||
|
||||
if (metrics.length === 1) {
|
||||
if (metrics.length) {
|
||||
onStatus("Reading data…");
|
||||
const result = await readMetric(metrics[0], action);
|
||||
this.previousTopics = [label(metrics[0].name)];
|
||||
return { output: renderData([result]), artifacts: [] };
|
||||
const results = await Promise.all(metrics.map((metric) => readMetric(metric, action)));
|
||||
this.previousTopics = metrics.map((metric) => label(metric.name));
|
||||
return { output: renderData(results), artifacts: [] };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user