UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

247 lines • 10.1 kB
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. ========= // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // ========= Copyright 2026 @ Strukto.AI All Rights Reserved. ========= import { compareCodePoints } from "../../utils/sort.js"; import { valueText } from "../render/json.js"; import { groupName, rowStem } from "./naming.js"; import { fieldValue } from "./payload.js"; export const SCROLL_BATCH = 256; /** * The non-string JSON scalar a rendered group segment also spells, or null. * * A group value renders through `valueText`, so a boolean or a number lists * as its compact JSON and the segment alone cannot say which type the * payload holds. Only a spelling `valueText` would produce counts: `007`, * `-0` and `1.50` are strings and nothing else. */ export function jsonScalar(text) { let parsed; try { parsed = JSON.parse(text); } catch { return null; } if (typeof parsed === 'boolean') return parsed; if (typeof parsed === 'number' && valueText(parsed) === text) return parsed; return null; } /** * What one rendered group segment matches in the payload: the string itself * always, and when the segment also spells a JSON scalar, that typed value * too, so descending into the `true` or `1.5` directory the listing * advertised finds the boolean or float points behind it. A number matches * as a closed range, which Qdrant applies to integer and float payloads * alike where `match` does not. */ export function condition(key, text) { const asText = { key, match: { value: text } }; const scalar = jsonScalar(text); if (scalar === null) return asText; const typed = typeof scalar === 'boolean' ? { key, match: { value: scalar } } : { key, range: { gte: scalar, lte: scalar } }; return { should: [asText, typed] }; } export function buildFilter(filters) { const keys = Object.keys(filters); if (keys.length === 0) return undefined; return { must: keys.map((key) => condition(key, filters[key] ?? '')) }; } /** Keep points whose id starts with a literal name prefix. */ export function idPrefixTest(prefix) { return (point) => String(point.id).startsWith(prefix); } /** Keep points whose payload value starts with a literal prefix. */ export function valuePrefixTest(column, prefix, basename = false) { return (point) => { const value = fieldValue(point.payload ?? {}, column); if (value === null || value === undefined) return false; return groupName(value, basename).startsWith(prefix); }; } /** Keep the first point of every raw value that renders as one group name. */ export function exactNameTest(column, name, basename, seen) { return (point) => { const value = fieldValue(point.payload ?? {}, column); if (value === null || value === undefined) return false; const raw = valueText(value); if (seen.has(raw) || groupName(raw, basename) !== name) return false; seen.add(raw); return true; }; } export function pointToRow(point, idField) { return { ...point.payload, [idField]: point.id }; } const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; export function candidateIds(rowId) { if (/^-?\d+$/.test(rowId)) return [Number.parseInt(rowId, 10)]; if (UUID_RE.test(rowId)) return [rowId]; return []; } async function scrollRaw(accessor, collection, filter, limit, keep) { // Without a test the limit bounds the scroll, which is the ordinary capped // listing. With one it bounds the MATCHES, because qdrant has no prefix // condition for a point id or a keyword field: the only way to answer a // glob for a row past the cap is to keep scrolling and test each page // here. A glob is a targeted request, so it pays a scan of the collection // where the plain listing pays one page. const client = await accessor.client(); const points = []; let offset = null; while (points.length < limit) { const res = (await client.scroll(collection, { ...(filter !== undefined ? { filter } : {}), limit: keep !== undefined ? SCROLL_BATCH : Math.min(SCROLL_BATCH, limit - points.length), offset, with_payload: true, with_vector: false, })); for (const item of keep === undefined ? res.points : res.points.filter(keep)) points.push(item); const next = res.next_page_offset; if (next === null || next === undefined) break; offset = next; } return points.slice(0, limit); } function isIndexRequired(err) { if (typeof err !== 'object' || err === null) return false; const e = err; if (e.status !== 400) return false; const text = `${JSON.stringify(e.data ?? '')} ${e.message ?? ''}`.toLowerCase(); return text.includes('index required'); } async function ensureIndexes(accessor, collection) { if (accessor.indexesEnsured.has(collection)) return; const client = await accessor.client(); for (const field of accessor.config.groupBy) { await client.createPayloadIndex(collection, { field_name: field, field_schema: 'keyword' }); } accessor.indexesEnsured.add(collection); } async function scrollAll(accessor, collection, filters, limit, keep) { const filter = buildFilter(filters); if (filter === undefined) return scrollRaw(accessor, collection, undefined, limit, keep); try { return await scrollRaw(accessor, collection, filter, limit, keep); } catch (err) { if (!isIndexRequired(err)) throw err; } await ensureIndexes(accessor, collection); return scrollRaw(accessor, collection, filter, limit, keep); } export async function listTables(accessor) { const client = await accessor.client(); const res = (await client.getCollections()); return res.collections.map((c) => c.name).sort(compareCodePoints); } export async function tableExists(accessor, name) { const client = await accessor.client(); const res = (await client.collectionExists(name)); return res.exists; } export async function distinctValues(accessor, table, column, filters, limit, prefix = '', basename = false) { const keep = prefix === '' ? undefined : valuePrefixTest(column, prefix, basename); const points = await scrollAll(accessor, table, filters, limit, keep); const values = new Set(); for (const point of points) { const value = fieldValue(point.payload ?? {}, column); if (value !== null && value !== undefined) values.add(valueText(value)); } return [...values].sort(compareCodePoints); } /** * The raw payload values one rendered group segment stands for. * * A basename drops the value's parents, so two sources can render as the * same directory. Telling them apart is a question about every point under * the parent group, not about the first `maxRows`: the scroll runs until it * is exhausted or a second distinct value has rendered as `name`, whichever * comes first. One value is the answer; two is a collision for the caller * to refuse. */ export async function resolveGroup(accessor, table, column, filters, name, basename = false) { const keep = exactNameTest(column, name, basename, new Set()); const points = await scrollAll(accessor, table, filters, 2, keep); return points .map((point) => valueText(fieldValue(point.payload ?? {}, column))) .sort(compareCodePoints); } export async function rowsMatching(accessor, table, filters, limit, prefix = '') { const config = accessor.config; const keep = prefix === '' ? undefined : config.nameField !== null ? (point) => rowStem(pointToRow(point, config.idField), config).startsWith(prefix) : idPrefixTest(prefix); const points = await scrollAll(accessor, table, filters, limit, keep); return points.map((point) => pointToRow(point, config.idField)); } export async function rowRecord(accessor, table, idField, rowId) { const ids = candidateIds(rowId); if (ids.length === 0) return null; const client = await accessor.client(); const found = (await client.retrieve(table, { ids, with_payload: true, with_vector: false, })); return found[0] !== undefined ? pointToRow(found[0], idField) : null; } export async function searchRows(accessor, table, query, limit) { return accessor.searches.get(JSON.stringify([table, query, limit]), async () => { const config = accessor.config; const client = await accessor.client(); // A caller-supplied `embed` vectorizes the query here, and cloud inference // sends the text to a cluster that embeds it. let vector; if (config.embed !== null) vector = await config.embed(query); else if (config.cloudInference) vector = { text: query, model: config.embeddingModel }; else { throw new Error('the query needs a vector: pass embed, or set cloud_inference for a cluster with inference'); } const res = (await client.query(table, { query: vector, limit, with_payload: true, })); return res.points.map((point) => { const row = pointToRow(point, config.idField); row._score = point.score; return row; }); }); } //# sourceMappingURL=query.js.map