@webskill/sdk 0.27.0 → 0.28.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent.d.ts +3 -3
- package/dist/agent.js +912 -120
- package/dist/{approval-BSwffz8P.js → approval-5BmspsRi.js} +1 -1
- package/dist/browser.d.ts +7 -7
- package/dist/browser.js +415 -49
- package/dist/governance.d.ts +3 -3
- package/dist/{index-QKiMepxO.d.ts → index-CkauSvus.d.ts} +103 -6
- package/dist/{index-C8-9-8zo.d.ts → index-DJeYqzlE.d.ts} +41 -13
- package/dist/{index-D6eSrEi5.d.ts → index-DU8sKyQq.d.ts} +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +10 -4
- package/dist/mcp.d.ts +2 -2
- package/dist/{miniChart-D0nYMzz8.js → miniChart-k95Dpy0q.js} +82 -4
- package/dist/node.d.ts +4 -3
- package/dist/node.js +18 -1
- package/dist/{openUiLibrary-CxtrnX54.js → openUiLibrary-BnhYUlqY.js} +1 -1
- package/dist/{openUiSpecLang-DIcLcybq.js → openUiSpecLang-C8qNOxzA.js} +432 -19
- package/dist/{renderResult-D9Q-Vu2x.js → renderResult-2v3WQzs_.js} +5 -2
- package/dist/{skillVersionStore-S4_HJ_Fl-Ds97pwm5.d.ts → skillVersionStore-S4_HJ_Fl-DzZM_UIw.d.ts} +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/{types-CpDRZ0rA-CFQ-vdKz.d.ts → types-wKp3YBSM-167azx1H.d.ts} +6 -2
- package/dist/ui-react.d.ts +3 -3
- package/dist/ui-react.js +39 -3
- package/dist/ui-vue.d.ts +1 -1
- package/dist/ui-vue.js +1 -1
- package/dist/ui.d.ts +4 -4
- package/dist/ui.js +13 -7
- package/dist/{webSkillApi-C_7CTwtB.js → webSkillApi-rX2e3SVB.js} +1 -1
- package/dist/{webskillLitCatalog-BPyjYuRT.js → webskillLitCatalog-MfiLE5aq.js} +41 -2
- package/package.json +2 -1
package/dist/agent.js
CHANGED
|
@@ -305,7 +305,7 @@ const SCRIPT_EXTENSIONS = [".ts", ".js"];
|
|
|
305
305
|
function defaultConfirmSkill(draft) {
|
|
306
306
|
return `Save this conversation as the skill "${draft.name}"? ${draft.description}\nIt will be submitted for review, not activated. Please check the preview below for anything that must not be stored.`;
|
|
307
307
|
}
|
|
308
|
-
function fail$
|
|
308
|
+
function fail$4(message, details) {
|
|
309
309
|
throw new WebSkillError("SKILL_GENERATION_VALIDATION_FAILED", message, details);
|
|
310
310
|
}
|
|
311
311
|
/**
|
|
@@ -313,21 +313,21 @@ function fail$2(message, details) {
|
|
|
313
313
|
* 再去打扰用户——不让用户确认一个注定会失败的技能。
|
|
314
314
|
*/
|
|
315
315
|
function validateDraft(draft) {
|
|
316
|
-
if (!isValidSkillName(draft.name)) fail$
|
|
317
|
-
if (draft.description.trim() === "") fail$
|
|
318
|
-
if (typeof draft.content !== "string" || draft.content.trim() === "") fail$
|
|
316
|
+
if (!isValidSkillName(draft.name)) fail$4(`"${draft.name}" is not a valid skill name; use lowercase letters, digits and hyphens`);
|
|
317
|
+
if (draft.description.trim() === "") fail$4("The skill description must not be empty");
|
|
318
|
+
if (typeof draft.content !== "string" || draft.content.trim() === "") fail$4("The SKILL.md content must not be empty");
|
|
319
319
|
let metadata;
|
|
320
320
|
try {
|
|
321
321
|
metadata = parseSkillMarkdown(draft.content).metadata;
|
|
322
322
|
} catch (e) {
|
|
323
|
-
fail$
|
|
323
|
+
fail$4(`The SKILL.md content is not a valid skill document: ${e instanceof Error ? e.message : String(e)}`);
|
|
324
324
|
}
|
|
325
|
-
if (metadata.name !== draft.name) fail$
|
|
325
|
+
if (metadata.name !== draft.name) fail$4(`The SKILL.md frontmatter declares name "${metadata.name}" but the draft name is "${draft.name}"`);
|
|
326
326
|
for (const file of draft.files ?? []) {
|
|
327
327
|
const path = file.path;
|
|
328
|
-
if (path.trim() === "") fail$
|
|
329
|
-
if (path.startsWith("/") || path.includes("\\") || path.split("/").includes("..")) fail$
|
|
330
|
-
if (path.startsWith("scripts/") && !SCRIPT_EXTENSIONS.some((ext) => path.endsWith(ext))) fail$
|
|
328
|
+
if (path.trim() === "") fail$4("Every attached file needs a path");
|
|
329
|
+
if (path.startsWith("/") || path.includes("\\") || path.split("/").includes("..")) fail$4(`The attached file path "${path}" must be relative and must not escape the skill folder`);
|
|
330
|
+
if (path.startsWith("scripts/") && !SCRIPT_EXTENSIONS.some((ext) => path.endsWith(ext))) fail$4(`The script "${path}" must end with .ts or .js`);
|
|
331
331
|
}
|
|
332
332
|
}
|
|
333
333
|
/** 确认面板正文:完整 SKILL.md + 附带文件,供用户逐字检查(FR-9.4 风险缓解) */
|
|
@@ -359,25 +359,25 @@ async function materializeBinaries(draft, options) {
|
|
|
359
359
|
const out = /* @__PURE__ */ new Map();
|
|
360
360
|
if (claimed.length === 0) return out;
|
|
361
361
|
const resolve = options.resolveAttachment;
|
|
362
|
-
if (resolve === void 0) fail$
|
|
362
|
+
if (resolve === void 0) fail$4("This host cannot put uploaded files into a skill. Remove \"attachmentId\" from the file entries and write their contents instead.");
|
|
363
363
|
const maxOne = options.policy?.maxBinaryBytes ?? DEFAULT_MAX_BINARY_BYTES;
|
|
364
364
|
const maxTotal = options.policy?.maxTotalBinaryBytes ?? DEFAULT_MAX_TOTAL_BINARY_BYTES;
|
|
365
365
|
let total = 0;
|
|
366
366
|
for (const file of claimed) {
|
|
367
367
|
const id = file.attachmentId;
|
|
368
368
|
const attachment = await resolve(id);
|
|
369
|
-
if (attachment === void 0) fail$
|
|
369
|
+
if (attachment === void 0) fail$4(`No file with attachment id "${id}" was uploaded in this conversation, so "${file.path}" cannot be added to the skill.`, {
|
|
370
370
|
attachmentId: id,
|
|
371
371
|
path: file.path
|
|
372
372
|
});
|
|
373
373
|
const size = attachment.bytes.byteLength;
|
|
374
|
-
if (size > maxOne) fail$
|
|
374
|
+
if (size > maxOne) fail$4(`The attached file "${attachment.name}" is ${size} bytes, above the ${maxOne} byte limit for one file.`, {
|
|
375
375
|
path: file.path,
|
|
376
376
|
size,
|
|
377
377
|
limit: maxOne
|
|
378
378
|
});
|
|
379
379
|
total += size;
|
|
380
|
-
if (total > maxTotal) fail$
|
|
380
|
+
if (total > maxTotal) fail$4(`The attached files add up to ${total} bytes, above the ${maxTotal} byte limit for one skill.`, {
|
|
381
381
|
path: file.path,
|
|
382
382
|
total,
|
|
383
383
|
limit: maxTotal
|
|
@@ -404,7 +404,7 @@ function assertStepsAreBackedByTrace(draft, records) {
|
|
|
404
404
|
const executed = new Set(records.map((r) => r.tool));
|
|
405
405
|
const unknown = [...new Set(claimed.map((s) => s.tool).filter((tool) => !executed.has(tool)))];
|
|
406
406
|
if (unknown.length === 0) return;
|
|
407
|
-
fail$
|
|
407
|
+
fail$4(`The skill claims tools that were never called in this session: ${unknown.join(", ")}. Tools actually called: ${executed.size === 0 ? "(none)" : [...executed].sort().join(", ")}. Rewrite the skill using only the tools above.`, {
|
|
408
408
|
claimed: unknown,
|
|
409
409
|
executed: [...executed].sort()
|
|
410
410
|
});
|
|
@@ -422,7 +422,7 @@ function assertStepsAreNotTruncated(draft, records) {
|
|
|
422
422
|
const usable = new Set(records.filter((r) => r.truncated !== true).map((r) => r.tool));
|
|
423
423
|
const truncated = [...new Set(claimed.map((s) => s.tool).filter((tool) => !usable.has(tool)))];
|
|
424
424
|
if (truncated.length === 0) return;
|
|
425
|
-
fail$
|
|
425
|
+
fail$4(`The arguments of these tool calls were too large to record, so the skill cannot state them: ${truncated.join(", ")}. Rewrite the skill without these steps.`, { truncated });
|
|
426
426
|
}
|
|
427
427
|
/**
|
|
428
428
|
* 确认卡证据(AC-30.12 / AC-30.13):逐条列出模型**写进技能的**步骤,
|
|
@@ -561,6 +561,7 @@ const SKILL_GENERATION_SYSTEM_PROMPT = [
|
|
|
561
561
|
" from inside SKILL.md as `skill:<the skill name>/<the path you gave it>`, for example",
|
|
562
562
|
" `skill:monthly-report/assets/template.xlsx`. Write that, never the attachment id of this",
|
|
563
563
|
" conversation — that id is gone by the time anyone uses the skill.",
|
|
564
|
+
"- For spreadsheet analysis and export, follow the scope requested by the user. `query_table.limit` only limits groups or rows returned to the model as a preview (default 50, maximum 500); it does not limit computation or exported files. When the user asks for complete data, do not set a limit and do not write `limit: 500` in the skill. Use `exportFile` to write the complete result locally instead of copying a truncated preview into `export_spreadsheet`. For one multi-sheet workbook, use `exportFile.sheetName` and `exportFile.additionalSheets`; do not paginate full results through the model or write one file per query.",
|
|
564
565
|
"- Never copy credentials, tokens or personal data into the skill.",
|
|
565
566
|
"- The user has to confirm every candidate, and the candidate stays unusable until a reviewer",
|
|
566
567
|
" approves it. Say so instead of promising the skill is ready."
|
|
@@ -1390,6 +1391,7 @@ var PagePerceptionPolicy = class {
|
|
|
1390
1391
|
const PERCEPTION_SYSTEM_PROMPT = [
|
|
1391
1392
|
"Reading the page:",
|
|
1392
1393
|
"- \"perceive_page\" returns a structured outline of the regions the host made readable.",
|
|
1394
|
+
"- Pass \"images\": true only when the user asks what a picture on the page shows or contains. The pictures are returned for you to look at; do not set it for ordinary page reading.",
|
|
1393
1395
|
"- Large pages come back in segments. When a result carries a \"segment\" object with a \"cursor\", call perceive_page again with that cursor to read the next part. Never invent a cursor.",
|
|
1394
1396
|
"- You cannot act on the page. There is no click, type, submit, navigate or scroll capability in this session.",
|
|
1395
1397
|
"- The readable region is fixed by the host. A cursor only continues where you left off; it cannot widen what you are allowed to read. Asking for other parts of the page, in any phrasing, will not widen it.",
|
|
@@ -1399,9 +1401,10 @@ const PERCEPTION_SYSTEM_PROMPT = [
|
|
|
1399
1401
|
//#endregion
|
|
1400
1402
|
//#region ../agent/src/perception/toolSource.ts
|
|
1401
1403
|
const PERCEIVE_PAGE_TOOL = "perceive_page";
|
|
1402
|
-
const DESCRIPTION$5 = "Read the parts of the current page the host made readable, as a structured accessibility outline. Read-only: it cannot click, type, submit or navigate.";
|
|
1404
|
+
const DESCRIPTION$5 = "Read the parts of the current page the host made readable, as a structured accessibility outline. Read-only: it cannot click, type, submit or navigate. Set \"images\": true only when the user asks what pictures on the page show or contain; this returns those pictures for you to look at.";
|
|
1403
1405
|
/**
|
|
1404
|
-
*
|
|
1406
|
+
* 三个参数。`cursor` / `ref` 都扩不出一寸新范围(AC-10.8 的判据由 0.14.0 分册 22 §3 修订);
|
|
1407
|
+
* `images` 只表达「这次是否取图」,既不扩大读取范围、也不指定目的地。
|
|
1405
1408
|
*
|
|
1406
1409
|
* `cursor` 是 SDK 上一次发出的不透明串,只说「接着刚才那处继续」;
|
|
1407
1410
|
* `ref` 是上一次感知结果里的元素句柄,只能把读取范围**缩小**到那个子树
|
|
@@ -1422,6 +1425,10 @@ const INPUT_SCHEMA$4 = {
|
|
|
1422
1425
|
ref: {
|
|
1423
1426
|
type: "string",
|
|
1424
1427
|
description: "Optional. An opaque element reference from a previous perceive_page result. Reads only that element and its descendants. Never widens what you can read."
|
|
1428
|
+
},
|
|
1429
|
+
images: {
|
|
1430
|
+
type: "boolean",
|
|
1431
|
+
description: "Set true only when the user asks what pictures on the page show or contain. This returns the pictures so you can look at them; leave it out for ordinary page reading."
|
|
1425
1432
|
}
|
|
1426
1433
|
},
|
|
1427
1434
|
additionalProperties: false
|
|
@@ -1561,8 +1568,11 @@ function createPagePerceptionToolSource(options) {
|
|
|
1561
1568
|
});
|
|
1562
1569
|
return resultOf(pending, index);
|
|
1563
1570
|
}
|
|
1571
|
+
const imagesArg = args["images"];
|
|
1572
|
+
if (imagesArg !== void 0 && typeof imagesArg !== "boolean") return toolError$6("VALIDATION_FAILED", "perceive_page needs \"images\" to be a boolean when present.");
|
|
1573
|
+
const wantsImages = imagesArg === true;
|
|
1564
1574
|
try {
|
|
1565
|
-
const image = await options.imageCapture?.();
|
|
1575
|
+
const image = wantsImages ? await options.imageCaptureForRequest?.() : await options.imageCapture?.();
|
|
1566
1576
|
const capture = {
|
|
1567
1577
|
images: image?.enabled === true,
|
|
1568
1578
|
maxImageBytes: image?.maxImageBytes ?? 0,
|
|
@@ -1576,6 +1586,7 @@ function createPagePerceptionToolSource(options) {
|
|
|
1576
1586
|
excluded: record.exclude,
|
|
1577
1587
|
nodes
|
|
1578
1588
|
};
|
|
1589
|
+
if (wantsImages && image?.enabled !== true) data["imagesUnavailable"] = image?.unavailableReason ?? "Page pictures were not returned. The host has page image capture off, or no image-capable model is configured. Do not describe the pictures from the surrounding text; tell the user what is missing.";
|
|
1579
1590
|
if (imagesOmitted > 0) data["imagesOmitted"] = imagesOmitted;
|
|
1580
1591
|
if ((record.imageFailures ?? 0) > 0) data["imageFailures"] = record.imageFailures;
|
|
1581
1592
|
const { segments, nodesTotal } = segmentPerception(nodes, budget);
|
|
@@ -1795,7 +1806,7 @@ const RECORD_LIMIT$1 = 50;
|
|
|
1795
1806
|
function sanitizeDescription(value) {
|
|
1796
1807
|
return sanitizeUntrustedLine(value, DESCRIPTION_LIMIT);
|
|
1797
1808
|
}
|
|
1798
|
-
function isRecord$
|
|
1809
|
+
function isRecord$3(value) {
|
|
1799
1810
|
return typeof value === "object" && value !== null;
|
|
1800
1811
|
}
|
|
1801
1812
|
/**
|
|
@@ -1805,7 +1816,7 @@ function isRecord$2(value) {
|
|
|
1805
1816
|
* 标成后者会让读代码的人以为它已经被验证过了。
|
|
1806
1817
|
*/
|
|
1807
1818
|
function validate(raw, provenance) {
|
|
1808
|
-
if (!isRecord$
|
|
1819
|
+
if (!isRecord$3(raw)) return void 0;
|
|
1809
1820
|
const { id, kind, description, target } = raw;
|
|
1810
1821
|
if (typeof id !== "string" || !ID_PATTERN.test(id)) return void 0;
|
|
1811
1822
|
if (typeof kind !== "string" || !OFFERABLE_KINDS.includes(kind)) return void 0;
|
|
@@ -3084,7 +3095,7 @@ function readContent(result) {
|
|
|
3084
3095
|
//#region ../agent/src/spreadsheet/toolSource.ts
|
|
3085
3096
|
const EXPORT_SPREADSHEET_TOOL = "export_spreadsheet";
|
|
3086
3097
|
const DESCRIPTION$3 = "Write tabular data — and, when they belong in the sheet, pictures — to an Excel (.xlsx) file the user can download.";
|
|
3087
|
-
const CELL_SCHEMA = {
|
|
3098
|
+
const CELL_SCHEMA$1 = {
|
|
3088
3099
|
anyOf: [
|
|
3089
3100
|
{ type: "string" },
|
|
3090
3101
|
{ type: "number" },
|
|
@@ -3180,7 +3191,7 @@ function inputSchema(withImages) {
|
|
|
3180
3191
|
items: {
|
|
3181
3192
|
type: "array",
|
|
3182
3193
|
maxItems: SPREADSHEET_LIMITS.columnsPerSheet,
|
|
3183
|
-
items: CELL_SCHEMA
|
|
3194
|
+
items: CELL_SCHEMA$1
|
|
3184
3195
|
}
|
|
3185
3196
|
},
|
|
3186
3197
|
...withImages ? { images: IMAGES_SCHEMA } : {}
|
|
@@ -3210,7 +3221,7 @@ function toolError$2(e) {
|
|
|
3210
3221
|
}
|
|
3211
3222
|
};
|
|
3212
3223
|
}
|
|
3213
|
-
const isRecord$
|
|
3224
|
+
const isRecord$2 = (v) => typeof v === "object" && v !== null;
|
|
3214
3225
|
/**
|
|
3215
3226
|
* FR-10.3a 的两条分支。忘写后缀是模型的高频行为,为它打断一整轮不划算;
|
|
3216
3227
|
* 写成 `.csv` 则是**意图冲突**而不是疏漏,必须拒。
|
|
@@ -3218,15 +3229,15 @@ const isRecord$1 = (v) => typeof v === "object" && v !== null;
|
|
|
3218
3229
|
* 路径安全不在这里做:`writeArtifact` 那一条(`resolveInsideRoot`)是唯一的一份,
|
|
3219
3230
|
* FR-10.3b 明确禁止另写一份。
|
|
3220
3231
|
*/
|
|
3221
|
-
function resolveFileName$1(value) {
|
|
3222
|
-
if (typeof value !== "string" || value.trim() === "") throw new WebSkillError("VALIDATION_FAILED",
|
|
3232
|
+
function resolveFileName$1(value, toolName = "export_spreadsheet") {
|
|
3233
|
+
if (typeof value !== "string" || value.trim() === "") throw new WebSkillError("VALIDATION_FAILED", `${toolName} requires a non-empty "fileName".`);
|
|
3223
3234
|
const fileName = value.trim();
|
|
3224
3235
|
const lastSegment = fileName.split(/[\\/]/).pop() ?? "";
|
|
3225
3236
|
const dot = lastSegment.lastIndexOf(".");
|
|
3226
3237
|
const extension = dot <= 0 ? "" : lastSegment.slice(dot + 1);
|
|
3227
3238
|
if (extension === "") return `${fileName}${SPREADSHEET_EXTENSION}`;
|
|
3228
3239
|
if (extension.toLowerCase() === "xlsx") return fileName;
|
|
3229
|
-
throw new WebSkillError("VALIDATION_FAILED",
|
|
3240
|
+
throw new WebSkillError("VALIDATION_FAILED", `${toolName} writes .xlsx files; "${fileName}" asks for .${extension}. Drop the suffix or use .xlsx.`);
|
|
3230
3241
|
}
|
|
3231
3242
|
function summarize(spec) {
|
|
3232
3243
|
const sheets = spec.sheets;
|
|
@@ -3260,8 +3271,8 @@ function createSpreadsheetToolSource(options) {
|
|
|
3260
3271
|
canHandle: (name) => name === EXPORT_SPREADSHEET_TOOL,
|
|
3261
3272
|
captureArgs: (_name, args) => {
|
|
3262
3273
|
const spec = args["spec"];
|
|
3263
|
-
const sheets = isRecord$
|
|
3264
|
-
const rowCount = sheets.reduce((total, sheet) => total + (isRecord$
|
|
3274
|
+
const sheets = isRecord$2(spec) && Array.isArray(spec["sheets"]) ? spec["sheets"] : [];
|
|
3275
|
+
const rowCount = sheets.reduce((total, sheet) => total + (isRecord$2(sheet) && Array.isArray(sheet["rows"]) ? sheet["rows"].length : 0), 0);
|
|
3265
3276
|
return {
|
|
3266
3277
|
...typeof args["fileName"] === "string" ? { fileName: args["fileName"] } : {},
|
|
3267
3278
|
sheetCount: sheets.length,
|
|
@@ -3317,17 +3328,17 @@ function columnLetter(index) {
|
|
|
3317
3328
|
}
|
|
3318
3329
|
return out;
|
|
3319
3330
|
}
|
|
3320
|
-
const blank = (cells) => cells.every((cell) => cell.trim() === "");
|
|
3321
|
-
function fail$
|
|
3331
|
+
const blank$1 = (cells) => cells.every((cell) => cell.trim() === "");
|
|
3332
|
+
function fail$3(message) {
|
|
3322
3333
|
throw new WebSkillError("VALIDATION_FAILED", message);
|
|
3323
3334
|
}
|
|
3324
3335
|
/** 先按序号(1 基)、再按表名定位;缺省取第一张有内容的表 */
|
|
3325
3336
|
function resolveSheet(workbook, ref) {
|
|
3326
3337
|
const { sheets } = workbook;
|
|
3327
|
-
if (sheets.length === 0) fail$
|
|
3338
|
+
if (sheets.length === 0) fail$3("This workbook has no sheets.");
|
|
3328
3339
|
const names = () => sheets.map((sheet, i) => `${i + 1}: "${sheet.name}"`).join(", ");
|
|
3329
3340
|
if (ref === void 0) {
|
|
3330
|
-
const found = sheets.findIndex((sheet) => sheet.rows.some((row) => !blank(row)));
|
|
3341
|
+
const found = sheets.findIndex((sheet) => sheet.rows.some((row) => !blank$1(row)));
|
|
3331
3342
|
const index = found === -1 ? 0 : found;
|
|
3332
3343
|
return {
|
|
3333
3344
|
sheet: sheets[index],
|
|
@@ -3336,7 +3347,7 @@ function resolveSheet(workbook, ref) {
|
|
|
3336
3347
|
}
|
|
3337
3348
|
if (typeof ref === "number") {
|
|
3338
3349
|
const sheet = Number.isInteger(ref) ? sheets[ref - 1] : void 0;
|
|
3339
|
-
if (sheet === void 0) fail$
|
|
3350
|
+
if (sheet === void 0) fail$3(`There is no sheet number ${String(ref)}. Sheets: ${names()}.`);
|
|
3340
3351
|
return {
|
|
3341
3352
|
sheet,
|
|
3342
3353
|
index: ref - 1
|
|
@@ -3344,24 +3355,24 @@ function resolveSheet(workbook, ref) {
|
|
|
3344
3355
|
}
|
|
3345
3356
|
if (typeof ref === "string") {
|
|
3346
3357
|
const index = sheets.findIndex((sheet) => sheet.name === ref);
|
|
3347
|
-
if (index === -1) fail$
|
|
3358
|
+
if (index === -1) fail$3(`There is no sheet named "${ref}". Sheets: ${names()}.`);
|
|
3348
3359
|
return {
|
|
3349
3360
|
sheet: sheets[index],
|
|
3350
3361
|
index
|
|
3351
3362
|
};
|
|
3352
3363
|
}
|
|
3353
|
-
return fail$
|
|
3364
|
+
return fail$3("\"sheet\" must be a sheet name or a 1-based sheet number.");
|
|
3354
3365
|
}
|
|
3355
3366
|
/** 表头行(缺省为第一个有内容的行)+ 其后有内容的行;行号保持工作表的 1 基行号 */
|
|
3356
3367
|
function buildView(workbook, sheetRef, headerRowArg) {
|
|
3357
3368
|
const { sheet, index } = resolveSheet(workbook, sheetRef);
|
|
3358
3369
|
const rows = sheet.rows;
|
|
3359
3370
|
let headerIndex;
|
|
3360
|
-
if (headerRowArg === void 0) headerIndex = rows.findIndex((row) => !blank(row));
|
|
3371
|
+
if (headerRowArg === void 0) headerIndex = rows.findIndex((row) => !blank$1(row));
|
|
3361
3372
|
else {
|
|
3362
|
-
if (typeof headerRowArg !== "number" || !Number.isInteger(headerRowArg) || headerRowArg < 1) fail$
|
|
3373
|
+
if (typeof headerRowArg !== "number" || !Number.isInteger(headerRowArg) || headerRowArg < 1) fail$3("\"headerRow\" must be a positive integer (1-based worksheet row number).");
|
|
3363
3374
|
headerIndex = headerRowArg - 1;
|
|
3364
|
-
if (headerIndex >= rows.length) fail$
|
|
3375
|
+
if (headerIndex >= rows.length) fail$3(`"headerRow" ${headerRowArg} is past the last row (${rows.length}) of the sheet.`);
|
|
3365
3376
|
}
|
|
3366
3377
|
if (headerIndex === -1) return {
|
|
3367
3378
|
sheetName: sheet.name,
|
|
@@ -3375,7 +3386,7 @@ function buildView(workbook, sheetRef, headerRowArg) {
|
|
|
3375
3386
|
let width = header.length;
|
|
3376
3387
|
for (let i = headerIndex + 1; i < rows.length; i += 1) {
|
|
3377
3388
|
const cells = rows[i];
|
|
3378
|
-
if (blank(cells)) continue;
|
|
3389
|
+
if (blank$1(cells)) continue;
|
|
3379
3390
|
if (cells.length > width) width = cells.length;
|
|
3380
3391
|
data.push({
|
|
3381
3392
|
row: i + 1,
|
|
@@ -3408,16 +3419,16 @@ function listColumns(view) {
|
|
|
3408
3419
|
}
|
|
3409
3420
|
/** 表头文字优先,其次列字母;表头重名时拒绝按表头引用,并点出同名各列的列字母 */
|
|
3410
3421
|
function resolveColumn(view, ref, what) {
|
|
3411
|
-
if (typeof ref !== "string" || ref.trim() === "") fail$
|
|
3422
|
+
if (typeof ref !== "string" || ref.trim() === "") fail$3(`${what} must name a column (header text or column letter).`);
|
|
3412
3423
|
const text = ref.trim();
|
|
3413
3424
|
const byHeader = view.columns.filter((column) => column.header === text);
|
|
3414
3425
|
if (byHeader.length === 1) return byHeader[0];
|
|
3415
|
-
if (byHeader.length > 1) fail$
|
|
3426
|
+
if (byHeader.length > 1) fail$3(`${what}: the header "${text}" is shared by columns ${byHeader.map((c) => c.letter).join(", ")}. Refer to the column by its letter instead.`);
|
|
3416
3427
|
if (LETTER_RE.test(text)) {
|
|
3417
3428
|
const byLetter = view.columns.find((column) => column.letter === text.toUpperCase());
|
|
3418
3429
|
if (byLetter !== void 0) return byLetter;
|
|
3419
3430
|
}
|
|
3420
|
-
return fail$
|
|
3431
|
+
return fail$3(`${what}: no column "${text}" on sheet "${view.sheetName}". Columns: ${listColumns(view)}.`);
|
|
3421
3432
|
}
|
|
3422
3433
|
/** Neumaier 补偿求和:几万个小数相加不累积漂移 */
|
|
3423
3434
|
var Summer = class {
|
|
@@ -3437,9 +3448,9 @@ const tidy = (x) => Number(x.toPrecision(12));
|
|
|
3437
3448
|
|
|
3438
3449
|
//#endregion
|
|
3439
3450
|
//#region ../agent/src/table/profile.ts
|
|
3440
|
-
const PROFILE_MAX_CHARS = 8e3;
|
|
3441
|
-
const DISTINCT_CAP = 1e4;
|
|
3442
|
-
const SAMPLE_ROWS = 5;
|
|
3451
|
+
const PROFILE_MAX_CHARS$1 = 8e3;
|
|
3452
|
+
const DISTINCT_CAP$1 = 1e4;
|
|
3453
|
+
const SAMPLE_ROWS$1 = 5;
|
|
3443
3454
|
function statsOf(view, column) {
|
|
3444
3455
|
let filled = 0;
|
|
3445
3456
|
let numbers = 0;
|
|
@@ -3470,7 +3481,7 @@ function statsOf(view, column) {
|
|
|
3470
3481
|
} else texts += 1;
|
|
3471
3482
|
const seen = freq.get(text);
|
|
3472
3483
|
if (seen !== void 0) freq.set(text, seen + 1);
|
|
3473
|
-
else if (freq.size < DISTINCT_CAP) freq.set(text, 1);
|
|
3484
|
+
else if (freq.size < DISTINCT_CAP$1) freq.set(text, 1);
|
|
3474
3485
|
else capped = true;
|
|
3475
3486
|
}
|
|
3476
3487
|
let type;
|
|
@@ -3495,16 +3506,16 @@ function statsOf(view, column) {
|
|
|
3495
3506
|
top
|
|
3496
3507
|
};
|
|
3497
3508
|
}
|
|
3498
|
-
const clip$
|
|
3509
|
+
const clip$3 = (text, max) => {
|
|
3499
3510
|
const chars = [...text.replace(/[\r\n\t]+/g, " ")];
|
|
3500
3511
|
return chars.length <= max ? chars.join("") : `${chars.slice(0, max - 1).join("")}…`;
|
|
3501
3512
|
};
|
|
3502
3513
|
/** 超出字符预算时的收缩阶梯:先少表、再少列、再收样本、最后去掉 top */
|
|
3503
|
-
const LADDER = [
|
|
3514
|
+
const LADDER$1 = [
|
|
3504
3515
|
{
|
|
3505
3516
|
maxSheets: 8,
|
|
3506
3517
|
maxColumns: 40,
|
|
3507
|
-
sampleRows: SAMPLE_ROWS,
|
|
3518
|
+
sampleRows: SAMPLE_ROWS$1,
|
|
3508
3519
|
sampleCell: 40,
|
|
3509
3520
|
withTop: true,
|
|
3510
3521
|
note: void 0
|
|
@@ -3512,7 +3523,7 @@ const LADDER = [
|
|
|
3512
3523
|
{
|
|
3513
3524
|
maxSheets: 3,
|
|
3514
3525
|
maxColumns: 40,
|
|
3515
|
-
sampleRows: SAMPLE_ROWS,
|
|
3526
|
+
sampleRows: SAMPLE_ROWS$1,
|
|
3516
3527
|
sampleCell: 40,
|
|
3517
3528
|
withTop: true,
|
|
3518
3529
|
note: "sheets"
|
|
@@ -3520,7 +3531,7 @@ const LADDER = [
|
|
|
3520
3531
|
{
|
|
3521
3532
|
maxSheets: 3,
|
|
3522
3533
|
maxColumns: 20,
|
|
3523
|
-
sampleRows: SAMPLE_ROWS,
|
|
3534
|
+
sampleRows: SAMPLE_ROWS$1,
|
|
3524
3535
|
sampleCell: 40,
|
|
3525
3536
|
withTop: true,
|
|
3526
3537
|
note: "sheets, columns"
|
|
@@ -3542,29 +3553,29 @@ const LADDER = [
|
|
|
3542
3553
|
note: "sheets, columns, sample, top values"
|
|
3543
3554
|
}
|
|
3544
3555
|
];
|
|
3545
|
-
function columnLine(column, stats, withTop) {
|
|
3556
|
+
function columnLine$1(column, stats, withTop) {
|
|
3546
3557
|
const parts = [
|
|
3547
|
-
`${column.letter} ${clip$
|
|
3558
|
+
`${column.letter} ${clip$3(column.header, 40)}`.trimEnd(),
|
|
3548
3559
|
stats.type,
|
|
3549
3560
|
`${stats.filled} filled`
|
|
3550
3561
|
];
|
|
3551
|
-
parts.push(stats.distinct === void 0 ? `>${DISTINCT_CAP} distinct` : `${stats.distinct} distinct`);
|
|
3562
|
+
parts.push(stats.distinct === void 0 ? `>${DISTINCT_CAP$1} distinct` : `${stats.distinct} distinct`);
|
|
3552
3563
|
if (stats.min !== void 0 && stats.max !== void 0) parts.push(`min ${tidy(stats.min)}`, `max ${tidy(stats.max)}`, `sum ${tidy(stats.sum)}`);
|
|
3553
3564
|
if (stats.earliest !== void 0 && stats.latest !== void 0) parts.push(`${stats.earliest} … ${stats.latest}`);
|
|
3554
|
-
if (withTop && stats.top.length > 0) parts.push(`top: ${stats.top.map(([value, count]) => `${clip$
|
|
3565
|
+
if (withTop && stats.top.length > 0) parts.push(`top: ${stats.top.map(([value, count]) => `${clip$3(value, 24)} ×${count}`).join(", ")}`);
|
|
3555
3566
|
return parts.join(" | ");
|
|
3556
3567
|
}
|
|
3557
|
-
function sheetSection(view, rung, expanded, statsFor) {
|
|
3568
|
+
function sheetSection$1(view, rung, expanded, statsFor) {
|
|
3558
3569
|
const head = `Sheet ${view.sheetIndex + 1} "${view.sheetName}": ${view.data.length} data rows × ${view.columns.length} columns` + (view.headerRow > 0 ? `, header at row ${view.headerRow}` : ", empty");
|
|
3559
3570
|
if (!expanded || view.columns.length === 0) return [head];
|
|
3560
3571
|
const lines = [head, "Columns:"];
|
|
3561
3572
|
const shown = view.columns.slice(0, rung.maxColumns);
|
|
3562
|
-
for (const column of shown) lines.push(columnLine(column, statsFor(column), rung.withTop));
|
|
3573
|
+
for (const column of shown) lines.push(columnLine$1(column, statsFor(column), rung.withTop));
|
|
3563
3574
|
if (view.columns.length > shown.length) lines.push(`+${view.columns.length - shown.length} more columns — call query_table with op "describe" and this sheet to list them`);
|
|
3564
3575
|
const sample = view.data.slice(0, rung.sampleRows);
|
|
3565
3576
|
if (sample.length > 0) {
|
|
3566
3577
|
lines.push(`Sample (sheet rows ${sample[0].row}–${sample[sample.length - 1].row}):`);
|
|
3567
|
-
for (const { cells } of sample) lines.push(shown.map((column) => clip$
|
|
3578
|
+
for (const { cells } of sample) lines.push(shown.map((column) => clip$3((cells[column.index] ?? "").trim(), rung.sampleCell)).join(" "));
|
|
3568
3579
|
}
|
|
3569
3580
|
return lines;
|
|
3570
3581
|
}
|
|
@@ -3591,13 +3602,293 @@ function profileTable(input) {
|
|
|
3591
3602
|
return stats;
|
|
3592
3603
|
};
|
|
3593
3604
|
let result = "";
|
|
3594
|
-
for (const rung of LADDER) {
|
|
3605
|
+
for (const rung of LADDER$1) {
|
|
3595
3606
|
const lines = [header];
|
|
3596
3607
|
let expandedCount = 0;
|
|
3597
3608
|
for (const [index, view] of views.entries()) {
|
|
3598
3609
|
const expanded = only === void 0 ? expandedCount < rung.maxSheets : index === only;
|
|
3599
3610
|
if (expanded && view.data.length > 0) expandedCount += 1;
|
|
3600
|
-
lines.push(...sheetSection(view, rung, expanded && (only !== void 0 || view.data.length > 0), statsFor(view)));
|
|
3611
|
+
lines.push(...sheetSection$1(view, rung, expanded && (only !== void 0 || view.data.length > 0), statsFor(view)));
|
|
3612
|
+
}
|
|
3613
|
+
if (rung.note !== void 0) lines.push(`Profile shortened to fit: ${rung.note}. Call query_table with op "describe" for the rest.`);
|
|
3614
|
+
result = lines.join("\n");
|
|
3615
|
+
if (result.length <= PROFILE_MAX_CHARS$1) return result;
|
|
3616
|
+
}
|
|
3617
|
+
return `${result.slice(0, PROFILE_MAX_CHARS$1 - 80)}\nProfile cut at ${PROFILE_MAX_CHARS$1} characters.`;
|
|
3618
|
+
}
|
|
3619
|
+
|
|
3620
|
+
//#endregion
|
|
3621
|
+
//#region ../agent/src/table/streaming.ts
|
|
3622
|
+
function isStreamingTableWorkbook(workbook) {
|
|
3623
|
+
return "kind" in workbook && workbook.kind === "streaming";
|
|
3624
|
+
}
|
|
3625
|
+
const blank = (cells) => cells.every((cell) => cell.trim() === "");
|
|
3626
|
+
function fail$2(message) {
|
|
3627
|
+
throw new WebSkillError("VALIDATION_FAILED", message);
|
|
3628
|
+
}
|
|
3629
|
+
async function resolveStreamingSheet(workbook, ref) {
|
|
3630
|
+
const { sheets } = workbook;
|
|
3631
|
+
if (sheets.length === 0) fail$2("This workbook has no sheets.");
|
|
3632
|
+
const names = () => sheets.map((sheet, i) => `${i + 1}: "${sheet.name}"`).join(", ");
|
|
3633
|
+
if (ref === void 0) {
|
|
3634
|
+
for (const [index, sheet] of sheets.entries()) if ((await sheet.meta()).firstNonEmptyRow > 0) return {
|
|
3635
|
+
sheet,
|
|
3636
|
+
index
|
|
3637
|
+
};
|
|
3638
|
+
return {
|
|
3639
|
+
sheet: sheets[0],
|
|
3640
|
+
index: 0
|
|
3641
|
+
};
|
|
3642
|
+
}
|
|
3643
|
+
if (typeof ref === "number") {
|
|
3644
|
+
const sheet = Number.isInteger(ref) ? sheets[ref - 1] : void 0;
|
|
3645
|
+
if (sheet === void 0) fail$2(`There is no sheet number ${String(ref)}. Sheets: ${names()}.`);
|
|
3646
|
+
return {
|
|
3647
|
+
sheet,
|
|
3648
|
+
index: ref - 1
|
|
3649
|
+
};
|
|
3650
|
+
}
|
|
3651
|
+
if (typeof ref === "string") {
|
|
3652
|
+
const index = sheets.findIndex((sheet) => sheet.name === ref);
|
|
3653
|
+
if (index === -1) fail$2(`There is no sheet named "${ref}". Sheets: ${names()}.`);
|
|
3654
|
+
return {
|
|
3655
|
+
sheet: sheets[index],
|
|
3656
|
+
index
|
|
3657
|
+
};
|
|
3658
|
+
}
|
|
3659
|
+
return fail$2("\"sheet\" must be a sheet name or a 1-based sheet number.");
|
|
3660
|
+
}
|
|
3661
|
+
async function rowAt(sheet, row) {
|
|
3662
|
+
for await (const entry of sheet.scan()) {
|
|
3663
|
+
if (entry.row === row) return entry.cells;
|
|
3664
|
+
if (entry.row > row) return void 0;
|
|
3665
|
+
}
|
|
3666
|
+
}
|
|
3667
|
+
async function buildStreamingView(workbook, sheetRef, headerRowArg) {
|
|
3668
|
+
const { sheet, index } = await resolveStreamingSheet(workbook, sheetRef);
|
|
3669
|
+
const meta = await sheet.meta();
|
|
3670
|
+
let headerRow;
|
|
3671
|
+
if (headerRowArg === void 0) headerRow = meta.firstNonEmptyRow;
|
|
3672
|
+
else {
|
|
3673
|
+
if (typeof headerRowArg !== "number" || !Number.isInteger(headerRowArg) || headerRowArg < 1) fail$2("\"headerRow\" must be a positive integer (1-based worksheet row number).");
|
|
3674
|
+
headerRow = headerRowArg;
|
|
3675
|
+
if (headerRow > meta.rowCount) fail$2(`"headerRow" ${headerRowArg} is past the last row (${meta.rowCount}) of the sheet.`);
|
|
3676
|
+
}
|
|
3677
|
+
if (headerRow === 0) return {
|
|
3678
|
+
sheetName: sheet.name,
|
|
3679
|
+
sheetIndex: index,
|
|
3680
|
+
headerRow: 0,
|
|
3681
|
+
columns: [],
|
|
3682
|
+
data: (async function* () {})()
|
|
3683
|
+
};
|
|
3684
|
+
const header = headerRow === meta.firstNonEmptyRow ? meta.firstRow : await rowAt(sheet, headerRow) ?? [];
|
|
3685
|
+
const width = Math.max(meta.columnCount, header.length);
|
|
3686
|
+
const columns = Array.from({ length: width }, (_, columnIndex) => {
|
|
3687
|
+
const text = (header[columnIndex] ?? "").trim();
|
|
3688
|
+
const letter = columnLetter(columnIndex);
|
|
3689
|
+
return {
|
|
3690
|
+
index: columnIndex,
|
|
3691
|
+
letter,
|
|
3692
|
+
header: text,
|
|
3693
|
+
label: text === "" ? letter : `${letter} ${text}`,
|
|
3694
|
+
name: text === "" ? letter : text
|
|
3695
|
+
};
|
|
3696
|
+
});
|
|
3697
|
+
const data = (async function* () {
|
|
3698
|
+
for await (const entry of sheet.scan()) if (entry.row > headerRow && !blank(entry.cells)) yield entry;
|
|
3699
|
+
})();
|
|
3700
|
+
return {
|
|
3701
|
+
sheetName: sheet.name,
|
|
3702
|
+
sheetIndex: index,
|
|
3703
|
+
headerRow,
|
|
3704
|
+
columns,
|
|
3705
|
+
data
|
|
3706
|
+
};
|
|
3707
|
+
}
|
|
3708
|
+
async function summarizeStreamingWorkbook(workbook) {
|
|
3709
|
+
let tokens = 0;
|
|
3710
|
+
let dataRows = 0;
|
|
3711
|
+
let textChars = 0;
|
|
3712
|
+
for (const sheet of workbook.sheets) {
|
|
3713
|
+
const meta = await sheet.meta();
|
|
3714
|
+
tokens += meta.estimatedTokens;
|
|
3715
|
+
dataRows += meta.dataRows;
|
|
3716
|
+
textChars += meta.textChars;
|
|
3717
|
+
}
|
|
3718
|
+
return {
|
|
3719
|
+
tokens,
|
|
3720
|
+
dataRows,
|
|
3721
|
+
textChars
|
|
3722
|
+
};
|
|
3723
|
+
}
|
|
3724
|
+
|
|
3725
|
+
//#endregion
|
|
3726
|
+
//#region ../agent/src/table/streamingProfile.ts
|
|
3727
|
+
const PROFILE_MAX_CHARS = 8e3;
|
|
3728
|
+
const DISTINCT_CAP = 1e4;
|
|
3729
|
+
const SAMPLE_ROWS = 5;
|
|
3730
|
+
var Stats = class {
|
|
3731
|
+
filled = 0;
|
|
3732
|
+
numbers = 0;
|
|
3733
|
+
dates = 0;
|
|
3734
|
+
texts = 0;
|
|
3735
|
+
summer = new Summer();
|
|
3736
|
+
min;
|
|
3737
|
+
max;
|
|
3738
|
+
earliest;
|
|
3739
|
+
latest;
|
|
3740
|
+
freq = /* @__PURE__ */ new Map();
|
|
3741
|
+
capped = false;
|
|
3742
|
+
add(raw) {
|
|
3743
|
+
const text = raw.trim();
|
|
3744
|
+
const kind = kindOf(text);
|
|
3745
|
+
if (kind === "empty") return;
|
|
3746
|
+
this.filled += 1;
|
|
3747
|
+
if (kind === "number") {
|
|
3748
|
+
this.numbers += 1;
|
|
3749
|
+
const value = Number(text);
|
|
3750
|
+
this.summer.add(value);
|
|
3751
|
+
if (this.min === void 0 || value < this.min) this.min = value;
|
|
3752
|
+
if (this.max === void 0 || value > this.max) this.max = value;
|
|
3753
|
+
} else if (kind === "date") {
|
|
3754
|
+
this.dates += 1;
|
|
3755
|
+
if (this.earliest === void 0 || text < this.earliest) this.earliest = text;
|
|
3756
|
+
if (this.latest === void 0 || text > this.latest) this.latest = text;
|
|
3757
|
+
} else this.texts += 1;
|
|
3758
|
+
const seen = this.freq.get(text);
|
|
3759
|
+
if (seen !== void 0) this.freq.set(text, seen + 1);
|
|
3760
|
+
else if (this.freq.size < DISTINCT_CAP) this.freq.set(text, 1);
|
|
3761
|
+
else this.capped = true;
|
|
3762
|
+
}
|
|
3763
|
+
finish() {
|
|
3764
|
+
let type;
|
|
3765
|
+
if (this.filled === 0) type = "empty";
|
|
3766
|
+
else if (this.numbers >= this.filled * .95) type = "number";
|
|
3767
|
+
else if (this.dates >= this.filled * .95) type = "date";
|
|
3768
|
+
else if (this.numbers + this.dates <= this.filled * .05) type = "text";
|
|
3769
|
+
else type = "mixed";
|
|
3770
|
+
const top = this.capped || type !== "text" && type !== "mixed" ? [] : [...this.freq.entries()].sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : 1)).slice(0, 3);
|
|
3771
|
+
return {
|
|
3772
|
+
filled: this.filled,
|
|
3773
|
+
type,
|
|
3774
|
+
distinct: this.capped ? void 0 : this.freq.size,
|
|
3775
|
+
sum: this.summer.total,
|
|
3776
|
+
min: this.min,
|
|
3777
|
+
max: this.max,
|
|
3778
|
+
earliest: this.earliest,
|
|
3779
|
+
latest: this.latest,
|
|
3780
|
+
top
|
|
3781
|
+
};
|
|
3782
|
+
}
|
|
3783
|
+
};
|
|
3784
|
+
async function analyze(view) {
|
|
3785
|
+
const collectors = view.columns.map(() => new Stats());
|
|
3786
|
+
const sample = [];
|
|
3787
|
+
let dataRows = 0;
|
|
3788
|
+
for await (const entry of view.data) {
|
|
3789
|
+
dataRows += 1;
|
|
3790
|
+
if (sample.length < SAMPLE_ROWS) sample.push(entry);
|
|
3791
|
+
for (const column of view.columns) collectors[column.index].add(entry.cells[column.index] ?? "");
|
|
3792
|
+
}
|
|
3793
|
+
return {
|
|
3794
|
+
dataRows,
|
|
3795
|
+
sample,
|
|
3796
|
+
columns: collectors.map((stats) => stats.finish())
|
|
3797
|
+
};
|
|
3798
|
+
}
|
|
3799
|
+
const clip$2 = (text, max) => {
|
|
3800
|
+
const chars = [...text.replace(/[\r\n\t]+/g, " ")];
|
|
3801
|
+
return chars.length <= max ? chars.join("") : `${chars.slice(0, max - 1).join("")}…`;
|
|
3802
|
+
};
|
|
3803
|
+
const LADDER = [
|
|
3804
|
+
{
|
|
3805
|
+
maxSheets: 8,
|
|
3806
|
+
maxColumns: 40,
|
|
3807
|
+
sampleRows: SAMPLE_ROWS,
|
|
3808
|
+
sampleCell: 40,
|
|
3809
|
+
withTop: true,
|
|
3810
|
+
note: void 0
|
|
3811
|
+
},
|
|
3812
|
+
{
|
|
3813
|
+
maxSheets: 3,
|
|
3814
|
+
maxColumns: 40,
|
|
3815
|
+
sampleRows: SAMPLE_ROWS,
|
|
3816
|
+
sampleCell: 40,
|
|
3817
|
+
withTop: true,
|
|
3818
|
+
note: "sheets"
|
|
3819
|
+
},
|
|
3820
|
+
{
|
|
3821
|
+
maxSheets: 3,
|
|
3822
|
+
maxColumns: 20,
|
|
3823
|
+
sampleRows: SAMPLE_ROWS,
|
|
3824
|
+
sampleCell: 40,
|
|
3825
|
+
withTop: true,
|
|
3826
|
+
note: "sheets, columns"
|
|
3827
|
+
},
|
|
3828
|
+
{
|
|
3829
|
+
maxSheets: 3,
|
|
3830
|
+
maxColumns: 20,
|
|
3831
|
+
sampleRows: 3,
|
|
3832
|
+
sampleCell: 16,
|
|
3833
|
+
withTop: true,
|
|
3834
|
+
note: "sheets, columns, sample"
|
|
3835
|
+
},
|
|
3836
|
+
{
|
|
3837
|
+
maxSheets: 2,
|
|
3838
|
+
maxColumns: 12,
|
|
3839
|
+
sampleRows: 2,
|
|
3840
|
+
sampleCell: 12,
|
|
3841
|
+
withTop: false,
|
|
3842
|
+
note: "sheets, columns, sample, top values"
|
|
3843
|
+
}
|
|
3844
|
+
];
|
|
3845
|
+
function columnLine(column, stats, withTop) {
|
|
3846
|
+
const parts = [
|
|
3847
|
+
`${column.letter} ${clip$2(column.header, 40)}`.trimEnd(),
|
|
3848
|
+
stats.type,
|
|
3849
|
+
`${stats.filled} filled`
|
|
3850
|
+
];
|
|
3851
|
+
parts.push(stats.distinct === void 0 ? `>${DISTINCT_CAP} distinct` : `${stats.distinct} distinct`);
|
|
3852
|
+
if (stats.min !== void 0 && stats.max !== void 0) parts.push(`min ${tidy(stats.min)}`, `max ${tidy(stats.max)}`, `sum ${tidy(stats.sum)}`);
|
|
3853
|
+
if (stats.earliest !== void 0 && stats.latest !== void 0) parts.push(`${stats.earliest} … ${stats.latest}`);
|
|
3854
|
+
if (withTop && stats.top.length > 0) parts.push(`top: ${stats.top.map(([value, count]) => `${clip$2(value, 24)} ×${count}`).join(", ")}`);
|
|
3855
|
+
return parts.join(" | ");
|
|
3856
|
+
}
|
|
3857
|
+
function sheetSection(view, analysis, rung, expanded) {
|
|
3858
|
+
const head = `Sheet ${view.sheetIndex + 1} "${view.sheetName}": ${analysis.dataRows} data rows × ${view.columns.length} columns` + (view.headerRow > 0 ? `, header at row ${view.headerRow}` : ", empty");
|
|
3859
|
+
if (!expanded || view.columns.length === 0) return [head];
|
|
3860
|
+
const lines = [head, "Columns:"];
|
|
3861
|
+
const shown = view.columns.slice(0, rung.maxColumns);
|
|
3862
|
+
for (const column of shown) lines.push(columnLine(column, analysis.columns[column.index], rung.withTop));
|
|
3863
|
+
if (view.columns.length > shown.length) lines.push(`+${view.columns.length - shown.length} more columns — call query_table with op "describe" and this sheet to list them`);
|
|
3864
|
+
const sample = analysis.sample.slice(0, rung.sampleRows);
|
|
3865
|
+
if (sample.length > 0) {
|
|
3866
|
+
lines.push(`Sample (sheet rows ${sample[0].row}–${sample[sample.length - 1].row}):`);
|
|
3867
|
+
for (const entry of sample) lines.push(shown.map((column) => clip$2((entry.cells[column.index] ?? "").trim(), rung.sampleCell)).join(" "));
|
|
3868
|
+
}
|
|
3869
|
+
return lines;
|
|
3870
|
+
}
|
|
3871
|
+
async function profileStreamingTable(input) {
|
|
3872
|
+
const { workbook } = input;
|
|
3873
|
+
const total = workbook.sheets.length;
|
|
3874
|
+
const only = input.sheet === void 0 ? void 0 : (await buildStreamingView(workbook, input.sheet, input.headerRow)).sheetIndex;
|
|
3875
|
+
const header = `--- Spreadsheet profile: ${input.name} (id: ${input.id}) · ${total} sheet${total === 1 ? "" : "s"} ---`;
|
|
3876
|
+
const views = [];
|
|
3877
|
+
const analyses = [];
|
|
3878
|
+
for (let index = 0; index < workbook.sheets.length; index += 1) {
|
|
3879
|
+
const view = await buildStreamingView(workbook, index + 1, only === index ? input.headerRow : void 0);
|
|
3880
|
+
views.push(view);
|
|
3881
|
+
analyses.push(await analyze(view));
|
|
3882
|
+
}
|
|
3883
|
+
let result = "";
|
|
3884
|
+
for (const rung of LADDER) {
|
|
3885
|
+
const lines = [header];
|
|
3886
|
+
let expandedCount = 0;
|
|
3887
|
+
for (const [index, view] of views.entries()) {
|
|
3888
|
+
const analysis = analyses[index];
|
|
3889
|
+
const expanded = only === void 0 ? expandedCount < rung.maxSheets : index === only;
|
|
3890
|
+
if (expanded && analysis.dataRows > 0) expandedCount += 1;
|
|
3891
|
+
lines.push(...sheetSection(view, analysis, rung, expanded && (only !== void 0 || analysis.dataRows > 0)));
|
|
3601
3892
|
}
|
|
3602
3893
|
if (rung.note !== void 0) lines.push(`Profile shortened to fit: ${rung.note}. Call query_table with op "describe" for the rest.`);
|
|
3603
3894
|
result = lines.join("\n");
|
|
@@ -3613,6 +3904,8 @@ const QUERY_LIMITS = {
|
|
|
3613
3904
|
maxRows: 500,
|
|
3614
3905
|
maxFilters: 20,
|
|
3615
3906
|
maxGroupBy: 3,
|
|
3907
|
+
maxPivotBy: 2,
|
|
3908
|
+
maxPivotColumns: 100,
|
|
3616
3909
|
maxMetrics: 8,
|
|
3617
3910
|
maxOrderBy: 3,
|
|
3618
3911
|
maxIn: 200,
|
|
@@ -3647,32 +3940,32 @@ const BUCKETS = [
|
|
|
3647
3940
|
"month",
|
|
3648
3941
|
"day"
|
|
3649
3942
|
];
|
|
3650
|
-
function fail(message) {
|
|
3943
|
+
function fail$1(message) {
|
|
3651
3944
|
throw new WebSkillError("VALIDATION_FAILED", message);
|
|
3652
3945
|
}
|
|
3653
3946
|
const isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
3654
3947
|
function list(v, what, max) {
|
|
3655
3948
|
if (v === void 0) return [];
|
|
3656
|
-
if (!Array.isArray(v)) fail(`"${what}" must be an array.`);
|
|
3657
|
-
if (v.length > max) fail(`"${what}" has ${v.length} entries; at most ${max} are allowed.`);
|
|
3949
|
+
if (!Array.isArray(v)) fail$1(`"${what}" must be an array.`);
|
|
3950
|
+
if (v.length > max) fail$1(`"${what}" has ${v.length} entries; at most ${max} are allowed.`);
|
|
3658
3951
|
return v;
|
|
3659
3952
|
}
|
|
3660
3953
|
function oneOf(v, allowed, what) {
|
|
3661
3954
|
if (typeof v === "string" && allowed.includes(v)) return v;
|
|
3662
|
-
return fail(`${what} must be one of: ${allowed.join(", ")}.`);
|
|
3955
|
+
return fail$1(`${what} must be one of: ${allowed.join(", ")}.`);
|
|
3663
3956
|
}
|
|
3664
3957
|
function noOtherKeys(obj, allowed, what) {
|
|
3665
3958
|
const extra = Object.keys(obj).filter((key) => !allowed.includes(key));
|
|
3666
|
-
if (extra.length > 0) fail(`${what} has unknown field${extra.length > 1 ? "s" : ""}: ${extra.join(", ")}.`);
|
|
3959
|
+
if (extra.length > 0) fail$1(`${what} has unknown field${extra.length > 1 ? "s" : ""}: ${extra.join(", ")}.`);
|
|
3667
3960
|
}
|
|
3668
3961
|
function scalar(v, what) {
|
|
3669
3962
|
if (typeof v === "string") return v;
|
|
3670
3963
|
if (typeof v === "number" && Number.isFinite(v)) return v;
|
|
3671
|
-
return fail(`${what} must be a string or a finite number.`);
|
|
3964
|
+
return fail$1(`${what} must be a string or a finite number.`);
|
|
3672
3965
|
}
|
|
3673
3966
|
function compileFilter(view, raw, index, uncomparable) {
|
|
3674
3967
|
const what = `where[${index}]`;
|
|
3675
|
-
if (!isObject(raw)) fail(`${what} must be an object.`);
|
|
3968
|
+
if (!isObject(raw)) fail$1(`${what} must be an object.`);
|
|
3676
3969
|
noOtherKeys(raw, [
|
|
3677
3970
|
"column",
|
|
3678
3971
|
"op",
|
|
@@ -3692,7 +3985,7 @@ function compileFilter(view, raw, index, uncomparable) {
|
|
|
3692
3985
|
};
|
|
3693
3986
|
if (op === "in") {
|
|
3694
3987
|
const values = raw["value"];
|
|
3695
|
-
if (!Array.isArray(values) || values.length === 0 || values.length > QUERY_LIMITS.maxIn) fail(`${what}.value must be a non-empty array of at most ${QUERY_LIMITS.maxIn} strings or numbers for op "in".`);
|
|
3988
|
+
if (!Array.isArray(values) || values.length === 0 || values.length > QUERY_LIMITS.maxIn) fail$1(`${what}.value must be a non-empty array of at most ${QUERY_LIMITS.maxIn} strings or numbers for op "in".`);
|
|
3696
3989
|
const texts = /* @__PURE__ */ new Set();
|
|
3697
3990
|
const numbers = /* @__PURE__ */ new Set();
|
|
3698
3991
|
for (const item of values) {
|
|
@@ -3708,7 +4001,7 @@ function compileFilter(view, raw, index, uncomparable) {
|
|
|
3708
4001
|
}
|
|
3709
4002
|
};
|
|
3710
4003
|
}
|
|
3711
|
-
if (raw["value"] === void 0) fail(`${what}.value is required for op "${op}".`);
|
|
4004
|
+
if (raw["value"] === void 0) fail$1(`${what}.value is required for op "${op}".`);
|
|
3712
4005
|
const value = String(scalar(raw["value"], `${what}.value`)).trim();
|
|
3713
4006
|
const valueKind = kindOf(value);
|
|
3714
4007
|
if (op === "contains") {
|
|
@@ -3730,7 +4023,7 @@ function compileFilter(view, raw, index, uncomparable) {
|
|
|
3730
4023
|
test: op === "eq" ? equal : (cells) => !equal(cells)
|
|
3731
4024
|
};
|
|
3732
4025
|
}
|
|
3733
|
-
if (valueKind !== "number" && valueKind !== "date") fail(`${what}.value must be a number or an ISO date (YYYY-MM-DD) for op "${op}", received "${value}".`);
|
|
4026
|
+
if (valueKind !== "number" && valueKind !== "date") fail$1(`${what}.value must be a number or an ISO date (YYYY-MM-DD) for op "${op}", received "${value}".`);
|
|
3734
4027
|
const numericValue = valueKind === "number" ? Number(value) : 0;
|
|
3735
4028
|
const satisfied = (order) => op === "gt" ? order > 0 : op === "gte" ? order >= 0 : op === "lt" ? order < 0 : order <= 0;
|
|
3736
4029
|
return {
|
|
@@ -3753,7 +4046,7 @@ function parseLimit(v) {
|
|
|
3753
4046
|
limit: QUERY_LIMITS.defaultRows,
|
|
3754
4047
|
clamped: false
|
|
3755
4048
|
};
|
|
3756
|
-
if (typeof v !== "number" || !Number.isInteger(v) || v < 1) fail("\"limit\" must be a positive integer.");
|
|
4049
|
+
if (typeof v !== "number" || !Number.isInteger(v) || v < 1) fail$1("\"limit\" must be a positive integer.");
|
|
3757
4050
|
return v > QUERY_LIMITS.maxRows ? {
|
|
3758
4051
|
limit: QUERY_LIMITS.maxRows,
|
|
3759
4052
|
clamped: true
|
|
@@ -3762,32 +4055,49 @@ function parseLimit(v) {
|
|
|
3762
4055
|
clamped: false
|
|
3763
4056
|
};
|
|
3764
4057
|
}
|
|
4058
|
+
function parseGroups(args, view, field) {
|
|
4059
|
+
const max = field === "groupBy" ? QUERY_LIMITS.maxGroupBy : QUERY_LIMITS.maxPivotBy;
|
|
4060
|
+
const groups = list(args[field], field, max).map((raw, i) => {
|
|
4061
|
+
if (!isObject(raw)) fail$1(`${field}[${i}] must be an object.`);
|
|
4062
|
+
noOtherKeys(raw, ["column", "bucket"], `${field}[${i}]`);
|
|
4063
|
+
const column = resolveColumn(view, raw["column"], `${field}[${i}].column`);
|
|
4064
|
+
return {
|
|
4065
|
+
column,
|
|
4066
|
+
bucket: raw["bucket"] === void 0 ? "value" : oneOf(raw["bucket"], BUCKETS, `${field}[${i}].bucket`),
|
|
4067
|
+
name: column.name
|
|
4068
|
+
};
|
|
4069
|
+
});
|
|
4070
|
+
if (new Set(groups.map((group) => group.column.index)).size !== groups.length) fail$1(`"${field}" names the same column twice.`);
|
|
4071
|
+
if (new Set(groups.map((group) => group.name)).size !== groups.length) for (const group of groups) group.name = group.column.label;
|
|
4072
|
+
return groups;
|
|
4073
|
+
}
|
|
3765
4074
|
function parseQuery(args, view) {
|
|
3766
4075
|
const op = oneOf(args["op"], ["aggregate", "rows"], "\"op\"");
|
|
3767
4076
|
const uncomparable = /* @__PURE__ */ new Map();
|
|
3768
4077
|
const filters = list(args["where"], "where", QUERY_LIMITS.maxFilters).map((raw, i) => compileFilter(view, raw, i, uncomparable));
|
|
3769
4078
|
const { limit, clamped } = parseLimit(args["limit"]);
|
|
3770
4079
|
const orderRaw = list(args["orderBy"], "orderBy", QUERY_LIMITS.maxOrderBy).map((raw, i) => {
|
|
3771
|
-
if (!isObject(raw)) fail(`orderBy[${i}] must be an object.`);
|
|
4080
|
+
if (!isObject(raw)) fail$1(`orderBy[${i}] must be an object.`);
|
|
3772
4081
|
noOtherKeys(raw, ["key", "desc"], `orderBy[${i}]`);
|
|
3773
|
-
if (typeof raw["key"] !== "string" || raw["key"].trim() === "") fail(`orderBy[${i}].key must be a string.`);
|
|
3774
|
-
if (raw["desc"] !== void 0 && typeof raw["desc"] !== "boolean") fail(`orderBy[${i}].desc must be a boolean.`);
|
|
4082
|
+
if (typeof raw["key"] !== "string" || raw["key"].trim() === "") fail$1(`orderBy[${i}].key must be a string.`);
|
|
4083
|
+
if (raw["desc"] !== void 0 && typeof raw["desc"] !== "boolean") fail$1(`orderBy[${i}].desc must be a boolean.`);
|
|
3775
4084
|
return {
|
|
3776
4085
|
key: raw["key"].trim(),
|
|
3777
4086
|
desc: raw["desc"] === true
|
|
3778
4087
|
};
|
|
3779
4088
|
});
|
|
3780
4089
|
if (op === "rows") {
|
|
3781
|
-
if (args["groupBy"] !== void 0 || args["metrics"] !== void 0) fail("\"groupBy\" and \"metrics\" belong to op \"aggregate\". Use op \"rows\" with \"columns\" to list rows.");
|
|
4090
|
+
if (args["groupBy"] !== void 0 || args["pivotBy"] !== void 0 || args["metrics"] !== void 0) fail$1("\"groupBy\", \"pivotBy\" and \"metrics\" belong to op \"aggregate\". Use op \"rows\" with \"columns\" to list rows.");
|
|
3782
4091
|
const columns = list(args["columns"], "columns", view.columns.length || 1).map((ref, i) => resolveColumn(view, ref, `columns[${i}]`));
|
|
3783
4092
|
for (const entry of orderRaw) resolveColumn(view, entry.key, "orderBy.key");
|
|
3784
4093
|
const offset = args["offset"];
|
|
3785
|
-
if (offset !== void 0 && (typeof offset !== "number" || !Number.isInteger(offset) || offset < 0)) fail("\"offset\" must be a non-negative integer.");
|
|
4094
|
+
if (offset !== void 0 && (typeof offset !== "number" || !Number.isInteger(offset) || offset < 0)) fail$1("\"offset\" must be a non-negative integer.");
|
|
3786
4095
|
return {
|
|
3787
4096
|
op,
|
|
3788
4097
|
filters,
|
|
3789
4098
|
uncomparable,
|
|
3790
4099
|
groups: [],
|
|
4100
|
+
pivot: [],
|
|
3791
4101
|
metrics: [],
|
|
3792
4102
|
columns: columns.length > 0 ? columns : [...view.columns],
|
|
3793
4103
|
order: orderRaw,
|
|
@@ -3796,31 +4106,22 @@ function parseQuery(args, view) {
|
|
|
3796
4106
|
offset: typeof offset === "number" ? offset : 0
|
|
3797
4107
|
};
|
|
3798
4108
|
}
|
|
3799
|
-
if (args["columns"] !== void 0 || args["offset"] !== void 0) fail("\"columns\" and \"offset\" belong to op \"rows\". Use op \"aggregate\" with \"groupBy\" and \"metrics\".");
|
|
3800
|
-
const groups =
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
const column = resolveColumn(view, raw["column"], `groupBy[${i}].column`);
|
|
3804
|
-
return {
|
|
3805
|
-
column,
|
|
3806
|
-
bucket: raw["bucket"] === void 0 ? "value" : oneOf(raw["bucket"], BUCKETS, `groupBy[${i}].bucket`),
|
|
3807
|
-
name: column.name
|
|
3808
|
-
};
|
|
3809
|
-
});
|
|
3810
|
-
if (new Set(groups.map((g) => g.column.index)).size !== groups.length) fail("\"groupBy\" names the same column twice.");
|
|
3811
|
-
if (new Set(groups.map((g) => g.name)).size !== groups.length) for (const g of groups) g.name = g.column.label;
|
|
4109
|
+
if (args["columns"] !== void 0 || args["offset"] !== void 0) fail$1("\"columns\" and \"offset\" belong to op \"rows\". Use op \"aggregate\" with \"groupBy\" and \"metrics\".");
|
|
4110
|
+
const groups = parseGroups(args, view, "groupBy");
|
|
4111
|
+
const pivot = parseGroups(args, view, "pivotBy");
|
|
4112
|
+
if (pivot.some((dimension) => groups.some((group) => group.column.index === dimension.column.index))) fail$1("A column cannot appear in both \"groupBy\" and \"pivotBy\".");
|
|
3812
4113
|
const metricsRaw = list(args["metrics"], "metrics", QUERY_LIMITS.maxMetrics);
|
|
3813
4114
|
const metrics = (metricsRaw.length > 0 ? metricsRaw : [{ fn: "count" }]).map((raw, i) => {
|
|
3814
|
-
if (!isObject(raw)) fail(`metrics[${i}] must be an object.`);
|
|
4115
|
+
if (!isObject(raw)) fail$1(`metrics[${i}] must be an object.`);
|
|
3815
4116
|
noOtherKeys(raw, [
|
|
3816
4117
|
"fn",
|
|
3817
4118
|
"column",
|
|
3818
4119
|
"as"
|
|
3819
4120
|
], `metrics[${i}]`);
|
|
3820
4121
|
const fn = oneOf(raw["fn"], METRIC_FNS, `metrics[${i}].fn`);
|
|
3821
|
-
if (raw["column"] === void 0 && fn !== "count") fail(`metrics[${i}].column is required for "${fn}".`);
|
|
4122
|
+
if (raw["column"] === void 0 && fn !== "count") fail$1(`metrics[${i}].column is required for "${fn}".`);
|
|
3822
4123
|
const column = raw["column"] === void 0 ? void 0 : resolveColumn(view, raw["column"], `metrics[${i}].column`);
|
|
3823
|
-
if (raw["as"] !== void 0 && (typeof raw["as"] !== "string" || raw["as"].trim() === "")) fail(`metrics[${i}].as must be a non-empty string.`);
|
|
4124
|
+
if (raw["as"] !== void 0 && (typeof raw["as"] !== "string" || raw["as"].trim() === "")) fail$1(`metrics[${i}].as must be a non-empty string.`);
|
|
3824
4125
|
const alias = typeof raw["as"] === "string" ? raw["as"].trim() : column === void 0 ? fn : `${fn}(${column.name})`;
|
|
3825
4126
|
return column === void 0 ? {
|
|
3826
4127
|
fn,
|
|
@@ -3831,18 +4132,19 @@ function parseQuery(args, view) {
|
|
|
3831
4132
|
alias
|
|
3832
4133
|
};
|
|
3833
4134
|
});
|
|
3834
|
-
if (new Set(metrics.map((m) => m.alias)).size !== metrics.length) fail("Two metrics have the same name; give one of them a distinct \"as\".");
|
|
4135
|
+
if (new Set(metrics.map((m) => m.alias)).size !== metrics.length) fail$1("Two metrics have the same name; give one of them a distinct \"as\".");
|
|
3835
4136
|
const available = () => [...metrics.map((m) => m.alias), ...groups.map((g) => g.name)].join(", ");
|
|
3836
4137
|
for (const entry of orderRaw) {
|
|
3837
4138
|
if (metrics.some((m) => m.alias === entry.key)) continue;
|
|
3838
4139
|
const column = resolveColumn(view, entry.key, "orderBy.key");
|
|
3839
|
-
if (!groups.some((g) => g.column.index === column.index)) fail(`orderBy.key "${entry.key}" is neither a metric nor a grouped column. Available: ${available()}.`);
|
|
4140
|
+
if (!groups.some((g) => g.column.index === column.index)) fail$1(`orderBy.key "${entry.key}" is neither a metric nor a grouped column. Available: ${available()}.`);
|
|
3840
4141
|
}
|
|
3841
4142
|
return {
|
|
3842
4143
|
op,
|
|
3843
4144
|
filters,
|
|
3844
4145
|
uncomparable,
|
|
3845
4146
|
groups,
|
|
4147
|
+
pivot,
|
|
3846
4148
|
metrics,
|
|
3847
4149
|
columns: [],
|
|
3848
4150
|
order: orderRaw,
|
|
@@ -3906,7 +4208,8 @@ function finish(metric, acc) {
|
|
|
3906
4208
|
return (metric.fn === "min" ? acc.minD : acc.maxD) ?? null;
|
|
3907
4209
|
}
|
|
3908
4210
|
}
|
|
3909
|
-
function runAggregate(view, query) {
|
|
4211
|
+
async function runAggregate(view, query, complete = false) {
|
|
4212
|
+
if (query.pivot.length > 0) return await runPivot(view, query, complete);
|
|
3910
4213
|
const { groups: groupSpecs, metrics } = query;
|
|
3911
4214
|
const skipped = new Map(metrics.map((m) => [m.alias, {
|
|
3912
4215
|
empty: 0,
|
|
@@ -3914,6 +4217,7 @@ function runAggregate(view, query) {
|
|
|
3914
4217
|
}]));
|
|
3915
4218
|
const unbucketed = /* @__PURE__ */ new Map();
|
|
3916
4219
|
const table = /* @__PURE__ */ new Map();
|
|
4220
|
+
let dataRows = 0;
|
|
3917
4221
|
let matched = 0;
|
|
3918
4222
|
const groupOf = (key) => {
|
|
3919
4223
|
const id = key.map((part) => part === null ? "\0" : part).join("");
|
|
@@ -3928,7 +4232,8 @@ function runAggregate(view, query) {
|
|
|
3928
4232
|
return group;
|
|
3929
4233
|
};
|
|
3930
4234
|
if (groupSpecs.length === 0) groupOf([]);
|
|
3931
|
-
for (const { cells } of view.data) {
|
|
4235
|
+
for await (const { cells } of view.data) {
|
|
4236
|
+
dataRows += 1;
|
|
3932
4237
|
if (!query.filters.every((filter) => filter.test(cells))) continue;
|
|
3933
4238
|
matched += 1;
|
|
3934
4239
|
const group = groupOf(groupSpecs.map((spec) => {
|
|
@@ -4001,7 +4306,10 @@ function runAggregate(view, query) {
|
|
|
4001
4306
|
const out = rows.slice(0, query.limit).map((row) => ({
|
|
4002
4307
|
key: Object.fromEntries(row.key.map((part, i) => {
|
|
4003
4308
|
if (part === null) return [names[i], null];
|
|
4004
|
-
const cut =
|
|
4309
|
+
const cut = complete ? {
|
|
4310
|
+
text: part,
|
|
4311
|
+
clipped: false
|
|
4312
|
+
} : clip$1(part);
|
|
4005
4313
|
if (cut.clipped) cellsTruncated = true;
|
|
4006
4314
|
return [names[i], cut.text];
|
|
4007
4315
|
})),
|
|
@@ -4016,6 +4324,7 @@ function runAggregate(view, query) {
|
|
|
4016
4324
|
};
|
|
4017
4325
|
}
|
|
4018
4326
|
return {
|
|
4327
|
+
dataRows,
|
|
4019
4328
|
matchedRows: matched,
|
|
4020
4329
|
uncomparable: Object.fromEntries(query.uncomparable),
|
|
4021
4330
|
groups: out,
|
|
@@ -4025,8 +4334,194 @@ function runAggregate(view, query) {
|
|
|
4025
4334
|
cellsTruncated
|
|
4026
4335
|
};
|
|
4027
4336
|
}
|
|
4028
|
-
function
|
|
4029
|
-
const
|
|
4337
|
+
async function runPivot(view, query, complete) {
|
|
4338
|
+
const { groups: rowSpecs, pivot: columnSpecs, metrics } = query;
|
|
4339
|
+
const skipped = new Map(metrics.map((metric) => [metric.alias, {
|
|
4340
|
+
empty: 0,
|
|
4341
|
+
nonNumeric: 0
|
|
4342
|
+
}]));
|
|
4343
|
+
const unbucketed = /* @__PURE__ */ new Map();
|
|
4344
|
+
const table = /* @__PURE__ */ new Map();
|
|
4345
|
+
const columnKeys = /* @__PURE__ */ new Map();
|
|
4346
|
+
const labels = /* @__PURE__ */ new Map();
|
|
4347
|
+
const uniqueLabels = /* @__PURE__ */ new Set();
|
|
4348
|
+
let dataRows = 0;
|
|
4349
|
+
let matched = 0;
|
|
4350
|
+
let cellsTruncated = false;
|
|
4351
|
+
const keyOf = (specs, cells) => specs.map((spec) => {
|
|
4352
|
+
const text = (cells[spec.column.index] ?? "").trim();
|
|
4353
|
+
if (text === "") return null;
|
|
4354
|
+
if (spec.bucket === "value") return text;
|
|
4355
|
+
if (kindOf(text) === "date") return text.slice(0, spec.bucket === "year" ? 4 : spec.bucket === "month" ? 7 : 10);
|
|
4356
|
+
unbucketed.set(spec.name, (unbucketed.get(spec.name) ?? 0) + 1);
|
|
4357
|
+
return null;
|
|
4358
|
+
});
|
|
4359
|
+
const idOf = (key) => key.map((part) => part === null ? "\0" : part).join("");
|
|
4360
|
+
const add = (metric, acc, cells, skip) => {
|
|
4361
|
+
if (metric.column === void 0) {
|
|
4362
|
+
acc.rows += 1;
|
|
4363
|
+
return;
|
|
4364
|
+
}
|
|
4365
|
+
const text = (cells[metric.column.index] ?? "").trim();
|
|
4366
|
+
if (metric.fn === "count") {
|
|
4367
|
+
if (text !== "") acc.rows += 1;
|
|
4368
|
+
return;
|
|
4369
|
+
}
|
|
4370
|
+
if (metric.fn === "countDistinct") {
|
|
4371
|
+
if (text !== "") acc.distinct.add(text);
|
|
4372
|
+
return;
|
|
4373
|
+
}
|
|
4374
|
+
const kind = kindOf(text);
|
|
4375
|
+
if (kind === "empty") {
|
|
4376
|
+
if (skip !== void 0) skip.empty += 1;
|
|
4377
|
+
} else if (kind === "number") {
|
|
4378
|
+
const value = Number(text);
|
|
4379
|
+
acc.numbers += 1;
|
|
4380
|
+
if (metric.fn === "sum" || metric.fn === "avg") acc.sum.add(value);
|
|
4381
|
+
else if (metric.fn === "median") acc.values.push(value);
|
|
4382
|
+
else if (metric.fn === "min") acc.minN = acc.minN === void 0 ? value : Math.min(acc.minN, value);
|
|
4383
|
+
else acc.maxN = acc.maxN === void 0 ? value : Math.max(acc.maxN, value);
|
|
4384
|
+
} else if (kind === "date" && (metric.fn === "min" || metric.fn === "max")) {
|
|
4385
|
+
acc.dates += 1;
|
|
4386
|
+
if (metric.fn === "min") acc.minD = acc.minD === void 0 ? text : acc.minD < text ? acc.minD : text;
|
|
4387
|
+
else acc.maxD = acc.maxD === void 0 ? text : acc.maxD > text ? acc.maxD : text;
|
|
4388
|
+
} else if (skip !== void 0) skip.nonNumeric += 1;
|
|
4389
|
+
};
|
|
4390
|
+
for await (const { cells } of view.data) {
|
|
4391
|
+
dataRows += 1;
|
|
4392
|
+
if (!query.filters.every((filter) => filter.test(cells))) continue;
|
|
4393
|
+
matched += 1;
|
|
4394
|
+
const rowKey = keyOf(rowSpecs, cells);
|
|
4395
|
+
const columnKey = keyOf(columnSpecs, cells);
|
|
4396
|
+
const rowId = idOf(rowKey);
|
|
4397
|
+
const columnId = idOf(columnKey);
|
|
4398
|
+
let group = table.get(rowId);
|
|
4399
|
+
if (group === void 0) {
|
|
4400
|
+
group = {
|
|
4401
|
+
key: rowKey,
|
|
4402
|
+
totals: metrics.map(newAcc),
|
|
4403
|
+
cells: /* @__PURE__ */ new Map()
|
|
4404
|
+
};
|
|
4405
|
+
table.set(rowId, group);
|
|
4406
|
+
}
|
|
4407
|
+
if (!columnKeys.has(columnId)) {
|
|
4408
|
+
columnKeys.set(columnId, columnKey);
|
|
4409
|
+
const base = columnKey.map((part) => part ?? "(empty)").join(" / ");
|
|
4410
|
+
let label = base;
|
|
4411
|
+
let suffix = 2;
|
|
4412
|
+
while (uniqueLabels.has(label)) label = `${base} (#${suffix++})`;
|
|
4413
|
+
labels.set(columnId, label);
|
|
4414
|
+
uniqueLabels.add(label);
|
|
4415
|
+
}
|
|
4416
|
+
let cellAccs = group.cells.get(columnId);
|
|
4417
|
+
if (cellAccs === void 0) {
|
|
4418
|
+
cellAccs = metrics.map(newAcc);
|
|
4419
|
+
group.cells.set(columnId, cellAccs);
|
|
4420
|
+
}
|
|
4421
|
+
for (const [index, metric] of metrics.entries()) {
|
|
4422
|
+
add(metric, group.totals[index], cells, skipped.get(metric.alias));
|
|
4423
|
+
add(metric, cellAccs[index], cells);
|
|
4424
|
+
}
|
|
4425
|
+
}
|
|
4426
|
+
const usedLabels = /* @__PURE__ */ new Set();
|
|
4427
|
+
const columnLabel = (base, alias) => {
|
|
4428
|
+
const clipped = complete ? {
|
|
4429
|
+
text: `${base} | ${alias}`,
|
|
4430
|
+
clipped: false
|
|
4431
|
+
} : clip$1(`${base} | ${alias}`);
|
|
4432
|
+
if (clipped.clipped) cellsTruncated = true;
|
|
4433
|
+
let label = clipped.text;
|
|
4434
|
+
let suffix = 2;
|
|
4435
|
+
while (usedLabels.has(label)) label = `${clipped.text.slice(0, 190)} (#${suffix++})`;
|
|
4436
|
+
usedLabels.add(label);
|
|
4437
|
+
return label;
|
|
4438
|
+
};
|
|
4439
|
+
const descriptors = [...columnKeys.keys()].map((id) => ({
|
|
4440
|
+
id,
|
|
4441
|
+
label: labels.get(id)
|
|
4442
|
+
}));
|
|
4443
|
+
const shown = complete ? descriptors : descriptors.slice(0, QUERY_LIMITS.maxPivotColumns);
|
|
4444
|
+
const dynamicColumns = shown.flatMap(({ label }) => metrics.map((metric) => columnLabel(label, metric.alias)));
|
|
4445
|
+
const totalColumns = metrics.map((metric) => columnLabel("Total", metric.alias));
|
|
4446
|
+
const outputColumns = [...dynamicColumns, ...totalColumns];
|
|
4447
|
+
const ordered = [...table.values()].map((group) => ({
|
|
4448
|
+
key: group.key,
|
|
4449
|
+
values: Object.fromEntries(metrics.map((metric, index) => [totalColumns[index], finish(metric, group.totals[index])]))
|
|
4450
|
+
}));
|
|
4451
|
+
const keyIndexOf = (key) => {
|
|
4452
|
+
const column = metrics.some((metric) => metric.alias === key) ? void 0 : resolveColumn(view, key, "orderBy.key");
|
|
4453
|
+
return column === void 0 ? -1 : rowSpecs.findIndex((spec) => spec.column.index === column.index);
|
|
4454
|
+
};
|
|
4455
|
+
const comparators = [];
|
|
4456
|
+
for (const entry of query.order) {
|
|
4457
|
+
const metric = metrics.find((candidate) => candidate.alias === entry.key);
|
|
4458
|
+
const metricIndex = metric === void 0 ? -1 : metrics.indexOf(metric);
|
|
4459
|
+
const groupIndex = metricIndex === -1 ? keyIndexOf(entry.key) : -1;
|
|
4460
|
+
comparators.push((a, b) => metricIndex === -1 ? compareNullable(a.key[groupIndex] ?? null, b.key[groupIndex] ?? null, entry.desc) : compareNullable(a.values[totalColumns[metricIndex]] ?? null, b.values[totalColumns[metricIndex]] ?? null, entry.desc));
|
|
4461
|
+
}
|
|
4462
|
+
if (comparators.length === 0 && rowSpecs.length > 0) {
|
|
4463
|
+
comparators.push((a, b) => compareNullable(a.values[totalColumns[0]] ?? null, b.values[totalColumns[0]] ?? null, true));
|
|
4464
|
+
for (let index = 0; index < rowSpecs.length; index += 1) comparators.push((a, b) => compareNullable(a.key[index] ?? null, b.key[index] ?? null, false));
|
|
4465
|
+
}
|
|
4466
|
+
ordered.sort((a, b) => {
|
|
4467
|
+
for (const compare of comparators) {
|
|
4468
|
+
const order = compare(a, b);
|
|
4469
|
+
if (order !== 0) return order;
|
|
4470
|
+
}
|
|
4471
|
+
return 0;
|
|
4472
|
+
});
|
|
4473
|
+
const names = rowSpecs.map((spec) => spec.name);
|
|
4474
|
+
const groups = ordered.slice(0, query.limit).map((entry) => {
|
|
4475
|
+
const original = table.get(idOf(entry.key));
|
|
4476
|
+
const values = {};
|
|
4477
|
+
for (const [index, metric] of metrics.entries()) {
|
|
4478
|
+
for (const [columnIndex, { id }] of shown.entries()) {
|
|
4479
|
+
const accs = original.cells.get(id);
|
|
4480
|
+
values[dynamicColumns[columnIndex * metrics.length + index]] = accs === void 0 ? null : finish(metric, accs[index]);
|
|
4481
|
+
}
|
|
4482
|
+
values[totalColumns[index]] = finish(metric, original.totals[index]);
|
|
4483
|
+
}
|
|
4484
|
+
return {
|
|
4485
|
+
key: Object.fromEntries(entry.key.map((part, index) => {
|
|
4486
|
+
if (part === null) return [names[index], null];
|
|
4487
|
+
const cut = complete ? {
|
|
4488
|
+
text: part,
|
|
4489
|
+
clipped: false
|
|
4490
|
+
} : clip$1(part);
|
|
4491
|
+
if (cut.clipped) cellsTruncated = true;
|
|
4492
|
+
return [names[index], cut.text];
|
|
4493
|
+
})),
|
|
4494
|
+
values
|
|
4495
|
+
};
|
|
4496
|
+
});
|
|
4497
|
+
const skippedOut = {};
|
|
4498
|
+
for (const [alias, { empty, nonNumeric }] of skipped) {
|
|
4499
|
+
if (empty === 0 && nonNumeric === 0) continue;
|
|
4500
|
+
skippedOut[alias] = {
|
|
4501
|
+
...empty > 0 ? { empty } : {},
|
|
4502
|
+
...nonNumeric > 0 ? { nonNumeric } : {}
|
|
4503
|
+
};
|
|
4504
|
+
}
|
|
4505
|
+
return {
|
|
4506
|
+
dataRows,
|
|
4507
|
+
matchedRows: matched,
|
|
4508
|
+
uncomparable: Object.fromEntries(query.uncomparable),
|
|
4509
|
+
groups,
|
|
4510
|
+
totalGroups: table.size,
|
|
4511
|
+
columns: outputColumns,
|
|
4512
|
+
...complete || descriptors.length <= QUERY_LIMITS.maxPivotColumns ? {} : { pivotColumnsTruncated: true },
|
|
4513
|
+
...Object.keys(skippedOut).length > 0 ? { skipped: skippedOut } : {},
|
|
4514
|
+
...unbucketed.size > 0 ? { unbucketed: Object.fromEntries(unbucketed) } : {},
|
|
4515
|
+
cellsTruncated
|
|
4516
|
+
};
|
|
4517
|
+
}
|
|
4518
|
+
async function runRows(view, query, complete = false) {
|
|
4519
|
+
const matched = [];
|
|
4520
|
+
let dataRows = 0;
|
|
4521
|
+
for await (const entry of view.data) {
|
|
4522
|
+
dataRows += 1;
|
|
4523
|
+
if (query.filters.every((filter) => filter.test(entry.cells))) matched.push(entry);
|
|
4524
|
+
}
|
|
4030
4525
|
if (query.order.length > 0) {
|
|
4031
4526
|
const keys = query.order.map((entry) => ({
|
|
4032
4527
|
index: resolveColumn(view, entry.key, "orderBy.key").index,
|
|
@@ -4048,12 +4543,17 @@ function runRows(view, query) {
|
|
|
4048
4543
|
const rows = matched.slice(query.offset, query.offset + query.limit).map((entry) => ({
|
|
4049
4544
|
row: entry.row,
|
|
4050
4545
|
cells: query.columns.map((column) => {
|
|
4051
|
-
const
|
|
4546
|
+
const raw = (entry.cells[column.index] ?? "").trim();
|
|
4547
|
+
const cut = complete ? {
|
|
4548
|
+
text: raw,
|
|
4549
|
+
clipped: false
|
|
4550
|
+
} : clip$1(raw);
|
|
4052
4551
|
if (cut.clipped) cellsTruncated = true;
|
|
4053
4552
|
return cut.text;
|
|
4054
4553
|
})
|
|
4055
4554
|
}));
|
|
4056
4555
|
return {
|
|
4556
|
+
dataRows,
|
|
4057
4557
|
matchedRows: matched.length,
|
|
4058
4558
|
uncomparable: Object.fromEntries(query.uncomparable),
|
|
4059
4559
|
rows,
|
|
@@ -4062,13 +4562,220 @@ function runRows(view, query) {
|
|
|
4062
4562
|
};
|
|
4063
4563
|
}
|
|
4064
4564
|
|
|
4565
|
+
//#endregion
|
|
4566
|
+
//#region ../agent/src/table/export.ts
|
|
4567
|
+
const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4568
|
+
function fail(message) {
|
|
4569
|
+
throw new WebSkillError("VALIDATION_FAILED", message);
|
|
4570
|
+
}
|
|
4571
|
+
function onlyKeys(value, allowed, path) {
|
|
4572
|
+
const extra = Object.keys(value).filter((key) => !allowed.includes(key));
|
|
4573
|
+
if (extra.length > 0) fail(`${path} has unsupported field(s): ${extra.join(", ")}.`);
|
|
4574
|
+
}
|
|
4575
|
+
function sheetName(value, path) {
|
|
4576
|
+
if (typeof value !== "string" || value.trim() === "") fail(`${path} must be a non-empty sheet name.`);
|
|
4577
|
+
return value.trim();
|
|
4578
|
+
}
|
|
4579
|
+
function literalRows(value, path) {
|
|
4580
|
+
if (!Array.isArray(value)) fail(`${path} must be an array of rows.`);
|
|
4581
|
+
return value.map((row, rowIndex) => {
|
|
4582
|
+
if (!Array.isArray(row)) fail(`${path}[${rowIndex}] must be an array of cells.`);
|
|
4583
|
+
return row.map((cell, columnIndex) => {
|
|
4584
|
+
if (cell !== null && ![
|
|
4585
|
+
"string",
|
|
4586
|
+
"number",
|
|
4587
|
+
"boolean"
|
|
4588
|
+
].includes(typeof cell)) fail(`${path}[${rowIndex}][${columnIndex}] must be a string, number, boolean or null.`);
|
|
4589
|
+
return cell;
|
|
4590
|
+
});
|
|
4591
|
+
});
|
|
4592
|
+
}
|
|
4593
|
+
function parseExportFile(raw) {
|
|
4594
|
+
if (raw === void 0) return void 0;
|
|
4595
|
+
if (!isRecord$1(raw)) fail("\"exportFile\" must be an object with a \"fileName\".");
|
|
4596
|
+
onlyKeys(raw, [
|
|
4597
|
+
"fileName",
|
|
4598
|
+
"sheetName",
|
|
4599
|
+
"additionalSheets"
|
|
4600
|
+
], "\"exportFile\"");
|
|
4601
|
+
const additionalRaw = raw["additionalSheets"];
|
|
4602
|
+
if (additionalRaw !== void 0 && (!Array.isArray(additionalRaw) || additionalRaw.length === 0)) fail("\"exportFile.additionalSheets\" must be a non-empty array when present.");
|
|
4603
|
+
const additionalSheets = (additionalRaw ?? []).map((entry, index) => {
|
|
4604
|
+
const path = `exportFile.additionalSheets[${index}]`;
|
|
4605
|
+
if (!isRecord$1(entry)) fail(`"${path}" must be an object.`);
|
|
4606
|
+
const name = sheetName(entry["name"], `"${path}.name"`);
|
|
4607
|
+
if ("query" in entry) {
|
|
4608
|
+
onlyKeys(entry, ["name", "query"], `"${path}"`);
|
|
4609
|
+
if (!isRecord$1(entry["query"])) fail(`"${path}.query" must be an aggregate or rows query object.`);
|
|
4610
|
+
return {
|
|
4611
|
+
name,
|
|
4612
|
+
query: entry["query"]
|
|
4613
|
+
};
|
|
4614
|
+
}
|
|
4615
|
+
onlyKeys(entry, [
|
|
4616
|
+
"name",
|
|
4617
|
+
"columns",
|
|
4618
|
+
"rows"
|
|
4619
|
+
], `"${path}"`);
|
|
4620
|
+
const columns = entry["columns"];
|
|
4621
|
+
if (columns !== void 0 && (!Array.isArray(columns) || columns.some((column) => typeof column !== "string"))) fail(`"${path}.columns" must be an array of header strings.`);
|
|
4622
|
+
return {
|
|
4623
|
+
name,
|
|
4624
|
+
...columns !== void 0 ? { columns } : {},
|
|
4625
|
+
rows: literalRows(entry["rows"], `${path}.rows`)
|
|
4626
|
+
};
|
|
4627
|
+
});
|
|
4628
|
+
return {
|
|
4629
|
+
fileName: resolveFileName$1(raw["fileName"], "query_table"),
|
|
4630
|
+
sheetName: raw["sheetName"] === void 0 ? "Query result" : sheetName(raw["sheetName"], "\"exportFile.sheetName\""),
|
|
4631
|
+
additionalSheets
|
|
4632
|
+
};
|
|
4633
|
+
}
|
|
4634
|
+
const CELL_SCHEMA = { anyOf: [
|
|
4635
|
+
{ type: "string" },
|
|
4636
|
+
{ type: "number" },
|
|
4637
|
+
{ type: "boolean" },
|
|
4638
|
+
{ type: "null" }
|
|
4639
|
+
] };
|
|
4640
|
+
function createExportFileSchema(queryProperties) {
|
|
4641
|
+
const nestedQueryProperties = Object.fromEntries(Object.entries(queryProperties).filter(([key]) => ![
|
|
4642
|
+
"id",
|
|
4643
|
+
"limit",
|
|
4644
|
+
"offset"
|
|
4645
|
+
].includes(key)).map(([key, value]) => key === "op" ? [key, {
|
|
4646
|
+
...value,
|
|
4647
|
+
enum: ["aggregate", "rows"],
|
|
4648
|
+
description: "The query operation for this sheet."
|
|
4649
|
+
}] : [key, value]));
|
|
4650
|
+
return {
|
|
4651
|
+
type: "object",
|
|
4652
|
+
description: "Write complete query results directly to one Excel file. Preview limits do not apply. The top-level query forms the primary sheet; additionalSheets can add more complete queries or small literal notes sheets.",
|
|
4653
|
+
properties: {
|
|
4654
|
+
fileName: {
|
|
4655
|
+
type: "string",
|
|
4656
|
+
description: "Workbook file name, for example \"contract-analysis.xlsx\"."
|
|
4657
|
+
},
|
|
4658
|
+
sheetName: {
|
|
4659
|
+
type: "string",
|
|
4660
|
+
description: "Name of the primary sheet produced by the top-level query. Defaults to \"Query result\"."
|
|
4661
|
+
},
|
|
4662
|
+
additionalSheets: {
|
|
4663
|
+
type: "array",
|
|
4664
|
+
minItems: 1,
|
|
4665
|
+
maxItems: SPREADSHEET_LIMITS.sheets - 1,
|
|
4666
|
+
description: "Additional sheets in the same workbook. Use {name, query} for complete query results. Use {name, columns, rows} only for a small notes or definitions sheet, never to copy large query results.",
|
|
4667
|
+
items: { anyOf: [{
|
|
4668
|
+
type: "object",
|
|
4669
|
+
properties: {
|
|
4670
|
+
name: {
|
|
4671
|
+
type: "string",
|
|
4672
|
+
description: "Output sheet name."
|
|
4673
|
+
},
|
|
4674
|
+
query: {
|
|
4675
|
+
type: "object",
|
|
4676
|
+
description: "A complete aggregate or rows query against the same workbook id. Every result row is written directly to this sheet without passing through the model. Preview limit and offset do not apply.",
|
|
4677
|
+
properties: nestedQueryProperties,
|
|
4678
|
+
required: ["op"],
|
|
4679
|
+
additionalProperties: false
|
|
4680
|
+
}
|
|
4681
|
+
},
|
|
4682
|
+
required: ["name", "query"],
|
|
4683
|
+
additionalProperties: false
|
|
4684
|
+
}, {
|
|
4685
|
+
type: "object",
|
|
4686
|
+
description: "A small literal notes or definitions sheet, not a way to transport large query results.",
|
|
4687
|
+
properties: {
|
|
4688
|
+
name: {
|
|
4689
|
+
type: "string",
|
|
4690
|
+
description: "Output sheet name."
|
|
4691
|
+
},
|
|
4692
|
+
columns: {
|
|
4693
|
+
type: "array",
|
|
4694
|
+
maxItems: SPREADSHEET_LIMITS.columnsPerSheet,
|
|
4695
|
+
items: { type: "string" }
|
|
4696
|
+
},
|
|
4697
|
+
rows: {
|
|
4698
|
+
type: "array",
|
|
4699
|
+
maxItems: SPREADSHEET_LIMITS.rowsPerSheet,
|
|
4700
|
+
items: {
|
|
4701
|
+
type: "array",
|
|
4702
|
+
maxItems: SPREADSHEET_LIMITS.columnsPerSheet,
|
|
4703
|
+
items: CELL_SCHEMA
|
|
4704
|
+
}
|
|
4705
|
+
}
|
|
4706
|
+
},
|
|
4707
|
+
required: ["name", "rows"],
|
|
4708
|
+
additionalProperties: false
|
|
4709
|
+
}] }
|
|
4710
|
+
}
|
|
4711
|
+
},
|
|
4712
|
+
required: ["fileName"],
|
|
4713
|
+
additionalProperties: false
|
|
4714
|
+
};
|
|
4715
|
+
}
|
|
4716
|
+
function sheetFromQuery(query, output, name) {
|
|
4717
|
+
if (output.groups !== void 0) {
|
|
4718
|
+
const valueColumns = query.pivot.length > 0 ? output.columns ?? [] : query.metrics.map((metric) => metric.alias);
|
|
4719
|
+
return {
|
|
4720
|
+
name,
|
|
4721
|
+
columns: [...query.groups.map((group) => group.name), ...valueColumns],
|
|
4722
|
+
rows: output.groups.map((group) => [...query.groups.map((dimension) => group.key[dimension.name] ?? null), ...valueColumns.map((column) => group.values[column] ?? null)])
|
|
4723
|
+
};
|
|
4724
|
+
}
|
|
4725
|
+
return {
|
|
4726
|
+
name,
|
|
4727
|
+
columns: output.columns ?? [],
|
|
4728
|
+
rows: (output.rows ?? []).map((row) => row.cells)
|
|
4729
|
+
};
|
|
4730
|
+
}
|
|
4731
|
+
async function exportQuerySheet(workbook, args, name) {
|
|
4732
|
+
const view = isStreamingTableWorkbook(workbook) ? await buildStreamingView(workbook, args["sheet"], args["headerRow"]) : buildView(workbook, args["sheet"], args["headerRow"]);
|
|
4733
|
+
const query = parseQuery(args, view);
|
|
4734
|
+
const computation = {
|
|
4735
|
+
...query,
|
|
4736
|
+
limit: Number.MAX_SAFE_INTEGER,
|
|
4737
|
+
offset: 0,
|
|
4738
|
+
limitClamped: false
|
|
4739
|
+
};
|
|
4740
|
+
const output = computation.op === "aggregate" ? await runAggregate(view, computation, true) : await runRows(view, computation, true);
|
|
4741
|
+
return {
|
|
4742
|
+
sheet: sheetFromQuery(query, output, name),
|
|
4743
|
+
matchedRows: output.matchedRows,
|
|
4744
|
+
...output.totalGroups !== void 0 ? { totalGroups: output.totalGroups } : {}
|
|
4745
|
+
};
|
|
4746
|
+
}
|
|
4747
|
+
function captureTableQueryArgs(args) {
|
|
4748
|
+
const exportFile = args["exportFile"];
|
|
4749
|
+
if (!isRecord$1(exportFile)) return args;
|
|
4750
|
+
const sheets = exportFile["additionalSheets"];
|
|
4751
|
+
if (!Array.isArray(sheets)) return args;
|
|
4752
|
+
let redacted = false;
|
|
4753
|
+
const captured = sheets.map((sheet) => {
|
|
4754
|
+
if (!isRecord$1(sheet) || !Array.isArray(sheet["rows"])) return sheet;
|
|
4755
|
+
redacted = true;
|
|
4756
|
+
const summary = {
|
|
4757
|
+
...sheet,
|
|
4758
|
+
rowCount: sheet["rows"].length
|
|
4759
|
+
};
|
|
4760
|
+
delete summary["rows"];
|
|
4761
|
+
return summary;
|
|
4762
|
+
});
|
|
4763
|
+
return redacted ? {
|
|
4764
|
+
...args,
|
|
4765
|
+
exportFile: {
|
|
4766
|
+
...exportFile,
|
|
4767
|
+
additionalSheets: captured
|
|
4768
|
+
}
|
|
4769
|
+
} : args;
|
|
4770
|
+
}
|
|
4771
|
+
|
|
4065
4772
|
//#endregion
|
|
4066
4773
|
//#region ../agent/src/table/toolSource.ts
|
|
4067
4774
|
/** @experimental */
|
|
4068
4775
|
const QUERY_TABLE_TOOL = "query_table";
|
|
4069
|
-
const DESCRIPTION$2 = "Run exact statistics over an Excel (.xlsx) workbook the user attached, without reading it row by row: describe its sheets and columns, aggregate (filter, group, count, distinct count, sum, average, min, max, median), or list matching rows. Every query covers all rows and is computed on the user’s device.";
|
|
4776
|
+
const DESCRIPTION$2 = "Run exact statistics over an Excel (.xlsx) workbook the user attached, without reading it row by row: describe its sheets and columns, aggregate (filter, group, pivot, count, distinct count, sum, average, min, max, median), or list matching rows. Every query covers all rows and is computed on the user’s device.";
|
|
4070
4777
|
const DESCRIPTION_WITH_DOWNLOADS = DESCRIPTION$2.replace("the user attached,", "the user attached or downloaded,");
|
|
4071
|
-
const SYSTEM_PROMPT$3 = "query_table computes over every row of an attached .xlsx workbook. Use it for any count, total, average, extreme, median, ranking or breakdown, and quote its numbers as returned — they are exact. Do not estimate statistics from sample rows, and do not page through a whole sheet with read_document to compute them. When a result reports skipped or uncomparable cells, tell the user how many and why. Attachment ids from earlier messages of this conversation stay valid for this tool.";
|
|
4778
|
+
const SYSTEM_PROMPT$3 = "query_table computes over every row of an attached .xlsx workbook. Use it for any count, total, average, extreme, median, ranking or breakdown, and quote its numbers as returned — they are exact. Do not estimate statistics from sample rows, and do not page through a whole sheet with read_document to compute them. For a multi-dimensional distinct count, put every row dimension in groupBy and use countDistinct directly. For a cross-tab, additionally put column dimensions in pivotBy; row totals are included. Do not delegate these single-scan queries to subagents. When a result reports skipped or uncomparable cells, tell the user how many and why. Attachment ids from earlier messages of this conversation stay valid for this tool.";
|
|
4072
4779
|
const SYSTEM_PROMPT_WITH_DOWNLOADS = `${SYSTEM_PROMPT$3} A downloaded workbook\u2019s id from list_downloaded_files works too.`;
|
|
4073
4780
|
const ID_DESCRIPTION = "The attachment id from the message that carried the workbook.";
|
|
4074
4781
|
const ID_DESCRIPTION_WITH_DOWNLOADS = `${ID_DESCRIPTION} For a downloaded file, use the id from list_downloaded_files; the user is asked for permission to read it.`;
|
|
@@ -4150,6 +4857,24 @@ const INPUT_SCHEMA$2 = {
|
|
|
4150
4857
|
additionalProperties: false
|
|
4151
4858
|
}
|
|
4152
4859
|
},
|
|
4860
|
+
pivotBy: {
|
|
4861
|
+
type: "array",
|
|
4862
|
+
maxItems: QUERY_LIMITS.maxPivotBy,
|
|
4863
|
+
description: "aggregate only. Pivot column dimensions. groupBy supplies the row dimensions and pivotBy supplies the column dimensions; every metric is computed for each cell and for the row total.",
|
|
4864
|
+
items: {
|
|
4865
|
+
type: "object",
|
|
4866
|
+
properties: {
|
|
4867
|
+
column: COLUMN,
|
|
4868
|
+
bucket: {
|
|
4869
|
+
type: "string",
|
|
4870
|
+
enum: [...BUCKETS],
|
|
4871
|
+
description: "\"value\" (default) groups by the cell text; year/month/day bucket ISO date cells."
|
|
4872
|
+
}
|
|
4873
|
+
},
|
|
4874
|
+
required: ["column"],
|
|
4875
|
+
additionalProperties: false
|
|
4876
|
+
}
|
|
4877
|
+
},
|
|
4153
4878
|
metrics: {
|
|
4154
4879
|
type: "array",
|
|
4155
4880
|
maxItems: QUERY_LIMITS.maxMetrics,
|
|
@@ -4193,7 +4918,7 @@ const INPUT_SCHEMA$2 = {
|
|
|
4193
4918
|
limit: {
|
|
4194
4919
|
type: "integer",
|
|
4195
4920
|
minimum: 1,
|
|
4196
|
-
description: `
|
|
4921
|
+
description: `Preview groups or rows to return. Default ${QUERY_LIMITS.defaultRows}, at most ${QUERY_LIMITS.maxRows}. Ignored when exportFile is present: exported files contain the complete query result.`
|
|
4197
4922
|
},
|
|
4198
4923
|
offset: {
|
|
4199
4924
|
type: "integer",
|
|
@@ -4238,10 +4963,17 @@ function fit(head, field, items, more) {
|
|
|
4238
4963
|
* @experimental
|
|
4239
4964
|
*/
|
|
4240
4965
|
function createTableQueryToolSource(options) {
|
|
4241
|
-
const
|
|
4966
|
+
const schema = options.writeArtifact === void 0 ? INPUT_SCHEMA$2 : {
|
|
4242
4967
|
...INPUT_SCHEMA$2,
|
|
4243
4968
|
properties: {
|
|
4244
4969
|
...INPUT_SCHEMA$2.properties,
|
|
4970
|
+
exportFile: createExportFileSchema(INPUT_SCHEMA$2.properties ?? {})
|
|
4971
|
+
}
|
|
4972
|
+
};
|
|
4973
|
+
const schemaWithDownloads = {
|
|
4974
|
+
...schema,
|
|
4975
|
+
properties: {
|
|
4976
|
+
...schema.properties,
|
|
4245
4977
|
id: {
|
|
4246
4978
|
type: "string",
|
|
4247
4979
|
description: ID_DESCRIPTION_WITH_DOWNLOADS
|
|
@@ -4249,6 +4981,7 @@ function createTableQueryToolSource(options) {
|
|
|
4249
4981
|
}
|
|
4250
4982
|
};
|
|
4251
4983
|
const downloads = () => options.downloadedFiles?.() === true;
|
|
4984
|
+
const exportPrompt = options.writeArtifact === void 0 ? "" : " When the user asks for an Excel file of query results, call this tool with exportFile so every group or matching row is written locally without passing through the model. Never copy a truncated preview into export_spreadsheet. For one workbook with multiple sheets, keep the primary query at the top level, name it with exportFile.sheetName, and put additional complete queries in exportFile.additionalSheets as {name, query}. A small notes sheet may use {name, columns, rows}. Do not page full results through the model or write one file per query when the user asked for one workbook.";
|
|
4252
4985
|
return {
|
|
4253
4986
|
kind: "table-query",
|
|
4254
4987
|
listToolSpecs: () => Promise.resolve([downloads() ? {
|
|
@@ -4258,11 +4991,12 @@ function createTableQueryToolSource(options) {
|
|
|
4258
4991
|
} : {
|
|
4259
4992
|
name: QUERY_TABLE_TOOL,
|
|
4260
4993
|
description: DESCRIPTION$2,
|
|
4261
|
-
inputSchema:
|
|
4994
|
+
inputSchema: schema
|
|
4262
4995
|
}]),
|
|
4263
|
-
systemPrompt: () => Promise.resolve(downloads() ? SYSTEM_PROMPT_WITH_DOWNLOADS : SYSTEM_PROMPT$3),
|
|
4996
|
+
systemPrompt: () => Promise.resolve((downloads() ? SYSTEM_PROMPT_WITH_DOWNLOADS : SYSTEM_PROMPT$3) + exportPrompt),
|
|
4264
4997
|
canHandle: (name) => name === QUERY_TABLE_TOOL,
|
|
4265
4998
|
argCaptureTrust: (name) => name === "query_table" ? { tier: "reviewed" } : void 0,
|
|
4999
|
+
captureArgs: (_name, args) => captureTableQueryArgs(args),
|
|
4266
5000
|
call: async (name, args, context) => {
|
|
4267
5001
|
try {
|
|
4268
5002
|
if (name !== "query_table") throw new WebSkillError("TOOL_NOT_FOUND", `Unknown tool ${name}.`);
|
|
@@ -4278,26 +5012,83 @@ function createTableQueryToolSource(options) {
|
|
|
4278
5012
|
const loaded = await options.tables.load(id, context);
|
|
4279
5013
|
if (loaded === void 0) throw new WebSkillError("TOOL_UNSUPPORTED", `No readable workbook with id "${id}". Use the id of an Excel file attached in this conversation; files from other conversations are not available.`);
|
|
4280
5014
|
const { workbook, name } = loaded;
|
|
4281
|
-
|
|
5015
|
+
const op = args["op"];
|
|
5016
|
+
const exportRequest = parseExportFile(args["exportFile"]);
|
|
5017
|
+
if (exportRequest !== void 0 && op === "describe") throw new WebSkillError("VALIDATION_FAILED", "\"exportFile\" applies to aggregate or rows, not describe.");
|
|
5018
|
+
if (exportRequest !== void 0 && options.writeArtifact === void 0) throw new WebSkillError("TOOL_UNSUPPORTED", "This host cannot write query results as Excel files.");
|
|
5019
|
+
const profileInput = {
|
|
5020
|
+
name,
|
|
5021
|
+
id,
|
|
5022
|
+
...args["sheet"] !== void 0 ? { sheet: args["sheet"] } : {},
|
|
5023
|
+
...args["headerRow"] !== void 0 ? { headerRow: args["headerRow"] } : {}
|
|
5024
|
+
};
|
|
5025
|
+
if (op === "describe") return {
|
|
4282
5026
|
ok: true,
|
|
4283
5027
|
content: [{
|
|
4284
5028
|
type: "text",
|
|
4285
|
-
text:
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
|
|
4289
|
-
...
|
|
4290
|
-
|
|
5029
|
+
text: isStreamingTableWorkbook(workbook) ? await profileStreamingTable({
|
|
5030
|
+
...profileInput,
|
|
5031
|
+
workbook
|
|
5032
|
+
}) : profileTable({
|
|
5033
|
+
...profileInput,
|
|
5034
|
+
workbook
|
|
4291
5035
|
})
|
|
4292
5036
|
}]
|
|
4293
5037
|
};
|
|
4294
|
-
|
|
5038
|
+
if (exportRequest !== void 0) {
|
|
5039
|
+
const main = await exportQuerySheet(workbook, args, exportRequest.sheetName);
|
|
5040
|
+
const spec = { sheets: [main.sheet] };
|
|
5041
|
+
const sheetSummaries = [{
|
|
5042
|
+
name: exportRequest.sheetName,
|
|
5043
|
+
rowCount: main.sheet.rows.length,
|
|
5044
|
+
matchedRows: main.matchedRows,
|
|
5045
|
+
...main.totalGroups !== void 0 ? { totalGroups: main.totalGroups } : {}
|
|
5046
|
+
}];
|
|
5047
|
+
for (const additional of exportRequest.additionalSheets) if ("query" in additional) {
|
|
5048
|
+
const result = await exportQuerySheet(workbook, additional.query, additional.name);
|
|
5049
|
+
spec.sheets.push(result.sheet);
|
|
5050
|
+
sheetSummaries.push({
|
|
5051
|
+
name: additional.name,
|
|
5052
|
+
rowCount: result.sheet.rows.length,
|
|
5053
|
+
matchedRows: result.matchedRows,
|
|
5054
|
+
...result.totalGroups !== void 0 ? { totalGroups: result.totalGroups } : {}
|
|
5055
|
+
});
|
|
5056
|
+
} else {
|
|
5057
|
+
spec.sheets.push({
|
|
5058
|
+
name: additional.name,
|
|
5059
|
+
...additional.columns !== void 0 ? { columns: additional.columns } : {},
|
|
5060
|
+
rows: additional.rows
|
|
5061
|
+
});
|
|
5062
|
+
sheetSummaries.push({
|
|
5063
|
+
name: additional.name,
|
|
5064
|
+
rowCount: additional.rows.length
|
|
5065
|
+
});
|
|
5066
|
+
}
|
|
5067
|
+
const bytes = await encodeSpreadsheet(spec);
|
|
5068
|
+
const artifact = await options.writeArtifact(exportRequest.fileName, bytes, SPREADSHEET_MIME_TYPE, context);
|
|
5069
|
+
return {
|
|
5070
|
+
ok: true,
|
|
5071
|
+
content: [{
|
|
5072
|
+
type: "json",
|
|
5073
|
+
data: {
|
|
5074
|
+
path: artifact.path,
|
|
5075
|
+
size: artifact.size,
|
|
5076
|
+
sheetCount: spec.sheets.length,
|
|
5077
|
+
rowCount: spec.sheets.reduce((total, sheet) => total + sheet.rows.length, 0),
|
|
5078
|
+
matchedRows: main.matchedRows,
|
|
5079
|
+
...main.totalGroups !== void 0 ? { totalGroups: main.totalGroups } : {},
|
|
5080
|
+
...exportRequest.additionalSheets.length > 0 ? { sheets: sheetSummaries } : {}
|
|
5081
|
+
}
|
|
5082
|
+
}]
|
|
5083
|
+
};
|
|
5084
|
+
}
|
|
5085
|
+
const view = isStreamingTableWorkbook(workbook) ? await buildStreamingView(workbook, args["sheet"], args["headerRow"]) : buildView(workbook, args["sheet"], args["headerRow"]);
|
|
4295
5086
|
const query = parseQuery(args, view);
|
|
4296
|
-
const output = query.op === "aggregate" ? runAggregate(view, query) : runRows(view, query);
|
|
5087
|
+
const output = query.op === "aggregate" ? await runAggregate(view, query, false) : await runRows(view, query, false);
|
|
4297
5088
|
const head = {
|
|
4298
5089
|
sheet: view.sheetName,
|
|
4299
5090
|
headerRow: view.headerRow,
|
|
4300
|
-
dataRows:
|
|
5091
|
+
dataRows: output.dataRows,
|
|
4301
5092
|
matchedRows: output.matchedRows,
|
|
4302
5093
|
...output.totalGroups !== void 0 ? { totalGroups: output.totalGroups } : {},
|
|
4303
5094
|
...output.columns !== void 0 ? { columns: output.columns } : {},
|
|
@@ -4305,10 +5096,11 @@ function createTableQueryToolSource(options) {
|
|
|
4305
5096
|
...output.unbucketed !== void 0 ? { unbucketed: output.unbucketed } : {},
|
|
4306
5097
|
...Object.keys(output.uncomparable).length > 0 ? { uncomparable: output.uncomparable } : {},
|
|
4307
5098
|
...output.cellsTruncated ? { cellsTruncated: true } : {},
|
|
5099
|
+
...output.pivotColumnsTruncated ? { pivotColumnsTruncated: true } : {},
|
|
4308
5100
|
...query.limitClamped ? { limitClamped: QUERY_LIMITS.maxRows } : {}
|
|
4309
5101
|
};
|
|
4310
5102
|
const items = output.groups ?? output.rows ?? [];
|
|
4311
|
-
const more = output.groups !== void 0 ? (output.totalGroups ?? 0) > output.groups.length : query.offset + items.length < output.matchedRows;
|
|
5103
|
+
const more = output.pivotColumnsTruncated === true || (output.groups !== void 0 ? (output.totalGroups ?? 0) > output.groups.length : query.offset + items.length < output.matchedRows);
|
|
4312
5104
|
return {
|
|
4313
5105
|
ok: true,
|
|
4314
5106
|
content: [{
|
|
@@ -4772,7 +5564,7 @@ function json(data) {
|
|
|
4772
5564
|
//#endregion
|
|
4773
5565
|
//#region ../agent/src/pageImage/toolSource.ts
|
|
4774
5566
|
const CAPTURE_PAGE_IMAGE_TOOL = "capture_page_image";
|
|
4775
|
-
const DESCRIPTION$1 = "Save a picture that is already on the page as a run artifact, so a skill can place it in a document.";
|
|
5567
|
+
const DESCRIPTION$1 = "Save a picture that is already on the page as a run artifact, so a skill can place it in a document. To see what a picture shows, use perceive_page with images: true instead.";
|
|
4776
5568
|
const INPUT_SCHEMA$1 = {
|
|
4777
5569
|
type: "object",
|
|
4778
5570
|
properties: {
|
|
@@ -4788,7 +5580,7 @@ const INPUT_SCHEMA$1 = {
|
|
|
4788
5580
|
required: ["ref"],
|
|
4789
5581
|
additionalProperties: false
|
|
4790
5582
|
};
|
|
4791
|
-
const SYSTEM_PROMPT$2 = "Use capture_page_image when a picture on the current page belongs in a document you are about to produce. It returns a reference, never the picture itself — pass that reference to the skill, do not try to read it back.
|
|
5583
|
+
const SYSTEM_PROMPT$2 = "Use capture_page_image when a picture on the current page belongs in a document you are about to produce. It returns a reference, never the picture itself — pass that reference to the skill, do not try to read it back. To see what a picture shows, call perceive_page with images: true; capturing here only saves it for a document and does not tell you what it shows. Capture one picture per call, and only for nodes the page perception marked as capturable. Do not list the captured references back to the user. They are working material for the document, not a deliverable; say how many pictures went in and leave it at that.";
|
|
4792
5584
|
const isFailure$1 = (result) => "reason" in result;
|
|
4793
5585
|
/**
|
|
4794
5586
|
* 页面取像工具(0.22.0 分册 12)。
|
|
@@ -5149,4 +5941,4 @@ function createDwgToolSource(options) {
|
|
|
5149
5941
|
}
|
|
5150
5942
|
|
|
5151
5943
|
//#endregion
|
|
5152
|
-
export { CAPTURE_PAGE_IMAGE_TOOL, CLOSE_PAGE_TAB_TOOL, DEFAULT_MAX_DATA_SOURCE_BYTES, DELEGATE_TASK_TOOL, DELEGATION_SYSTEM_PROMPT, DOCUMENT_READ_BUDGET, DOWNLOAD_WAIT_DEFAULT_MS, DOWNLOAD_WAIT_MAX_MS, DOWNLOAD_WAIT_MIN_MS, DWG_MODEL_VIEW_ID, DataSourceCandidatePolicy, DataSourcePolicy, DelegationOrchestrator, DownloadedFilePolicy, EMU_PER_PIXEL, EMU_PER_POINT, EMU_PER_TWIP, EXPORT_SPREADSHEET_TOOL, FETCH_REMOTE_IMAGE_TOOL, FILL_TEMPLATE_TOOL, FOCUS_PAGE_TAB_TOOL, GENERATE_SKILL_TOOL, INSPECT_TEMPLATE_TOOL, LIST_DOWNLOADED_FILES_TOOL, LIST_PAGE_TABS_TOOL, MANAGE_TODO_TOOL, MAX_DIGIT_WIDTH, PAGE_ACTION_KINDS, PAGE_ACTION_SYSTEM_PROMPT, PAGE_ACTION_TOOL, PAGE_DOCUMENT_REF, PDF_GARBLED_RATIO, PDF_LARGE_IMAGE_RATIO, PERCEIVE_PAGE_TOOL, PERCEPTION_PAGING_DEFAULTS, PERCEPTION_SYSTEM_PROMPT, PageActionPolicy, PagePerceptionPolicy, QUERY_TABLE_TOOL, READ_DOCUMENT_TOOL, READ_DOWNLOADED_FILE_TOOL, SCROLL_DIRECTIONS, SKILL_GENERATION_SYSTEM_PROMPT, SkillGenerator, TAB_WORKSET_DEFAULT_MAX, TAB_WORKSET_MAX_MAX, TAB_WORKSET_MIN_MAX, TAB_WORKSET_SYSTEM_PROMPT, TEMPLATE_INSPECT_BUDGET, TODO_SYSTEM_PROMPT, TabWorksetPolicy, TodoStore, VIEW_DWG_TOOL, asReadMode, clampDownloadWaitMs, clampWorksetMax, columnWidthToPixels, createDelegationToolSource, createDocumentToolSource, createDownloadedFileToolSource, createDwgToolSource, createHttpDataSourceTransport, createPageActionToolSource, createPageImageToolSource, createPagePerceptionToolSource, createRemoteImageToolSource, createSkillGenerationToolSource, createSpreadsheetToolSource, createTabWorksetToolSource, createTableQueryToolSource, createTemplateToolSource, createTodoToolSource, defersImage, dwgPickView, dwgViewsOf, fitWithin, formatCellAddress, frameLabel, frameSteps, isPdfTextTrustworthy, isRangeTopLeft, measurePdfTextQuality, parseCellAddress, parseCellRange, profileTable, rangeContains, readImageSize, sniffDocumentFormat, takesImage, toActionFrameScopes, toFrameScopes, withDelegationOrigin };
|
|
5944
|
+
export { CAPTURE_PAGE_IMAGE_TOOL, CLOSE_PAGE_TAB_TOOL, DEFAULT_MAX_DATA_SOURCE_BYTES, DELEGATE_TASK_TOOL, DELEGATION_SYSTEM_PROMPT, DOCUMENT_READ_BUDGET, DOWNLOAD_WAIT_DEFAULT_MS, DOWNLOAD_WAIT_MAX_MS, DOWNLOAD_WAIT_MIN_MS, DWG_MODEL_VIEW_ID, DataSourceCandidatePolicy, DataSourcePolicy, DelegationOrchestrator, DownloadedFilePolicy, EMU_PER_PIXEL, EMU_PER_POINT, EMU_PER_TWIP, EXPORT_SPREADSHEET_TOOL, FETCH_REMOTE_IMAGE_TOOL, FILL_TEMPLATE_TOOL, FOCUS_PAGE_TAB_TOOL, GENERATE_SKILL_TOOL, INSPECT_TEMPLATE_TOOL, LIST_DOWNLOADED_FILES_TOOL, LIST_PAGE_TABS_TOOL, MANAGE_TODO_TOOL, MAX_DIGIT_WIDTH, PAGE_ACTION_KINDS, PAGE_ACTION_SYSTEM_PROMPT, PAGE_ACTION_TOOL, PAGE_DOCUMENT_REF, PDF_GARBLED_RATIO, PDF_LARGE_IMAGE_RATIO, PERCEIVE_PAGE_TOOL, PERCEPTION_PAGING_DEFAULTS, PERCEPTION_SYSTEM_PROMPT, PageActionPolicy, PagePerceptionPolicy, QUERY_TABLE_TOOL, READ_DOCUMENT_TOOL, READ_DOWNLOADED_FILE_TOOL, SCROLL_DIRECTIONS, SKILL_GENERATION_SYSTEM_PROMPT, SkillGenerator, TAB_WORKSET_DEFAULT_MAX, TAB_WORKSET_MAX_MAX, TAB_WORKSET_MIN_MAX, TAB_WORKSET_SYSTEM_PROMPT, TEMPLATE_INSPECT_BUDGET, TODO_SYSTEM_PROMPT, TabWorksetPolicy, TodoStore, VIEW_DWG_TOOL, asReadMode, buildStreamingView, clampDownloadWaitMs, clampWorksetMax, columnWidthToPixels, createDelegationToolSource, createDocumentToolSource, createDownloadedFileToolSource, createDwgToolSource, createHttpDataSourceTransport, createPageActionToolSource, createPageImageToolSource, createPagePerceptionToolSource, createRemoteImageToolSource, createSkillGenerationToolSource, createSpreadsheetToolSource, createTabWorksetToolSource, createTableQueryToolSource, createTemplateToolSource, createTodoToolSource, defersImage, dwgPickView, dwgViewsOf, fitWithin, formatCellAddress, frameLabel, frameSteps, isPdfTextTrustworthy, isRangeTopLeft, isStreamingTableWorkbook, measurePdfTextQuality, parseCellAddress, parseCellRange, profileStreamingTable, profileTable, rangeContains, readImageSize, sniffDocumentFormat, summarizeStreamingWorkbook, takesImage, toActionFrameScopes, toFrameScopes, withDelegationOrigin };
|