Spaces:
Running
Running
File size: 13,339 Bytes
a942796 856d716 a942796 856d716 a942796 856d716 a942796 856d716 a942796 627a3d6 a942796 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 | // Reading the index in the browser. Every file comes from one commit of the dataset, so every number agrees.
import { asyncBufferFromUrl, parquetMetadataAsync, parquetReadObjects } from 'https://cdn.jsdelivr.net/npm/hyparquet@1.31.5/+esm';
import { compressors } from 'https://cdn.jsdelivr.net/npm/hyparquet-compressors@1.1.2/+esm';
export const REPO = 'Battam/agentic-env-index';
export const HF = 'https://huggingface.co';
export const DATASET_URL = `${HF}/datasets/${REPO}`;
export const fileUrl = (sha, path) => `${DATASET_URL}/resolve/${sha}/${path}`;
export const num = (v) => (v == null ? null : typeof v === 'bigint' ? Number(v) : Number(v));
async function getJson(url) {
const r = await fetch(url);
if (!r.ok) throw new Error(`${url}: HTTP ${r.status}`);
return r.json();
}
async function getText(url) {
const r = await fetch(url);
if (!r.ok) throw new Error(`${url}: HTTP ${r.status}`);
return r.text();
}
/** A small file read whole (the meta tables): one request instead of a size probe and ranges. */
export async function readParquet(url, options = {}) {
const r = await fetch(url);
if (!r.ok) throw new Error(`${url}: HTTP ${r.status}`);
const file = await r.arrayBuffer();
return parquetReadObjects({ file, compressors, utf8: false, ...options });
}
export async function parquetRows(url) {
const file = await asyncBufferFromUrl({ url });
return num((await parquetMetadataAsync(file)).num_rows);
}
/** The columns of cols that the file has, read with hyparquet (byte ranges); range: {rowStart, rowEnd}. */
export async function readColumns(url, cols, range = {}) {
const file = await asyncBufferFromUrl({ url });
const metadata = await parquetMetadataAsync(file);
const names = new Set(metadata.schema.slice(1).map((s) => s.name));
const columns = cols.filter((c) => names.has(c));
const rows = await parquetReadObjects({ file, metadata, columns, compressors, utf8: false, ...range });
return { rows, total: num(metadata.num_rows) };
}
/** The commit to read: ?rev=<sha or branch>, else main. Returns {sha, date, files}. */
export async function loadRevision(rev) {
const j = await getJson(`${HF}/api/datasets/${REPO}/revision/${encodeURIComponent(rev || 'main')}`);
if (!/^[0-9a-f]{40}$/.test(j.sha || '')) throw new Error('the Hub gave no commit id');
return { sha: j.sha, date: j.lastModified || null, files: (j.siblings || []).map((s) => s.rfilename) };
}
/** Batch folders of one source with the date of their last commit (Hub tree API). */
export async function loadBatchDates(sha, source) {
const j = await getJson(`${HF}/api/datasets/${REPO}/tree/${sha}/data/${encodeURIComponent(source)}?expand=true`);
const out = new Map();
for (const e of j) {
if (e.type !== 'directory') continue;
out.set(e.path.split('/').pop(), e.lastCommit?.date || null);
}
return out;
}
// ---- the classification contract (#252): domain is a list of codes (D1 to D10, with leaves), or multi, or
// unclassified; form is one id; names come from meta/vocabulary.json. Without them everything shows as unclassified.
const pick = (o, keys) => {
if (!o || typeof o !== 'object') return undefined;
for (const k of keys) if (o[k] != null) return o[k];
return undefined;
};
const NAME_KEYS = ['name', 'label', 'title', 'en'];
const CODE_KEYS = ['code', 'id', 'key', 'value'];
const KIDS_KEYS = ['children', 'leaves', 'subdomains', 'items'];
const ABOUT_KEYS = ['description', 'about', 'definition'];
function vocabSection(node) {
const names = new Map();
const about = new Map();
const order = [];
const add = (code, name, text) => {
if (code == null) return;
code = String(code);
if (!names.has(code)) order.push(code);
if (typeof name === 'string' && name.trim()) names.set(code, name.trim());
else if (!names.has(code)) names.set(code, null);
if (typeof text === 'string' && text.trim()) about.set(code, text.trim());
};
const walk = (n) => {
if (!n) return;
if (Array.isArray(n)) {
for (const it of n) {
if (typeof it === 'string') add(it, null);
else if (it && typeof it === 'object') {
add(pick(it, CODE_KEYS), pick(it, NAME_KEYS), pick(it, ABOUT_KEYS));
walk(pick(it, KIDS_KEYS));
}
}
} else if (typeof n === 'object') {
for (const [k, v] of Object.entries(n)) {
if (typeof v === 'string') add(k, v);
else if (v && typeof v === 'object') {
add(k, pick(v, NAME_KEYS), pick(v, ABOUT_KEYS));
walk(pick(v, KIDS_KEYS));
} else add(k, null);
}
}
};
walk(node);
return { names, about, order };
}
// meta/vocabulary.json (#304): domains [{code, name, about, parent}], forms [{id, about}]. A domain shows its name; a
// form has no name, so it shows its id written as words (test-suites -> test suites).
export function parseVocabulary(j) {
const form = vocabSection(pick(j, ['form', 'forms']));
for (const [id, name] of form.names) if (!name) form.names.set(id, id.replace(/[-_]+/g, ' '));
return { domain: vocabSection(pick(j, ['domain', 'domains'])), form };
}
export const topOf = (code) => String(code).split('.')[0];
function domainOf(v) {
let list = v;
if (typeof list === 'string') list = [list];
if (list && typeof list.toArray === 'function') list = list.toArray();
list = Array.isArray(list) ? list.map(String).map((s) => s.trim()).filter(Boolean) : [];
if (!list.length || list.includes('unclassified')) return { kind: 'none', codes: [] };
if (list.includes('multi')) return { kind: 'multi', codes: [] };
return { kind: 'codes', codes: [...new Set(list)] };
}
// ---- small parsers of the index's own text files
const intOf = (s) => Number(String(s).replace(/,/g, ''));
/** CHANGELOG.md -> [{date, title, kind, sourcesAdded, sourcesRemoved, rowsAdded, rowsRemoved, sources, rows}] */
export function parseChangelog(md) {
const out = [];
const parts = md.split(/^## /m).slice(1);
for (const part of parts) {
const [head, ...lines] = part.split('\n');
const m = head.match(/^(\d{4}-\d{2}-\d{2}):\s*(.+)$/);
if (!m) continue;
const e = { date: m[1], title: m[2].trim(), kind: 'other', sourcesAdded: null, sourcesRemoved: null, rowsAdded: 0, rowsRemoved: 0 };
const body = lines.join('\n');
const start = body.match(/lists ([\d,]+) sources with ([\d,]+) rows/);
if (start) {
e.kind = 'start';
e.sources = intOf(start[1]);
e.rows = intOf(start[2]);
}
const sa = body.match(/^- Sources added \((\d+)\)/m);
const sr = body.match(/^- Sources removed \((\d+)\)/m);
if (sa || sr) {
e.kind = 'update';
e.sourcesAdded = sa ? Number(sa[1]) : 0;
e.sourcesRemoved = sr ? Number(sr[1]) : 0;
}
for (const r of body.matchAll(/^\s+- \S+: .*?\(([\d,]+) to ([\d,]+)\)\s*$/gm)) {
const d = intOf(r[2]) - intOf(r[1]);
if (d > 0) e.rowsAdded += d;
else e.rowsRemoved -= d;
}
out.push(e);
}
return out;
}
/** README.md, section "Not included, and why": category -> requirement sentence. */
export function parseRequirements(md) {
const out = new Map();
const sec = md.split(/^## /m).find((s) => s.startsWith('Not included'));
if (!sec) return out;
for (const m of sec.matchAll(/^\| `([a-z0-9_]+)` \| (.+?) \| \d+ \| \d+ \|\s*$/gm)) out.set(m[1], m[2].trim());
return out;
}
/** What every row of a source allows, from sources.json license_rows: 'yes' (every row), 'cond' (yes or
* conditional for every row) or 'no' (some rows are no or unknown). */
function permission(lr) {
if (!lr) return 'no';
const keys = Object.entries(lr).filter(([, v]) => num(v?.rows) > 0).map(([k]) => k);
if (!keys.length) return 'no';
if (keys.every((k) => k === 'yes')) return 'yes';
if (keys.every((k) => k === 'yes' || k === 'conditional')) return 'cond';
return 'no';
}
async function readCatalog(url) {
if (/\.json(\?|$)/i.test(url)) {
const j = await getJson(url);
return Array.isArray(j) ? j : j.sources || j.rows || [];
}
return readParquet(url);
}
/** Everything the page shows, from one commit. params: URLSearchParams (rev, catalog, vocabulary). */
export async function loadIndex(params, onNote) {
const rev = await loadRevision(params.get('rev'));
const at = (p) => fileUrl(rev.sha, p);
const catalogUrl = params.get('catalog') || at('meta/catalog.parquet');
const has = (p) => rev.files.includes(p);
const vocabUrl = params.get('vocabulary') || (has('meta/vocabulary.json') ? at('meta/vocabulary.json') : null);
const [catalog, sourcesJson, coverage, batches, notIncluded, changelog, readme, vocabJson] = await Promise.all([
readCatalog(catalogUrl),
getJson(at('meta/sources.json')),
getJson(at('meta/coverage.json')),
readParquet(at('meta/batches.parquet')).catch(() => []),
readParquet(at('meta/not_included.parquet')).catch(() => []),
getText(at('CHANGELOG.md')).catch(() => ''),
getText(at('README.md')).catch(() => ''),
vocabUrl ? getJson(vocabUrl).catch((e) => { onNote?.(`The vocabulary could not be read (${e.message}); codes are shown as they are.`); return null; }) : null,
]);
const vocab = parseVocabulary(vocabJson || {});
const srcInfo = new Map((sourcesJson.sources || []).map((e) => [e.source, e]));
const cov = coverage.by_source || {};
const batchesBy = new Map();
for (const b of batches) {
if (!batchesBy.has(b.source)) batchesBy.set(b.source, []);
batchesBy.get(b.source).push(b);
}
const filesBy = new Map();
for (const f of rev.files) {
const m = f.match(/^data\/([^/]+)\/([^/]+)\/[^/]+\.parquet$/);
if (!m) continue;
if (!filesBy.has(m[1])) filesBy.set(m[1], []);
filesBy.get(m[1]).push({ path: f, batch: m[2] });
}
const sources = catalog.map((r) => {
const info = srcInfo.get(r.source) || {};
const lic = r.license || r.licence || {};
const dom = domainOf(r.domain);
const rows = num(r.files ?? info.rows) || 0;
const bytes = num(r.bytes);
const form = r.form == null || r.form === '' || r.form === 'unclassified' ? null : String(r.form);
// A unit is one unit group (a test case, one workbook); files are counted apart. Once the catalog carries the
// columns units (distinct units; a raw corpus counts rows), answers (units that have an answer) and files (files
// of every role), they are read as they are. Until then: rows of a collection carry roles (sources.json unit: unit,
// companion, answer) and such a source is task-ready; its units are its answer files when it has any (one answer
// per unit group), else its unit rows. Every other source is a raw corpus: each row is one unit to mine.
const roles = info.unit || {};
const has = (k) => r[k] != null && r[k] !== '' && Number.isFinite(num(r[k]));
const answers = has('answers') ? num(r.answers) : num(roles.answer) || 0;
const isTask = num(roles.unit) > 0 || answers > 0 || (has('units') && num(r.units) < rows);
const units = has('units') ? num(r.units) : isTask ? (answers || num(roles.unit)) : rows;
const lr = info.license_rows?.redistribution_ok || {};
const shipRows = (num(lr.yes?.rows) || 0) + (num(lr.conditional?.rows) || 0);
const shipShare = rows ? Math.min(1, shipRows / rows) : 0;
return {
isTask,
readyUnits: isTask ? units : 0,
rawRows: isTask ? 0 : rows,
fromCatalog: has('units'),
roles: { unit: num(roles.unit) || 0, companion: num(roles.companion) || 0, answer: answers },
kinds: Object.keys(roles).filter((k) => !['unit', 'companion', 'answer'].includes(k)),
units,
answered: answers,
shipShare,
id: r.source,
title: r.title || r.source,
description: r.description || info.description || '',
summary: r.summary || '',
unit: r.unit || '',
status: r.status || null,
reasons: Array.isArray(r.status_reasons) ? r.status_reasons : r.status_reasons?.toArray?.() || [],
domainKind: dom.kind,
codes: dom.codes,
tops: [...new Set(dom.codes.map(topOf))],
form,
tier: lic.tier || 'unknown',
ship: lic.ship || null,
spdx: lic.spdx || null,
licenceNote: lic.note || null,
contamination: r.contamination || {},
pii: r.pii || {},
rows,
bytes: bytes && bytes > 0 ? bytes : null,
// distinct content (#364): bytes minus the sizes of the rows meta/duplicates.parquet lists; null when not recorded
distinct: has('distinct_bytes') ? num(r.distinct_bytes) : null,
rowsNoSize: num(r.files_without_size) || 0,
topTypes: Array.isArray(r.top_types) ? r.top_types : r.top_types?.toArray?.() || [],
needsAuth: r.needs_auth || null,
train: permission(info.license_rows?.training_ok),
redist: permission(info.license_rows?.redistribution_ok),
info,
cov: cov[r.source] || null,
batches: batchesBy.get(r.source) || [],
files: (filesBy.get(r.source) || []).sort((a, b) => a.path.localeCompare(b.path)),
};
});
return {
sha: rev.sha,
date: rev.date,
overrides: { catalog: params.get('catalog'), vocabulary: params.get('vocabulary') },
hasVocabulary: Boolean(vocabJson),
vocab,
sources,
coverage,
notIncluded,
requirements: parseRequirements(readme),
changelog: parseChangelog(changelog),
};
}
|