@webskill/sdk 0.27.1 → 0.29.0
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 +901 -120
- package/dist/{approval-BSwffz8P.js → approval-DnUytrkq.js} +7 -2
- package/dist/browser.d.ts +12 -12
- package/dist/browser.js +527 -69
- package/dist/{geometry-BXMPNuM6.js → geometry-QmrkJw1_.js} +13 -2
- package/dist/governance.d.ts +3 -3
- package/dist/{index--GvzV6Lq.d.ts → index-B5zHZDNA.d.ts} +91 -6
- package/dist/{index-C8-9-8zo.d.ts → index-C3t1jKAs.d.ts} +41 -13
- package/dist/{index-D6eSrEi5.d.ts → index-Cngzg2WS.d.ts} +2 -2
- package/dist/index.d.ts +4 -4
- package/dist/index.js +12 -7
- package/dist/{linkedDocument-0iiVPd5t.js → linkedDocument-GE-VV_Gj.js} +112 -2
- 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 +19 -2
- 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-AiNsSSdO.d.ts} +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/{types-CpDRZ0rA-CFQ-vdKz.d.ts → types-wKp3YBSM-D7w-ZbsB.d.ts} +8 -3
- package/dist/ui-react.d.ts +3 -3
- package/dist/ui-react.js +5767 -793
- 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-TeWPPFDn.js} +1 -1
- package/dist/{webskillLitCatalog-BPyjYuRT.js → webskillLitCatalog-MfiLE5aq.js} +41 -2
- package/package.json +3 -1
- package/dist/kind-Dc8x0HWz.js +0 -110
package/dist/agent.js
CHANGED
|
@@ -2,9 +2,8 @@ import { n as messageOf, t as WebSkillError } from "./errors-BDZNpC13.js";
|
|
|
2
2
|
import { b as parseSkillMarkdown, y as isValidSkillName } from "./skill-WSUDp6A1.js";
|
|
3
3
|
import { t as assertRemoteUrlAllowed } from "./urlSafety-CiSuCJvX.js";
|
|
4
4
|
import { n as sanitizeUntrustedLine, t as UNTRUSTED_LINE_LIMIT } from "./untrustedText-BIaRvPZK.js";
|
|
5
|
-
import {
|
|
6
|
-
import { c as
|
|
7
|
-
import { A as frameSteps, C as DOCUMENT_READ_BUDGET, D as SCROLL_DIRECTIONS, E as toActionFrameScopes, O as toFrameScopes, S as sniffDocumentFormat, T as PAGE_DOCUMENT_REF, _ as defersImage, a as columnWidthToPixels, b as isPdfTextTrustworthy, c as isRangeTopLeft, d as rangeContains, f as readImageSize, g as asReadMode, h as createDocumentToolSource, i as MAX_DIGIT_WIDTH, k as frameLabel, l as parseCellAddress, m as READ_DOCUMENT_TOOL, n as EMU_PER_POINT, o as fitWithin, p as PDF_LARGE_IMAGE_RATIO, r as EMU_PER_TWIP, s as formatCellAddress, t as EMU_PER_PIXEL, u as parseCellRange, v as takesImage, w as PAGE_ACTION_KINDS, x as measurePdfTextQuality, y as PDF_GARBLED_RATIO } from "./geometry-BXMPNuM6.js";
|
|
5
|
+
import { A as extractedDocumentFormat, c as toBase64, d as SPREADSHEET_LIMITS, f as SPREADSHEET_MIME_TYPE, j as formatAttachmentText, k as classifyAttachment, l as encodeSpreadsheet, m as DEFAULT_MAX_DATA_SOURCE_BYTES, u as SPREADSHEET_EXTENSION } from "./linkedDocument-GE-VV_Gj.js";
|
|
6
|
+
import { A as frameSteps, C as DOCUMENT_READ_BUDGET, D as SCROLL_DIRECTIONS, E as toActionFrameScopes, O as toFrameScopes, S as sniffDocumentFormat, T as PAGE_DOCUMENT_REF, _ as defersImage, a as columnWidthToPixels, b as isPdfTextTrustworthy, c as isRangeTopLeft, d as rangeContains, f as readImageSize, g as asReadMode, h as createDocumentToolSource, i as MAX_DIGIT_WIDTH, k as frameLabel, l as parseCellAddress, m as READ_DOCUMENT_TOOL, n as EMU_PER_POINT, o as fitWithin, p as PDF_LARGE_IMAGE_RATIO, r as EMU_PER_TWIP, s as formatCellAddress, t as EMU_PER_PIXEL, u as parseCellRange, v as takesImage, w as PAGE_ACTION_KINDS, x as measurePdfTextQuality, y as PDF_GARBLED_RATIO } from "./geometry-QmrkJw1_.js";
|
|
8
7
|
|
|
9
8
|
//#region ../agent/src/todo/store.ts
|
|
10
9
|
const STATUSES = [
|
|
@@ -305,7 +304,7 @@ const SCRIPT_EXTENSIONS = [".ts", ".js"];
|
|
|
305
304
|
function defaultConfirmSkill(draft) {
|
|
306
305
|
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
306
|
}
|
|
308
|
-
function fail$
|
|
307
|
+
function fail$4(message, details) {
|
|
309
308
|
throw new WebSkillError("SKILL_GENERATION_VALIDATION_FAILED", message, details);
|
|
310
309
|
}
|
|
311
310
|
/**
|
|
@@ -313,21 +312,21 @@ function fail$2(message, details) {
|
|
|
313
312
|
* 再去打扰用户——不让用户确认一个注定会失败的技能。
|
|
314
313
|
*/
|
|
315
314
|
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$
|
|
315
|
+
if (!isValidSkillName(draft.name)) fail$4(`"${draft.name}" is not a valid skill name; use lowercase letters, digits and hyphens`);
|
|
316
|
+
if (draft.description.trim() === "") fail$4("The skill description must not be empty");
|
|
317
|
+
if (typeof draft.content !== "string" || draft.content.trim() === "") fail$4("The SKILL.md content must not be empty");
|
|
319
318
|
let metadata;
|
|
320
319
|
try {
|
|
321
320
|
metadata = parseSkillMarkdown(draft.content).metadata;
|
|
322
321
|
} catch (e) {
|
|
323
|
-
fail$
|
|
322
|
+
fail$4(`The SKILL.md content is not a valid skill document: ${e instanceof Error ? e.message : String(e)}`);
|
|
324
323
|
}
|
|
325
|
-
if (metadata.name !== draft.name) fail$
|
|
324
|
+
if (metadata.name !== draft.name) fail$4(`The SKILL.md frontmatter declares name "${metadata.name}" but the draft name is "${draft.name}"`);
|
|
326
325
|
for (const file of draft.files ?? []) {
|
|
327
326
|
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$
|
|
327
|
+
if (path.trim() === "") fail$4("Every attached file needs a path");
|
|
328
|
+
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`);
|
|
329
|
+
if (path.startsWith("scripts/") && !SCRIPT_EXTENSIONS.some((ext) => path.endsWith(ext))) fail$4(`The script "${path}" must end with .ts or .js`);
|
|
331
330
|
}
|
|
332
331
|
}
|
|
333
332
|
/** 确认面板正文:完整 SKILL.md + 附带文件,供用户逐字检查(FR-9.4 风险缓解) */
|
|
@@ -359,25 +358,25 @@ async function materializeBinaries(draft, options) {
|
|
|
359
358
|
const out = /* @__PURE__ */ new Map();
|
|
360
359
|
if (claimed.length === 0) return out;
|
|
361
360
|
const resolve = options.resolveAttachment;
|
|
362
|
-
if (resolve === void 0) fail$
|
|
361
|
+
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
362
|
const maxOne = options.policy?.maxBinaryBytes ?? DEFAULT_MAX_BINARY_BYTES;
|
|
364
363
|
const maxTotal = options.policy?.maxTotalBinaryBytes ?? DEFAULT_MAX_TOTAL_BINARY_BYTES;
|
|
365
364
|
let total = 0;
|
|
366
365
|
for (const file of claimed) {
|
|
367
366
|
const id = file.attachmentId;
|
|
368
367
|
const attachment = await resolve(id);
|
|
369
|
-
if (attachment === void 0) fail$
|
|
368
|
+
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
369
|
attachmentId: id,
|
|
371
370
|
path: file.path
|
|
372
371
|
});
|
|
373
372
|
const size = attachment.bytes.byteLength;
|
|
374
|
-
if (size > maxOne) fail$
|
|
373
|
+
if (size > maxOne) fail$4(`The attached file "${attachment.name}" is ${size} bytes, above the ${maxOne} byte limit for one file.`, {
|
|
375
374
|
path: file.path,
|
|
376
375
|
size,
|
|
377
376
|
limit: maxOne
|
|
378
377
|
});
|
|
379
378
|
total += size;
|
|
380
|
-
if (total > maxTotal) fail$
|
|
379
|
+
if (total > maxTotal) fail$4(`The attached files add up to ${total} bytes, above the ${maxTotal} byte limit for one skill.`, {
|
|
381
380
|
path: file.path,
|
|
382
381
|
total,
|
|
383
382
|
limit: maxTotal
|
|
@@ -404,7 +403,7 @@ function assertStepsAreBackedByTrace(draft, records) {
|
|
|
404
403
|
const executed = new Set(records.map((r) => r.tool));
|
|
405
404
|
const unknown = [...new Set(claimed.map((s) => s.tool).filter((tool) => !executed.has(tool)))];
|
|
406
405
|
if (unknown.length === 0) return;
|
|
407
|
-
fail$
|
|
406
|
+
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
407
|
claimed: unknown,
|
|
409
408
|
executed: [...executed].sort()
|
|
410
409
|
});
|
|
@@ -422,7 +421,7 @@ function assertStepsAreNotTruncated(draft, records) {
|
|
|
422
421
|
const usable = new Set(records.filter((r) => r.truncated !== true).map((r) => r.tool));
|
|
423
422
|
const truncated = [...new Set(claimed.map((s) => s.tool).filter((tool) => !usable.has(tool)))];
|
|
424
423
|
if (truncated.length === 0) return;
|
|
425
|
-
fail$
|
|
424
|
+
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
425
|
}
|
|
427
426
|
/**
|
|
428
427
|
* 确认卡证据(AC-30.12 / AC-30.13):逐条列出模型**写进技能的**步骤,
|
|
@@ -561,6 +560,7 @@ const SKILL_GENERATION_SYSTEM_PROMPT = [
|
|
|
561
560
|
" from inside SKILL.md as `skill:<the skill name>/<the path you gave it>`, for example",
|
|
562
561
|
" `skill:monthly-report/assets/template.xlsx`. Write that, never the attachment id of this",
|
|
563
562
|
" conversation — that id is gone by the time anyone uses the skill.",
|
|
563
|
+
"- 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
564
|
"- Never copy credentials, tokens or personal data into the skill.",
|
|
565
565
|
"- The user has to confirm every candidate, and the candidate stays unusable until a reviewer",
|
|
566
566
|
" approves it. Say so instead of promising the skill is ready."
|
|
@@ -1805,7 +1805,7 @@ const RECORD_LIMIT$1 = 50;
|
|
|
1805
1805
|
function sanitizeDescription(value) {
|
|
1806
1806
|
return sanitizeUntrustedLine(value, DESCRIPTION_LIMIT);
|
|
1807
1807
|
}
|
|
1808
|
-
function isRecord$
|
|
1808
|
+
function isRecord$3(value) {
|
|
1809
1809
|
return typeof value === "object" && value !== null;
|
|
1810
1810
|
}
|
|
1811
1811
|
/**
|
|
@@ -1815,7 +1815,7 @@ function isRecord$2(value) {
|
|
|
1815
1815
|
* 标成后者会让读代码的人以为它已经被验证过了。
|
|
1816
1816
|
*/
|
|
1817
1817
|
function validate(raw, provenance) {
|
|
1818
|
-
if (!isRecord$
|
|
1818
|
+
if (!isRecord$3(raw)) return void 0;
|
|
1819
1819
|
const { id, kind, description, target } = raw;
|
|
1820
1820
|
if (typeof id !== "string" || !ID_PATTERN.test(id)) return void 0;
|
|
1821
1821
|
if (typeof kind !== "string" || !OFFERABLE_KINDS.includes(kind)) return void 0;
|
|
@@ -2667,7 +2667,7 @@ function createTabWorksetToolSource(options) {
|
|
|
2667
2667
|
|
|
2668
2668
|
//#endregion
|
|
2669
2669
|
//#region ../agent/src/downloads/policy.ts
|
|
2670
|
-
const READABLE_TYPES = "Readable types: plain text, JSON, CSV, PDF, Word (.docx), Excel (.xlsx) and PNG/JPEG/WebP/GIF images.";
|
|
2670
|
+
const READABLE_TYPES = "Readable types: plain text, JSON, CSV, PDF, Word (.docx), Excel (.xlsx/.xls) and PNG/JPEG/WebP/GIF images.";
|
|
2671
2671
|
const IMAGE_UNREADABLE = "It is an image, and the selected model cannot read images. Switch model to read it.";
|
|
2672
2672
|
const RECORD_LIMIT = 50;
|
|
2673
2673
|
function formatSize(bytes) {
|
|
@@ -3094,7 +3094,7 @@ function readContent(result) {
|
|
|
3094
3094
|
//#region ../agent/src/spreadsheet/toolSource.ts
|
|
3095
3095
|
const EXPORT_SPREADSHEET_TOOL = "export_spreadsheet";
|
|
3096
3096
|
const DESCRIPTION$3 = "Write tabular data — and, when they belong in the sheet, pictures — to an Excel (.xlsx) file the user can download.";
|
|
3097
|
-
const CELL_SCHEMA = {
|
|
3097
|
+
const CELL_SCHEMA$1 = {
|
|
3098
3098
|
anyOf: [
|
|
3099
3099
|
{ type: "string" },
|
|
3100
3100
|
{ type: "number" },
|
|
@@ -3190,7 +3190,7 @@ function inputSchema(withImages) {
|
|
|
3190
3190
|
items: {
|
|
3191
3191
|
type: "array",
|
|
3192
3192
|
maxItems: SPREADSHEET_LIMITS.columnsPerSheet,
|
|
3193
|
-
items: CELL_SCHEMA
|
|
3193
|
+
items: CELL_SCHEMA$1
|
|
3194
3194
|
}
|
|
3195
3195
|
},
|
|
3196
3196
|
...withImages ? { images: IMAGES_SCHEMA } : {}
|
|
@@ -3220,7 +3220,7 @@ function toolError$2(e) {
|
|
|
3220
3220
|
}
|
|
3221
3221
|
};
|
|
3222
3222
|
}
|
|
3223
|
-
const isRecord$
|
|
3223
|
+
const isRecord$2 = (v) => typeof v === "object" && v !== null;
|
|
3224
3224
|
/**
|
|
3225
3225
|
* FR-10.3a 的两条分支。忘写后缀是模型的高频行为,为它打断一整轮不划算;
|
|
3226
3226
|
* 写成 `.csv` 则是**意图冲突**而不是疏漏,必须拒。
|
|
@@ -3228,15 +3228,15 @@ const isRecord$1 = (v) => typeof v === "object" && v !== null;
|
|
|
3228
3228
|
* 路径安全不在这里做:`writeArtifact` 那一条(`resolveInsideRoot`)是唯一的一份,
|
|
3229
3229
|
* FR-10.3b 明确禁止另写一份。
|
|
3230
3230
|
*/
|
|
3231
|
-
function resolveFileName$1(value) {
|
|
3232
|
-
if (typeof value !== "string" || value.trim() === "") throw new WebSkillError("VALIDATION_FAILED",
|
|
3231
|
+
function resolveFileName$1(value, toolName = "export_spreadsheet") {
|
|
3232
|
+
if (typeof value !== "string" || value.trim() === "") throw new WebSkillError("VALIDATION_FAILED", `${toolName} requires a non-empty "fileName".`);
|
|
3233
3233
|
const fileName = value.trim();
|
|
3234
3234
|
const lastSegment = fileName.split(/[\\/]/).pop() ?? "";
|
|
3235
3235
|
const dot = lastSegment.lastIndexOf(".");
|
|
3236
3236
|
const extension = dot <= 0 ? "" : lastSegment.slice(dot + 1);
|
|
3237
3237
|
if (extension === "") return `${fileName}${SPREADSHEET_EXTENSION}`;
|
|
3238
3238
|
if (extension.toLowerCase() === "xlsx") return fileName;
|
|
3239
|
-
throw new WebSkillError("VALIDATION_FAILED",
|
|
3239
|
+
throw new WebSkillError("VALIDATION_FAILED", `${toolName} writes .xlsx files; "${fileName}" asks for .${extension}. Drop the suffix or use .xlsx.`);
|
|
3240
3240
|
}
|
|
3241
3241
|
function summarize(spec) {
|
|
3242
3242
|
const sheets = spec.sheets;
|
|
@@ -3270,8 +3270,8 @@ function createSpreadsheetToolSource(options) {
|
|
|
3270
3270
|
canHandle: (name) => name === EXPORT_SPREADSHEET_TOOL,
|
|
3271
3271
|
captureArgs: (_name, args) => {
|
|
3272
3272
|
const spec = args["spec"];
|
|
3273
|
-
const sheets = isRecord$
|
|
3274
|
-
const rowCount = sheets.reduce((total, sheet) => total + (isRecord$
|
|
3273
|
+
const sheets = isRecord$2(spec) && Array.isArray(spec["sheets"]) ? spec["sheets"] : [];
|
|
3274
|
+
const rowCount = sheets.reduce((total, sheet) => total + (isRecord$2(sheet) && Array.isArray(sheet["rows"]) ? sheet["rows"].length : 0), 0);
|
|
3275
3275
|
return {
|
|
3276
3276
|
...typeof args["fileName"] === "string" ? { fileName: args["fileName"] } : {},
|
|
3277
3277
|
sheetCount: sheets.length,
|
|
@@ -3327,17 +3327,17 @@ function columnLetter(index) {
|
|
|
3327
3327
|
}
|
|
3328
3328
|
return out;
|
|
3329
3329
|
}
|
|
3330
|
-
const blank = (cells) => cells.every((cell) => cell.trim() === "");
|
|
3331
|
-
function fail$
|
|
3330
|
+
const blank$1 = (cells) => cells.every((cell) => cell.trim() === "");
|
|
3331
|
+
function fail$3(message) {
|
|
3332
3332
|
throw new WebSkillError("VALIDATION_FAILED", message);
|
|
3333
3333
|
}
|
|
3334
3334
|
/** 先按序号(1 基)、再按表名定位;缺省取第一张有内容的表 */
|
|
3335
3335
|
function resolveSheet(workbook, ref) {
|
|
3336
3336
|
const { sheets } = workbook;
|
|
3337
|
-
if (sheets.length === 0) fail$
|
|
3337
|
+
if (sheets.length === 0) fail$3("This workbook has no sheets.");
|
|
3338
3338
|
const names = () => sheets.map((sheet, i) => `${i + 1}: "${sheet.name}"`).join(", ");
|
|
3339
3339
|
if (ref === void 0) {
|
|
3340
|
-
const found = sheets.findIndex((sheet) => sheet.rows.some((row) => !blank(row)));
|
|
3340
|
+
const found = sheets.findIndex((sheet) => sheet.rows.some((row) => !blank$1(row)));
|
|
3341
3341
|
const index = found === -1 ? 0 : found;
|
|
3342
3342
|
return {
|
|
3343
3343
|
sheet: sheets[index],
|
|
@@ -3346,7 +3346,7 @@ function resolveSheet(workbook, ref) {
|
|
|
3346
3346
|
}
|
|
3347
3347
|
if (typeof ref === "number") {
|
|
3348
3348
|
const sheet = Number.isInteger(ref) ? sheets[ref - 1] : void 0;
|
|
3349
|
-
if (sheet === void 0) fail$
|
|
3349
|
+
if (sheet === void 0) fail$3(`There is no sheet number ${String(ref)}. Sheets: ${names()}.`);
|
|
3350
3350
|
return {
|
|
3351
3351
|
sheet,
|
|
3352
3352
|
index: ref - 1
|
|
@@ -3354,24 +3354,24 @@ function resolveSheet(workbook, ref) {
|
|
|
3354
3354
|
}
|
|
3355
3355
|
if (typeof ref === "string") {
|
|
3356
3356
|
const index = sheets.findIndex((sheet) => sheet.name === ref);
|
|
3357
|
-
if (index === -1) fail$
|
|
3357
|
+
if (index === -1) fail$3(`There is no sheet named "${ref}". Sheets: ${names()}.`);
|
|
3358
3358
|
return {
|
|
3359
3359
|
sheet: sheets[index],
|
|
3360
3360
|
index
|
|
3361
3361
|
};
|
|
3362
3362
|
}
|
|
3363
|
-
return fail$
|
|
3363
|
+
return fail$3("\"sheet\" must be a sheet name or a 1-based sheet number.");
|
|
3364
3364
|
}
|
|
3365
3365
|
/** 表头行(缺省为第一个有内容的行)+ 其后有内容的行;行号保持工作表的 1 基行号 */
|
|
3366
3366
|
function buildView(workbook, sheetRef, headerRowArg) {
|
|
3367
3367
|
const { sheet, index } = resolveSheet(workbook, sheetRef);
|
|
3368
3368
|
const rows = sheet.rows;
|
|
3369
3369
|
let headerIndex;
|
|
3370
|
-
if (headerRowArg === void 0) headerIndex = rows.findIndex((row) => !blank(row));
|
|
3370
|
+
if (headerRowArg === void 0) headerIndex = rows.findIndex((row) => !blank$1(row));
|
|
3371
3371
|
else {
|
|
3372
|
-
if (typeof headerRowArg !== "number" || !Number.isInteger(headerRowArg) || headerRowArg < 1) fail$
|
|
3372
|
+
if (typeof headerRowArg !== "number" || !Number.isInteger(headerRowArg) || headerRowArg < 1) fail$3("\"headerRow\" must be a positive integer (1-based worksheet row number).");
|
|
3373
3373
|
headerIndex = headerRowArg - 1;
|
|
3374
|
-
if (headerIndex >= rows.length) fail$
|
|
3374
|
+
if (headerIndex >= rows.length) fail$3(`"headerRow" ${headerRowArg} is past the last row (${rows.length}) of the sheet.`);
|
|
3375
3375
|
}
|
|
3376
3376
|
if (headerIndex === -1) return {
|
|
3377
3377
|
sheetName: sheet.name,
|
|
@@ -3385,7 +3385,7 @@ function buildView(workbook, sheetRef, headerRowArg) {
|
|
|
3385
3385
|
let width = header.length;
|
|
3386
3386
|
for (let i = headerIndex + 1; i < rows.length; i += 1) {
|
|
3387
3387
|
const cells = rows[i];
|
|
3388
|
-
if (blank(cells)) continue;
|
|
3388
|
+
if (blank$1(cells)) continue;
|
|
3389
3389
|
if (cells.length > width) width = cells.length;
|
|
3390
3390
|
data.push({
|
|
3391
3391
|
row: i + 1,
|
|
@@ -3418,16 +3418,16 @@ function listColumns(view) {
|
|
|
3418
3418
|
}
|
|
3419
3419
|
/** 表头文字优先,其次列字母;表头重名时拒绝按表头引用,并点出同名各列的列字母 */
|
|
3420
3420
|
function resolveColumn(view, ref, what) {
|
|
3421
|
-
if (typeof ref !== "string" || ref.trim() === "") fail$
|
|
3421
|
+
if (typeof ref !== "string" || ref.trim() === "") fail$3(`${what} must name a column (header text or column letter).`);
|
|
3422
3422
|
const text = ref.trim();
|
|
3423
3423
|
const byHeader = view.columns.filter((column) => column.header === text);
|
|
3424
3424
|
if (byHeader.length === 1) return byHeader[0];
|
|
3425
|
-
if (byHeader.length > 1) fail$
|
|
3425
|
+
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.`);
|
|
3426
3426
|
if (LETTER_RE.test(text)) {
|
|
3427
3427
|
const byLetter = view.columns.find((column) => column.letter === text.toUpperCase());
|
|
3428
3428
|
if (byLetter !== void 0) return byLetter;
|
|
3429
3429
|
}
|
|
3430
|
-
return fail$
|
|
3430
|
+
return fail$3(`${what}: no column "${text}" on sheet "${view.sheetName}". Columns: ${listColumns(view)}.`);
|
|
3431
3431
|
}
|
|
3432
3432
|
/** Neumaier 补偿求和:几万个小数相加不累积漂移 */
|
|
3433
3433
|
var Summer = class {
|
|
@@ -3447,9 +3447,9 @@ const tidy = (x) => Number(x.toPrecision(12));
|
|
|
3447
3447
|
|
|
3448
3448
|
//#endregion
|
|
3449
3449
|
//#region ../agent/src/table/profile.ts
|
|
3450
|
-
const PROFILE_MAX_CHARS = 8e3;
|
|
3451
|
-
const DISTINCT_CAP = 1e4;
|
|
3452
|
-
const SAMPLE_ROWS = 5;
|
|
3450
|
+
const PROFILE_MAX_CHARS$1 = 8e3;
|
|
3451
|
+
const DISTINCT_CAP$1 = 1e4;
|
|
3452
|
+
const SAMPLE_ROWS$1 = 5;
|
|
3453
3453
|
function statsOf(view, column) {
|
|
3454
3454
|
let filled = 0;
|
|
3455
3455
|
let numbers = 0;
|
|
@@ -3480,7 +3480,7 @@ function statsOf(view, column) {
|
|
|
3480
3480
|
} else texts += 1;
|
|
3481
3481
|
const seen = freq.get(text);
|
|
3482
3482
|
if (seen !== void 0) freq.set(text, seen + 1);
|
|
3483
|
-
else if (freq.size < DISTINCT_CAP) freq.set(text, 1);
|
|
3483
|
+
else if (freq.size < DISTINCT_CAP$1) freq.set(text, 1);
|
|
3484
3484
|
else capped = true;
|
|
3485
3485
|
}
|
|
3486
3486
|
let type;
|
|
@@ -3505,16 +3505,16 @@ function statsOf(view, column) {
|
|
|
3505
3505
|
top
|
|
3506
3506
|
};
|
|
3507
3507
|
}
|
|
3508
|
-
const clip$
|
|
3508
|
+
const clip$3 = (text, max) => {
|
|
3509
3509
|
const chars = [...text.replace(/[\r\n\t]+/g, " ")];
|
|
3510
3510
|
return chars.length <= max ? chars.join("") : `${chars.slice(0, max - 1).join("")}…`;
|
|
3511
3511
|
};
|
|
3512
3512
|
/** 超出字符预算时的收缩阶梯:先少表、再少列、再收样本、最后去掉 top */
|
|
3513
|
-
const LADDER = [
|
|
3513
|
+
const LADDER$1 = [
|
|
3514
3514
|
{
|
|
3515
3515
|
maxSheets: 8,
|
|
3516
3516
|
maxColumns: 40,
|
|
3517
|
-
sampleRows: SAMPLE_ROWS,
|
|
3517
|
+
sampleRows: SAMPLE_ROWS$1,
|
|
3518
3518
|
sampleCell: 40,
|
|
3519
3519
|
withTop: true,
|
|
3520
3520
|
note: void 0
|
|
@@ -3522,7 +3522,7 @@ const LADDER = [
|
|
|
3522
3522
|
{
|
|
3523
3523
|
maxSheets: 3,
|
|
3524
3524
|
maxColumns: 40,
|
|
3525
|
-
sampleRows: SAMPLE_ROWS,
|
|
3525
|
+
sampleRows: SAMPLE_ROWS$1,
|
|
3526
3526
|
sampleCell: 40,
|
|
3527
3527
|
withTop: true,
|
|
3528
3528
|
note: "sheets"
|
|
@@ -3530,7 +3530,7 @@ const LADDER = [
|
|
|
3530
3530
|
{
|
|
3531
3531
|
maxSheets: 3,
|
|
3532
3532
|
maxColumns: 20,
|
|
3533
|
-
sampleRows: SAMPLE_ROWS,
|
|
3533
|
+
sampleRows: SAMPLE_ROWS$1,
|
|
3534
3534
|
sampleCell: 40,
|
|
3535
3535
|
withTop: true,
|
|
3536
3536
|
note: "sheets, columns"
|
|
@@ -3552,29 +3552,29 @@ const LADDER = [
|
|
|
3552
3552
|
note: "sheets, columns, sample, top values"
|
|
3553
3553
|
}
|
|
3554
3554
|
];
|
|
3555
|
-
function columnLine(column, stats, withTop) {
|
|
3555
|
+
function columnLine$1(column, stats, withTop) {
|
|
3556
3556
|
const parts = [
|
|
3557
|
-
`${column.letter} ${clip$
|
|
3557
|
+
`${column.letter} ${clip$3(column.header, 40)}`.trimEnd(),
|
|
3558
3558
|
stats.type,
|
|
3559
3559
|
`${stats.filled} filled`
|
|
3560
3560
|
];
|
|
3561
|
-
parts.push(stats.distinct === void 0 ? `>${DISTINCT_CAP} distinct` : `${stats.distinct} distinct`);
|
|
3561
|
+
parts.push(stats.distinct === void 0 ? `>${DISTINCT_CAP$1} distinct` : `${stats.distinct} distinct`);
|
|
3562
3562
|
if (stats.min !== void 0 && stats.max !== void 0) parts.push(`min ${tidy(stats.min)}`, `max ${tidy(stats.max)}`, `sum ${tidy(stats.sum)}`);
|
|
3563
3563
|
if (stats.earliest !== void 0 && stats.latest !== void 0) parts.push(`${stats.earliest} … ${stats.latest}`);
|
|
3564
|
-
if (withTop && stats.top.length > 0) parts.push(`top: ${stats.top.map(([value, count]) => `${clip$
|
|
3564
|
+
if (withTop && stats.top.length > 0) parts.push(`top: ${stats.top.map(([value, count]) => `${clip$3(value, 24)} ×${count}`).join(", ")}`);
|
|
3565
3565
|
return parts.join(" | ");
|
|
3566
3566
|
}
|
|
3567
|
-
function sheetSection(view, rung, expanded, statsFor) {
|
|
3567
|
+
function sheetSection$1(view, rung, expanded, statsFor) {
|
|
3568
3568
|
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");
|
|
3569
3569
|
if (!expanded || view.columns.length === 0) return [head];
|
|
3570
3570
|
const lines = [head, "Columns:"];
|
|
3571
3571
|
const shown = view.columns.slice(0, rung.maxColumns);
|
|
3572
|
-
for (const column of shown) lines.push(columnLine(column, statsFor(column), rung.withTop));
|
|
3572
|
+
for (const column of shown) lines.push(columnLine$1(column, statsFor(column), rung.withTop));
|
|
3573
3573
|
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`);
|
|
3574
3574
|
const sample = view.data.slice(0, rung.sampleRows);
|
|
3575
3575
|
if (sample.length > 0) {
|
|
3576
3576
|
lines.push(`Sample (sheet rows ${sample[0].row}–${sample[sample.length - 1].row}):`);
|
|
3577
|
-
for (const { cells } of sample) lines.push(shown.map((column) => clip$
|
|
3577
|
+
for (const { cells } of sample) lines.push(shown.map((column) => clip$3((cells[column.index] ?? "").trim(), rung.sampleCell)).join(" "));
|
|
3578
3578
|
}
|
|
3579
3579
|
return lines;
|
|
3580
3580
|
}
|
|
@@ -3601,13 +3601,293 @@ function profileTable(input) {
|
|
|
3601
3601
|
return stats;
|
|
3602
3602
|
};
|
|
3603
3603
|
let result = "";
|
|
3604
|
-
for (const rung of LADDER) {
|
|
3604
|
+
for (const rung of LADDER$1) {
|
|
3605
3605
|
const lines = [header];
|
|
3606
3606
|
let expandedCount = 0;
|
|
3607
3607
|
for (const [index, view] of views.entries()) {
|
|
3608
3608
|
const expanded = only === void 0 ? expandedCount < rung.maxSheets : index === only;
|
|
3609
3609
|
if (expanded && view.data.length > 0) expandedCount += 1;
|
|
3610
|
-
lines.push(...sheetSection(view, rung, expanded && (only !== void 0 || view.data.length > 0), statsFor(view)));
|
|
3610
|
+
lines.push(...sheetSection$1(view, rung, expanded && (only !== void 0 || view.data.length > 0), statsFor(view)));
|
|
3611
|
+
}
|
|
3612
|
+
if (rung.note !== void 0) lines.push(`Profile shortened to fit: ${rung.note}. Call query_table with op "describe" for the rest.`);
|
|
3613
|
+
result = lines.join("\n");
|
|
3614
|
+
if (result.length <= PROFILE_MAX_CHARS$1) return result;
|
|
3615
|
+
}
|
|
3616
|
+
return `${result.slice(0, PROFILE_MAX_CHARS$1 - 80)}\nProfile cut at ${PROFILE_MAX_CHARS$1} characters.`;
|
|
3617
|
+
}
|
|
3618
|
+
|
|
3619
|
+
//#endregion
|
|
3620
|
+
//#region ../agent/src/table/streaming.ts
|
|
3621
|
+
function isStreamingTableWorkbook(workbook) {
|
|
3622
|
+
return "kind" in workbook && workbook.kind === "streaming";
|
|
3623
|
+
}
|
|
3624
|
+
const blank = (cells) => cells.every((cell) => cell.trim() === "");
|
|
3625
|
+
function fail$2(message) {
|
|
3626
|
+
throw new WebSkillError("VALIDATION_FAILED", message);
|
|
3627
|
+
}
|
|
3628
|
+
async function resolveStreamingSheet(workbook, ref) {
|
|
3629
|
+
const { sheets } = workbook;
|
|
3630
|
+
if (sheets.length === 0) fail$2("This workbook has no sheets.");
|
|
3631
|
+
const names = () => sheets.map((sheet, i) => `${i + 1}: "${sheet.name}"`).join(", ");
|
|
3632
|
+
if (ref === void 0) {
|
|
3633
|
+
for (const [index, sheet] of sheets.entries()) if ((await sheet.meta()).firstNonEmptyRow > 0) return {
|
|
3634
|
+
sheet,
|
|
3635
|
+
index
|
|
3636
|
+
};
|
|
3637
|
+
return {
|
|
3638
|
+
sheet: sheets[0],
|
|
3639
|
+
index: 0
|
|
3640
|
+
};
|
|
3641
|
+
}
|
|
3642
|
+
if (typeof ref === "number") {
|
|
3643
|
+
const sheet = Number.isInteger(ref) ? sheets[ref - 1] : void 0;
|
|
3644
|
+
if (sheet === void 0) fail$2(`There is no sheet number ${String(ref)}. Sheets: ${names()}.`);
|
|
3645
|
+
return {
|
|
3646
|
+
sheet,
|
|
3647
|
+
index: ref - 1
|
|
3648
|
+
};
|
|
3649
|
+
}
|
|
3650
|
+
if (typeof ref === "string") {
|
|
3651
|
+
const index = sheets.findIndex((sheet) => sheet.name === ref);
|
|
3652
|
+
if (index === -1) fail$2(`There is no sheet named "${ref}". Sheets: ${names()}.`);
|
|
3653
|
+
return {
|
|
3654
|
+
sheet: sheets[index],
|
|
3655
|
+
index
|
|
3656
|
+
};
|
|
3657
|
+
}
|
|
3658
|
+
return fail$2("\"sheet\" must be a sheet name or a 1-based sheet number.");
|
|
3659
|
+
}
|
|
3660
|
+
async function rowAt(sheet, row) {
|
|
3661
|
+
for await (const entry of sheet.scan()) {
|
|
3662
|
+
if (entry.row === row) return entry.cells;
|
|
3663
|
+
if (entry.row > row) return void 0;
|
|
3664
|
+
}
|
|
3665
|
+
}
|
|
3666
|
+
async function buildStreamingView(workbook, sheetRef, headerRowArg) {
|
|
3667
|
+
const { sheet, index } = await resolveStreamingSheet(workbook, sheetRef);
|
|
3668
|
+
const meta = await sheet.meta();
|
|
3669
|
+
let headerRow;
|
|
3670
|
+
if (headerRowArg === void 0) headerRow = meta.firstNonEmptyRow;
|
|
3671
|
+
else {
|
|
3672
|
+
if (typeof headerRowArg !== "number" || !Number.isInteger(headerRowArg) || headerRowArg < 1) fail$2("\"headerRow\" must be a positive integer (1-based worksheet row number).");
|
|
3673
|
+
headerRow = headerRowArg;
|
|
3674
|
+
if (headerRow > meta.rowCount) fail$2(`"headerRow" ${headerRowArg} is past the last row (${meta.rowCount}) of the sheet.`);
|
|
3675
|
+
}
|
|
3676
|
+
if (headerRow === 0) return {
|
|
3677
|
+
sheetName: sheet.name,
|
|
3678
|
+
sheetIndex: index,
|
|
3679
|
+
headerRow: 0,
|
|
3680
|
+
columns: [],
|
|
3681
|
+
data: (async function* () {})()
|
|
3682
|
+
};
|
|
3683
|
+
const header = headerRow === meta.firstNonEmptyRow ? meta.firstRow : await rowAt(sheet, headerRow) ?? [];
|
|
3684
|
+
const width = Math.max(meta.columnCount, header.length);
|
|
3685
|
+
const columns = Array.from({ length: width }, (_, columnIndex) => {
|
|
3686
|
+
const text = (header[columnIndex] ?? "").trim();
|
|
3687
|
+
const letter = columnLetter(columnIndex);
|
|
3688
|
+
return {
|
|
3689
|
+
index: columnIndex,
|
|
3690
|
+
letter,
|
|
3691
|
+
header: text,
|
|
3692
|
+
label: text === "" ? letter : `${letter} ${text}`,
|
|
3693
|
+
name: text === "" ? letter : text
|
|
3694
|
+
};
|
|
3695
|
+
});
|
|
3696
|
+
const data = (async function* () {
|
|
3697
|
+
for await (const entry of sheet.scan()) if (entry.row > headerRow && !blank(entry.cells)) yield entry;
|
|
3698
|
+
})();
|
|
3699
|
+
return {
|
|
3700
|
+
sheetName: sheet.name,
|
|
3701
|
+
sheetIndex: index,
|
|
3702
|
+
headerRow,
|
|
3703
|
+
columns,
|
|
3704
|
+
data
|
|
3705
|
+
};
|
|
3706
|
+
}
|
|
3707
|
+
async function summarizeStreamingWorkbook(workbook) {
|
|
3708
|
+
let tokens = 0;
|
|
3709
|
+
let dataRows = 0;
|
|
3710
|
+
let textChars = 0;
|
|
3711
|
+
for (const sheet of workbook.sheets) {
|
|
3712
|
+
const meta = await sheet.meta();
|
|
3713
|
+
tokens += meta.estimatedTokens;
|
|
3714
|
+
dataRows += meta.dataRows;
|
|
3715
|
+
textChars += meta.textChars;
|
|
3716
|
+
}
|
|
3717
|
+
return {
|
|
3718
|
+
tokens,
|
|
3719
|
+
dataRows,
|
|
3720
|
+
textChars
|
|
3721
|
+
};
|
|
3722
|
+
}
|
|
3723
|
+
|
|
3724
|
+
//#endregion
|
|
3725
|
+
//#region ../agent/src/table/streamingProfile.ts
|
|
3726
|
+
const PROFILE_MAX_CHARS = 8e3;
|
|
3727
|
+
const DISTINCT_CAP = 1e4;
|
|
3728
|
+
const SAMPLE_ROWS = 5;
|
|
3729
|
+
var Stats = class {
|
|
3730
|
+
filled = 0;
|
|
3731
|
+
numbers = 0;
|
|
3732
|
+
dates = 0;
|
|
3733
|
+
texts = 0;
|
|
3734
|
+
summer = new Summer();
|
|
3735
|
+
min;
|
|
3736
|
+
max;
|
|
3737
|
+
earliest;
|
|
3738
|
+
latest;
|
|
3739
|
+
freq = /* @__PURE__ */ new Map();
|
|
3740
|
+
capped = false;
|
|
3741
|
+
add(raw) {
|
|
3742
|
+
const text = raw.trim();
|
|
3743
|
+
const kind = kindOf(text);
|
|
3744
|
+
if (kind === "empty") return;
|
|
3745
|
+
this.filled += 1;
|
|
3746
|
+
if (kind === "number") {
|
|
3747
|
+
this.numbers += 1;
|
|
3748
|
+
const value = Number(text);
|
|
3749
|
+
this.summer.add(value);
|
|
3750
|
+
if (this.min === void 0 || value < this.min) this.min = value;
|
|
3751
|
+
if (this.max === void 0 || value > this.max) this.max = value;
|
|
3752
|
+
} else if (kind === "date") {
|
|
3753
|
+
this.dates += 1;
|
|
3754
|
+
if (this.earliest === void 0 || text < this.earliest) this.earliest = text;
|
|
3755
|
+
if (this.latest === void 0 || text > this.latest) this.latest = text;
|
|
3756
|
+
} else this.texts += 1;
|
|
3757
|
+
const seen = this.freq.get(text);
|
|
3758
|
+
if (seen !== void 0) this.freq.set(text, seen + 1);
|
|
3759
|
+
else if (this.freq.size < DISTINCT_CAP) this.freq.set(text, 1);
|
|
3760
|
+
else this.capped = true;
|
|
3761
|
+
}
|
|
3762
|
+
finish() {
|
|
3763
|
+
let type;
|
|
3764
|
+
if (this.filled === 0) type = "empty";
|
|
3765
|
+
else if (this.numbers >= this.filled * .95) type = "number";
|
|
3766
|
+
else if (this.dates >= this.filled * .95) type = "date";
|
|
3767
|
+
else if (this.numbers + this.dates <= this.filled * .05) type = "text";
|
|
3768
|
+
else type = "mixed";
|
|
3769
|
+
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);
|
|
3770
|
+
return {
|
|
3771
|
+
filled: this.filled,
|
|
3772
|
+
type,
|
|
3773
|
+
distinct: this.capped ? void 0 : this.freq.size,
|
|
3774
|
+
sum: this.summer.total,
|
|
3775
|
+
min: this.min,
|
|
3776
|
+
max: this.max,
|
|
3777
|
+
earliest: this.earliest,
|
|
3778
|
+
latest: this.latest,
|
|
3779
|
+
top
|
|
3780
|
+
};
|
|
3781
|
+
}
|
|
3782
|
+
};
|
|
3783
|
+
async function analyze(view) {
|
|
3784
|
+
const collectors = view.columns.map(() => new Stats());
|
|
3785
|
+
const sample = [];
|
|
3786
|
+
let dataRows = 0;
|
|
3787
|
+
for await (const entry of view.data) {
|
|
3788
|
+
dataRows += 1;
|
|
3789
|
+
if (sample.length < SAMPLE_ROWS) sample.push(entry);
|
|
3790
|
+
for (const column of view.columns) collectors[column.index].add(entry.cells[column.index] ?? "");
|
|
3791
|
+
}
|
|
3792
|
+
return {
|
|
3793
|
+
dataRows,
|
|
3794
|
+
sample,
|
|
3795
|
+
columns: collectors.map((stats) => stats.finish())
|
|
3796
|
+
};
|
|
3797
|
+
}
|
|
3798
|
+
const clip$2 = (text, max) => {
|
|
3799
|
+
const chars = [...text.replace(/[\r\n\t]+/g, " ")];
|
|
3800
|
+
return chars.length <= max ? chars.join("") : `${chars.slice(0, max - 1).join("")}…`;
|
|
3801
|
+
};
|
|
3802
|
+
const LADDER = [
|
|
3803
|
+
{
|
|
3804
|
+
maxSheets: 8,
|
|
3805
|
+
maxColumns: 40,
|
|
3806
|
+
sampleRows: SAMPLE_ROWS,
|
|
3807
|
+
sampleCell: 40,
|
|
3808
|
+
withTop: true,
|
|
3809
|
+
note: void 0
|
|
3810
|
+
},
|
|
3811
|
+
{
|
|
3812
|
+
maxSheets: 3,
|
|
3813
|
+
maxColumns: 40,
|
|
3814
|
+
sampleRows: SAMPLE_ROWS,
|
|
3815
|
+
sampleCell: 40,
|
|
3816
|
+
withTop: true,
|
|
3817
|
+
note: "sheets"
|
|
3818
|
+
},
|
|
3819
|
+
{
|
|
3820
|
+
maxSheets: 3,
|
|
3821
|
+
maxColumns: 20,
|
|
3822
|
+
sampleRows: SAMPLE_ROWS,
|
|
3823
|
+
sampleCell: 40,
|
|
3824
|
+
withTop: true,
|
|
3825
|
+
note: "sheets, columns"
|
|
3826
|
+
},
|
|
3827
|
+
{
|
|
3828
|
+
maxSheets: 3,
|
|
3829
|
+
maxColumns: 20,
|
|
3830
|
+
sampleRows: 3,
|
|
3831
|
+
sampleCell: 16,
|
|
3832
|
+
withTop: true,
|
|
3833
|
+
note: "sheets, columns, sample"
|
|
3834
|
+
},
|
|
3835
|
+
{
|
|
3836
|
+
maxSheets: 2,
|
|
3837
|
+
maxColumns: 12,
|
|
3838
|
+
sampleRows: 2,
|
|
3839
|
+
sampleCell: 12,
|
|
3840
|
+
withTop: false,
|
|
3841
|
+
note: "sheets, columns, sample, top values"
|
|
3842
|
+
}
|
|
3843
|
+
];
|
|
3844
|
+
function columnLine(column, stats, withTop) {
|
|
3845
|
+
const parts = [
|
|
3846
|
+
`${column.letter} ${clip$2(column.header, 40)}`.trimEnd(),
|
|
3847
|
+
stats.type,
|
|
3848
|
+
`${stats.filled} filled`
|
|
3849
|
+
];
|
|
3850
|
+
parts.push(stats.distinct === void 0 ? `>${DISTINCT_CAP} distinct` : `${stats.distinct} distinct`);
|
|
3851
|
+
if (stats.min !== void 0 && stats.max !== void 0) parts.push(`min ${tidy(stats.min)}`, `max ${tidy(stats.max)}`, `sum ${tidy(stats.sum)}`);
|
|
3852
|
+
if (stats.earliest !== void 0 && stats.latest !== void 0) parts.push(`${stats.earliest} … ${stats.latest}`);
|
|
3853
|
+
if (withTop && stats.top.length > 0) parts.push(`top: ${stats.top.map(([value, count]) => `${clip$2(value, 24)} ×${count}`).join(", ")}`);
|
|
3854
|
+
return parts.join(" | ");
|
|
3855
|
+
}
|
|
3856
|
+
function sheetSection(view, analysis, rung, expanded) {
|
|
3857
|
+
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");
|
|
3858
|
+
if (!expanded || view.columns.length === 0) return [head];
|
|
3859
|
+
const lines = [head, "Columns:"];
|
|
3860
|
+
const shown = view.columns.slice(0, rung.maxColumns);
|
|
3861
|
+
for (const column of shown) lines.push(columnLine(column, analysis.columns[column.index], rung.withTop));
|
|
3862
|
+
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`);
|
|
3863
|
+
const sample = analysis.sample.slice(0, rung.sampleRows);
|
|
3864
|
+
if (sample.length > 0) {
|
|
3865
|
+
lines.push(`Sample (sheet rows ${sample[0].row}–${sample[sample.length - 1].row}):`);
|
|
3866
|
+
for (const entry of sample) lines.push(shown.map((column) => clip$2((entry.cells[column.index] ?? "").trim(), rung.sampleCell)).join(" "));
|
|
3867
|
+
}
|
|
3868
|
+
return lines;
|
|
3869
|
+
}
|
|
3870
|
+
async function profileStreamingTable(input) {
|
|
3871
|
+
const { workbook } = input;
|
|
3872
|
+
const total = workbook.sheets.length;
|
|
3873
|
+
const only = input.sheet === void 0 ? void 0 : (await buildStreamingView(workbook, input.sheet, input.headerRow)).sheetIndex;
|
|
3874
|
+
const header = `--- Spreadsheet profile: ${input.name} (id: ${input.id}) · ${total} sheet${total === 1 ? "" : "s"} ---`;
|
|
3875
|
+
const views = [];
|
|
3876
|
+
const analyses = [];
|
|
3877
|
+
for (let index = 0; index < workbook.sheets.length; index += 1) {
|
|
3878
|
+
const view = await buildStreamingView(workbook, index + 1, only === index ? input.headerRow : void 0);
|
|
3879
|
+
views.push(view);
|
|
3880
|
+
analyses.push(await analyze(view));
|
|
3881
|
+
}
|
|
3882
|
+
let result = "";
|
|
3883
|
+
for (const rung of LADDER) {
|
|
3884
|
+
const lines = [header];
|
|
3885
|
+
let expandedCount = 0;
|
|
3886
|
+
for (const [index, view] of views.entries()) {
|
|
3887
|
+
const analysis = analyses[index];
|
|
3888
|
+
const expanded = only === void 0 ? expandedCount < rung.maxSheets : index === only;
|
|
3889
|
+
if (expanded && analysis.dataRows > 0) expandedCount += 1;
|
|
3890
|
+
lines.push(...sheetSection(view, analysis, rung, expanded && (only !== void 0 || analysis.dataRows > 0)));
|
|
3611
3891
|
}
|
|
3612
3892
|
if (rung.note !== void 0) lines.push(`Profile shortened to fit: ${rung.note}. Call query_table with op "describe" for the rest.`);
|
|
3613
3893
|
result = lines.join("\n");
|
|
@@ -3623,6 +3903,8 @@ const QUERY_LIMITS = {
|
|
|
3623
3903
|
maxRows: 500,
|
|
3624
3904
|
maxFilters: 20,
|
|
3625
3905
|
maxGroupBy: 3,
|
|
3906
|
+
maxPivotBy: 2,
|
|
3907
|
+
maxPivotColumns: 100,
|
|
3626
3908
|
maxMetrics: 8,
|
|
3627
3909
|
maxOrderBy: 3,
|
|
3628
3910
|
maxIn: 200,
|
|
@@ -3657,32 +3939,32 @@ const BUCKETS = [
|
|
|
3657
3939
|
"month",
|
|
3658
3940
|
"day"
|
|
3659
3941
|
];
|
|
3660
|
-
function fail(message) {
|
|
3942
|
+
function fail$1(message) {
|
|
3661
3943
|
throw new WebSkillError("VALIDATION_FAILED", message);
|
|
3662
3944
|
}
|
|
3663
3945
|
const isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
3664
3946
|
function list(v, what, max) {
|
|
3665
3947
|
if (v === void 0) return [];
|
|
3666
|
-
if (!Array.isArray(v)) fail(`"${what}" must be an array.`);
|
|
3667
|
-
if (v.length > max) fail(`"${what}" has ${v.length} entries; at most ${max} are allowed.`);
|
|
3948
|
+
if (!Array.isArray(v)) fail$1(`"${what}" must be an array.`);
|
|
3949
|
+
if (v.length > max) fail$1(`"${what}" has ${v.length} entries; at most ${max} are allowed.`);
|
|
3668
3950
|
return v;
|
|
3669
3951
|
}
|
|
3670
3952
|
function oneOf(v, allowed, what) {
|
|
3671
3953
|
if (typeof v === "string" && allowed.includes(v)) return v;
|
|
3672
|
-
return fail(`${what} must be one of: ${allowed.join(", ")}.`);
|
|
3954
|
+
return fail$1(`${what} must be one of: ${allowed.join(", ")}.`);
|
|
3673
3955
|
}
|
|
3674
3956
|
function noOtherKeys(obj, allowed, what) {
|
|
3675
3957
|
const extra = Object.keys(obj).filter((key) => !allowed.includes(key));
|
|
3676
|
-
if (extra.length > 0) fail(`${what} has unknown field${extra.length > 1 ? "s" : ""}: ${extra.join(", ")}.`);
|
|
3958
|
+
if (extra.length > 0) fail$1(`${what} has unknown field${extra.length > 1 ? "s" : ""}: ${extra.join(", ")}.`);
|
|
3677
3959
|
}
|
|
3678
3960
|
function scalar(v, what) {
|
|
3679
3961
|
if (typeof v === "string") return v;
|
|
3680
3962
|
if (typeof v === "number" && Number.isFinite(v)) return v;
|
|
3681
|
-
return fail(`${what} must be a string or a finite number.`);
|
|
3963
|
+
return fail$1(`${what} must be a string or a finite number.`);
|
|
3682
3964
|
}
|
|
3683
3965
|
function compileFilter(view, raw, index, uncomparable) {
|
|
3684
3966
|
const what = `where[${index}]`;
|
|
3685
|
-
if (!isObject(raw)) fail(`${what} must be an object.`);
|
|
3967
|
+
if (!isObject(raw)) fail$1(`${what} must be an object.`);
|
|
3686
3968
|
noOtherKeys(raw, [
|
|
3687
3969
|
"column",
|
|
3688
3970
|
"op",
|
|
@@ -3702,7 +3984,7 @@ function compileFilter(view, raw, index, uncomparable) {
|
|
|
3702
3984
|
};
|
|
3703
3985
|
if (op === "in") {
|
|
3704
3986
|
const values = raw["value"];
|
|
3705
|
-
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".`);
|
|
3987
|
+
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".`);
|
|
3706
3988
|
const texts = /* @__PURE__ */ new Set();
|
|
3707
3989
|
const numbers = /* @__PURE__ */ new Set();
|
|
3708
3990
|
for (const item of values) {
|
|
@@ -3718,7 +4000,7 @@ function compileFilter(view, raw, index, uncomparable) {
|
|
|
3718
4000
|
}
|
|
3719
4001
|
};
|
|
3720
4002
|
}
|
|
3721
|
-
if (raw["value"] === void 0) fail(`${what}.value is required for op "${op}".`);
|
|
4003
|
+
if (raw["value"] === void 0) fail$1(`${what}.value is required for op "${op}".`);
|
|
3722
4004
|
const value = String(scalar(raw["value"], `${what}.value`)).trim();
|
|
3723
4005
|
const valueKind = kindOf(value);
|
|
3724
4006
|
if (op === "contains") {
|
|
@@ -3740,7 +4022,7 @@ function compileFilter(view, raw, index, uncomparable) {
|
|
|
3740
4022
|
test: op === "eq" ? equal : (cells) => !equal(cells)
|
|
3741
4023
|
};
|
|
3742
4024
|
}
|
|
3743
|
-
if (valueKind !== "number" && valueKind !== "date") fail(`${what}.value must be a number or an ISO date (YYYY-MM-DD) for op "${op}", received "${value}".`);
|
|
4025
|
+
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}".`);
|
|
3744
4026
|
const numericValue = valueKind === "number" ? Number(value) : 0;
|
|
3745
4027
|
const satisfied = (order) => op === "gt" ? order > 0 : op === "gte" ? order >= 0 : op === "lt" ? order < 0 : order <= 0;
|
|
3746
4028
|
return {
|
|
@@ -3763,7 +4045,7 @@ function parseLimit(v) {
|
|
|
3763
4045
|
limit: QUERY_LIMITS.defaultRows,
|
|
3764
4046
|
clamped: false
|
|
3765
4047
|
};
|
|
3766
|
-
if (typeof v !== "number" || !Number.isInteger(v) || v < 1) fail("\"limit\" must be a positive integer.");
|
|
4048
|
+
if (typeof v !== "number" || !Number.isInteger(v) || v < 1) fail$1("\"limit\" must be a positive integer.");
|
|
3767
4049
|
return v > QUERY_LIMITS.maxRows ? {
|
|
3768
4050
|
limit: QUERY_LIMITS.maxRows,
|
|
3769
4051
|
clamped: true
|
|
@@ -3772,32 +4054,49 @@ function parseLimit(v) {
|
|
|
3772
4054
|
clamped: false
|
|
3773
4055
|
};
|
|
3774
4056
|
}
|
|
4057
|
+
function parseGroups(args, view, field) {
|
|
4058
|
+
const max = field === "groupBy" ? QUERY_LIMITS.maxGroupBy : QUERY_LIMITS.maxPivotBy;
|
|
4059
|
+
const groups = list(args[field], field, max).map((raw, i) => {
|
|
4060
|
+
if (!isObject(raw)) fail$1(`${field}[${i}] must be an object.`);
|
|
4061
|
+
noOtherKeys(raw, ["column", "bucket"], `${field}[${i}]`);
|
|
4062
|
+
const column = resolveColumn(view, raw["column"], `${field}[${i}].column`);
|
|
4063
|
+
return {
|
|
4064
|
+
column,
|
|
4065
|
+
bucket: raw["bucket"] === void 0 ? "value" : oneOf(raw["bucket"], BUCKETS, `${field}[${i}].bucket`),
|
|
4066
|
+
name: column.name
|
|
4067
|
+
};
|
|
4068
|
+
});
|
|
4069
|
+
if (new Set(groups.map((group) => group.column.index)).size !== groups.length) fail$1(`"${field}" names the same column twice.`);
|
|
4070
|
+
if (new Set(groups.map((group) => group.name)).size !== groups.length) for (const group of groups) group.name = group.column.label;
|
|
4071
|
+
return groups;
|
|
4072
|
+
}
|
|
3775
4073
|
function parseQuery(args, view) {
|
|
3776
4074
|
const op = oneOf(args["op"], ["aggregate", "rows"], "\"op\"");
|
|
3777
4075
|
const uncomparable = /* @__PURE__ */ new Map();
|
|
3778
4076
|
const filters = list(args["where"], "where", QUERY_LIMITS.maxFilters).map((raw, i) => compileFilter(view, raw, i, uncomparable));
|
|
3779
4077
|
const { limit, clamped } = parseLimit(args["limit"]);
|
|
3780
4078
|
const orderRaw = list(args["orderBy"], "orderBy", QUERY_LIMITS.maxOrderBy).map((raw, i) => {
|
|
3781
|
-
if (!isObject(raw)) fail(`orderBy[${i}] must be an object.`);
|
|
4079
|
+
if (!isObject(raw)) fail$1(`orderBy[${i}] must be an object.`);
|
|
3782
4080
|
noOtherKeys(raw, ["key", "desc"], `orderBy[${i}]`);
|
|
3783
|
-
if (typeof raw["key"] !== "string" || raw["key"].trim() === "") fail(`orderBy[${i}].key must be a string.`);
|
|
3784
|
-
if (raw["desc"] !== void 0 && typeof raw["desc"] !== "boolean") fail(`orderBy[${i}].desc must be a boolean.`);
|
|
4081
|
+
if (typeof raw["key"] !== "string" || raw["key"].trim() === "") fail$1(`orderBy[${i}].key must be a string.`);
|
|
4082
|
+
if (raw["desc"] !== void 0 && typeof raw["desc"] !== "boolean") fail$1(`orderBy[${i}].desc must be a boolean.`);
|
|
3785
4083
|
return {
|
|
3786
4084
|
key: raw["key"].trim(),
|
|
3787
4085
|
desc: raw["desc"] === true
|
|
3788
4086
|
};
|
|
3789
4087
|
});
|
|
3790
4088
|
if (op === "rows") {
|
|
3791
|
-
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.");
|
|
4089
|
+
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.");
|
|
3792
4090
|
const columns = list(args["columns"], "columns", view.columns.length || 1).map((ref, i) => resolveColumn(view, ref, `columns[${i}]`));
|
|
3793
4091
|
for (const entry of orderRaw) resolveColumn(view, entry.key, "orderBy.key");
|
|
3794
4092
|
const offset = args["offset"];
|
|
3795
|
-
if (offset !== void 0 && (typeof offset !== "number" || !Number.isInteger(offset) || offset < 0)) fail("\"offset\" must be a non-negative integer.");
|
|
4093
|
+
if (offset !== void 0 && (typeof offset !== "number" || !Number.isInteger(offset) || offset < 0)) fail$1("\"offset\" must be a non-negative integer.");
|
|
3796
4094
|
return {
|
|
3797
4095
|
op,
|
|
3798
4096
|
filters,
|
|
3799
4097
|
uncomparable,
|
|
3800
4098
|
groups: [],
|
|
4099
|
+
pivot: [],
|
|
3801
4100
|
metrics: [],
|
|
3802
4101
|
columns: columns.length > 0 ? columns : [...view.columns],
|
|
3803
4102
|
order: orderRaw,
|
|
@@ -3806,31 +4105,22 @@ function parseQuery(args, view) {
|
|
|
3806
4105
|
offset: typeof offset === "number" ? offset : 0
|
|
3807
4106
|
};
|
|
3808
4107
|
}
|
|
3809
|
-
if (args["columns"] !== void 0 || args["offset"] !== void 0) fail("\"columns\" and \"offset\" belong to op \"rows\". Use op \"aggregate\" with \"groupBy\" and \"metrics\".");
|
|
3810
|
-
const groups =
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
const column = resolveColumn(view, raw["column"], `groupBy[${i}].column`);
|
|
3814
|
-
return {
|
|
3815
|
-
column,
|
|
3816
|
-
bucket: raw["bucket"] === void 0 ? "value" : oneOf(raw["bucket"], BUCKETS, `groupBy[${i}].bucket`),
|
|
3817
|
-
name: column.name
|
|
3818
|
-
};
|
|
3819
|
-
});
|
|
3820
|
-
if (new Set(groups.map((g) => g.column.index)).size !== groups.length) fail("\"groupBy\" names the same column twice.");
|
|
3821
|
-
if (new Set(groups.map((g) => g.name)).size !== groups.length) for (const g of groups) g.name = g.column.label;
|
|
4108
|
+
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\".");
|
|
4109
|
+
const groups = parseGroups(args, view, "groupBy");
|
|
4110
|
+
const pivot = parseGroups(args, view, "pivotBy");
|
|
4111
|
+
if (pivot.some((dimension) => groups.some((group) => group.column.index === dimension.column.index))) fail$1("A column cannot appear in both \"groupBy\" and \"pivotBy\".");
|
|
3822
4112
|
const metricsRaw = list(args["metrics"], "metrics", QUERY_LIMITS.maxMetrics);
|
|
3823
4113
|
const metrics = (metricsRaw.length > 0 ? metricsRaw : [{ fn: "count" }]).map((raw, i) => {
|
|
3824
|
-
if (!isObject(raw)) fail(`metrics[${i}] must be an object.`);
|
|
4114
|
+
if (!isObject(raw)) fail$1(`metrics[${i}] must be an object.`);
|
|
3825
4115
|
noOtherKeys(raw, [
|
|
3826
4116
|
"fn",
|
|
3827
4117
|
"column",
|
|
3828
4118
|
"as"
|
|
3829
4119
|
], `metrics[${i}]`);
|
|
3830
4120
|
const fn = oneOf(raw["fn"], METRIC_FNS, `metrics[${i}].fn`);
|
|
3831
|
-
if (raw["column"] === void 0 && fn !== "count") fail(`metrics[${i}].column is required for "${fn}".`);
|
|
4121
|
+
if (raw["column"] === void 0 && fn !== "count") fail$1(`metrics[${i}].column is required for "${fn}".`);
|
|
3832
4122
|
const column = raw["column"] === void 0 ? void 0 : resolveColumn(view, raw["column"], `metrics[${i}].column`);
|
|
3833
|
-
if (raw["as"] !== void 0 && (typeof raw["as"] !== "string" || raw["as"].trim() === "")) fail(`metrics[${i}].as must be a non-empty string.`);
|
|
4123
|
+
if (raw["as"] !== void 0 && (typeof raw["as"] !== "string" || raw["as"].trim() === "")) fail$1(`metrics[${i}].as must be a non-empty string.`);
|
|
3834
4124
|
const alias = typeof raw["as"] === "string" ? raw["as"].trim() : column === void 0 ? fn : `${fn}(${column.name})`;
|
|
3835
4125
|
return column === void 0 ? {
|
|
3836
4126
|
fn,
|
|
@@ -3841,18 +4131,19 @@ function parseQuery(args, view) {
|
|
|
3841
4131
|
alias
|
|
3842
4132
|
};
|
|
3843
4133
|
});
|
|
3844
|
-
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\".");
|
|
4134
|
+
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\".");
|
|
3845
4135
|
const available = () => [...metrics.map((m) => m.alias), ...groups.map((g) => g.name)].join(", ");
|
|
3846
4136
|
for (const entry of orderRaw) {
|
|
3847
4137
|
if (metrics.some((m) => m.alias === entry.key)) continue;
|
|
3848
4138
|
const column = resolveColumn(view, entry.key, "orderBy.key");
|
|
3849
|
-
if (!groups.some((g) => g.column.index === column.index)) fail(`orderBy.key "${entry.key}" is neither a metric nor a grouped column. Available: ${available()}.`);
|
|
4139
|
+
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()}.`);
|
|
3850
4140
|
}
|
|
3851
4141
|
return {
|
|
3852
4142
|
op,
|
|
3853
4143
|
filters,
|
|
3854
4144
|
uncomparable,
|
|
3855
4145
|
groups,
|
|
4146
|
+
pivot,
|
|
3856
4147
|
metrics,
|
|
3857
4148
|
columns: [],
|
|
3858
4149
|
order: orderRaw,
|
|
@@ -3916,7 +4207,8 @@ function finish(metric, acc) {
|
|
|
3916
4207
|
return (metric.fn === "min" ? acc.minD : acc.maxD) ?? null;
|
|
3917
4208
|
}
|
|
3918
4209
|
}
|
|
3919
|
-
function runAggregate(view, query) {
|
|
4210
|
+
async function runAggregate(view, query, complete = false) {
|
|
4211
|
+
if (query.pivot.length > 0) return await runPivot(view, query, complete);
|
|
3920
4212
|
const { groups: groupSpecs, metrics } = query;
|
|
3921
4213
|
const skipped = new Map(metrics.map((m) => [m.alias, {
|
|
3922
4214
|
empty: 0,
|
|
@@ -3924,6 +4216,7 @@ function runAggregate(view, query) {
|
|
|
3924
4216
|
}]));
|
|
3925
4217
|
const unbucketed = /* @__PURE__ */ new Map();
|
|
3926
4218
|
const table = /* @__PURE__ */ new Map();
|
|
4219
|
+
let dataRows = 0;
|
|
3927
4220
|
let matched = 0;
|
|
3928
4221
|
const groupOf = (key) => {
|
|
3929
4222
|
const id = key.map((part) => part === null ? "\0" : part).join("");
|
|
@@ -3938,7 +4231,8 @@ function runAggregate(view, query) {
|
|
|
3938
4231
|
return group;
|
|
3939
4232
|
};
|
|
3940
4233
|
if (groupSpecs.length === 0) groupOf([]);
|
|
3941
|
-
for (const { cells } of view.data) {
|
|
4234
|
+
for await (const { cells } of view.data) {
|
|
4235
|
+
dataRows += 1;
|
|
3942
4236
|
if (!query.filters.every((filter) => filter.test(cells))) continue;
|
|
3943
4237
|
matched += 1;
|
|
3944
4238
|
const group = groupOf(groupSpecs.map((spec) => {
|
|
@@ -4011,7 +4305,10 @@ function runAggregate(view, query) {
|
|
|
4011
4305
|
const out = rows.slice(0, query.limit).map((row) => ({
|
|
4012
4306
|
key: Object.fromEntries(row.key.map((part, i) => {
|
|
4013
4307
|
if (part === null) return [names[i], null];
|
|
4014
|
-
const cut =
|
|
4308
|
+
const cut = complete ? {
|
|
4309
|
+
text: part,
|
|
4310
|
+
clipped: false
|
|
4311
|
+
} : clip$1(part);
|
|
4015
4312
|
if (cut.clipped) cellsTruncated = true;
|
|
4016
4313
|
return [names[i], cut.text];
|
|
4017
4314
|
})),
|
|
@@ -4026,6 +4323,7 @@ function runAggregate(view, query) {
|
|
|
4026
4323
|
};
|
|
4027
4324
|
}
|
|
4028
4325
|
return {
|
|
4326
|
+
dataRows,
|
|
4029
4327
|
matchedRows: matched,
|
|
4030
4328
|
uncomparable: Object.fromEntries(query.uncomparable),
|
|
4031
4329
|
groups: out,
|
|
@@ -4035,8 +4333,194 @@ function runAggregate(view, query) {
|
|
|
4035
4333
|
cellsTruncated
|
|
4036
4334
|
};
|
|
4037
4335
|
}
|
|
4038
|
-
function
|
|
4039
|
-
const
|
|
4336
|
+
async function runPivot(view, query, complete) {
|
|
4337
|
+
const { groups: rowSpecs, pivot: columnSpecs, metrics } = query;
|
|
4338
|
+
const skipped = new Map(metrics.map((metric) => [metric.alias, {
|
|
4339
|
+
empty: 0,
|
|
4340
|
+
nonNumeric: 0
|
|
4341
|
+
}]));
|
|
4342
|
+
const unbucketed = /* @__PURE__ */ new Map();
|
|
4343
|
+
const table = /* @__PURE__ */ new Map();
|
|
4344
|
+
const columnKeys = /* @__PURE__ */ new Map();
|
|
4345
|
+
const labels = /* @__PURE__ */ new Map();
|
|
4346
|
+
const uniqueLabels = /* @__PURE__ */ new Set();
|
|
4347
|
+
let dataRows = 0;
|
|
4348
|
+
let matched = 0;
|
|
4349
|
+
let cellsTruncated = false;
|
|
4350
|
+
const keyOf = (specs, cells) => specs.map((spec) => {
|
|
4351
|
+
const text = (cells[spec.column.index] ?? "").trim();
|
|
4352
|
+
if (text === "") return null;
|
|
4353
|
+
if (spec.bucket === "value") return text;
|
|
4354
|
+
if (kindOf(text) === "date") return text.slice(0, spec.bucket === "year" ? 4 : spec.bucket === "month" ? 7 : 10);
|
|
4355
|
+
unbucketed.set(spec.name, (unbucketed.get(spec.name) ?? 0) + 1);
|
|
4356
|
+
return null;
|
|
4357
|
+
});
|
|
4358
|
+
const idOf = (key) => key.map((part) => part === null ? "\0" : part).join("");
|
|
4359
|
+
const add = (metric, acc, cells, skip) => {
|
|
4360
|
+
if (metric.column === void 0) {
|
|
4361
|
+
acc.rows += 1;
|
|
4362
|
+
return;
|
|
4363
|
+
}
|
|
4364
|
+
const text = (cells[metric.column.index] ?? "").trim();
|
|
4365
|
+
if (metric.fn === "count") {
|
|
4366
|
+
if (text !== "") acc.rows += 1;
|
|
4367
|
+
return;
|
|
4368
|
+
}
|
|
4369
|
+
if (metric.fn === "countDistinct") {
|
|
4370
|
+
if (text !== "") acc.distinct.add(text);
|
|
4371
|
+
return;
|
|
4372
|
+
}
|
|
4373
|
+
const kind = kindOf(text);
|
|
4374
|
+
if (kind === "empty") {
|
|
4375
|
+
if (skip !== void 0) skip.empty += 1;
|
|
4376
|
+
} else if (kind === "number") {
|
|
4377
|
+
const value = Number(text);
|
|
4378
|
+
acc.numbers += 1;
|
|
4379
|
+
if (metric.fn === "sum" || metric.fn === "avg") acc.sum.add(value);
|
|
4380
|
+
else if (metric.fn === "median") acc.values.push(value);
|
|
4381
|
+
else if (metric.fn === "min") acc.minN = acc.minN === void 0 ? value : Math.min(acc.minN, value);
|
|
4382
|
+
else acc.maxN = acc.maxN === void 0 ? value : Math.max(acc.maxN, value);
|
|
4383
|
+
} else if (kind === "date" && (metric.fn === "min" || metric.fn === "max")) {
|
|
4384
|
+
acc.dates += 1;
|
|
4385
|
+
if (metric.fn === "min") acc.minD = acc.minD === void 0 ? text : acc.minD < text ? acc.minD : text;
|
|
4386
|
+
else acc.maxD = acc.maxD === void 0 ? text : acc.maxD > text ? acc.maxD : text;
|
|
4387
|
+
} else if (skip !== void 0) skip.nonNumeric += 1;
|
|
4388
|
+
};
|
|
4389
|
+
for await (const { cells } of view.data) {
|
|
4390
|
+
dataRows += 1;
|
|
4391
|
+
if (!query.filters.every((filter) => filter.test(cells))) continue;
|
|
4392
|
+
matched += 1;
|
|
4393
|
+
const rowKey = keyOf(rowSpecs, cells);
|
|
4394
|
+
const columnKey = keyOf(columnSpecs, cells);
|
|
4395
|
+
const rowId = idOf(rowKey);
|
|
4396
|
+
const columnId = idOf(columnKey);
|
|
4397
|
+
let group = table.get(rowId);
|
|
4398
|
+
if (group === void 0) {
|
|
4399
|
+
group = {
|
|
4400
|
+
key: rowKey,
|
|
4401
|
+
totals: metrics.map(newAcc),
|
|
4402
|
+
cells: /* @__PURE__ */ new Map()
|
|
4403
|
+
};
|
|
4404
|
+
table.set(rowId, group);
|
|
4405
|
+
}
|
|
4406
|
+
if (!columnKeys.has(columnId)) {
|
|
4407
|
+
columnKeys.set(columnId, columnKey);
|
|
4408
|
+
const base = columnKey.map((part) => part ?? "(empty)").join(" / ");
|
|
4409
|
+
let label = base;
|
|
4410
|
+
let suffix = 2;
|
|
4411
|
+
while (uniqueLabels.has(label)) label = `${base} (#${suffix++})`;
|
|
4412
|
+
labels.set(columnId, label);
|
|
4413
|
+
uniqueLabels.add(label);
|
|
4414
|
+
}
|
|
4415
|
+
let cellAccs = group.cells.get(columnId);
|
|
4416
|
+
if (cellAccs === void 0) {
|
|
4417
|
+
cellAccs = metrics.map(newAcc);
|
|
4418
|
+
group.cells.set(columnId, cellAccs);
|
|
4419
|
+
}
|
|
4420
|
+
for (const [index, metric] of metrics.entries()) {
|
|
4421
|
+
add(metric, group.totals[index], cells, skipped.get(metric.alias));
|
|
4422
|
+
add(metric, cellAccs[index], cells);
|
|
4423
|
+
}
|
|
4424
|
+
}
|
|
4425
|
+
const usedLabels = /* @__PURE__ */ new Set();
|
|
4426
|
+
const columnLabel = (base, alias) => {
|
|
4427
|
+
const clipped = complete ? {
|
|
4428
|
+
text: `${base} | ${alias}`,
|
|
4429
|
+
clipped: false
|
|
4430
|
+
} : clip$1(`${base} | ${alias}`);
|
|
4431
|
+
if (clipped.clipped) cellsTruncated = true;
|
|
4432
|
+
let label = clipped.text;
|
|
4433
|
+
let suffix = 2;
|
|
4434
|
+
while (usedLabels.has(label)) label = `${clipped.text.slice(0, 190)} (#${suffix++})`;
|
|
4435
|
+
usedLabels.add(label);
|
|
4436
|
+
return label;
|
|
4437
|
+
};
|
|
4438
|
+
const descriptors = [...columnKeys.keys()].map((id) => ({
|
|
4439
|
+
id,
|
|
4440
|
+
label: labels.get(id)
|
|
4441
|
+
}));
|
|
4442
|
+
const shown = complete ? descriptors : descriptors.slice(0, QUERY_LIMITS.maxPivotColumns);
|
|
4443
|
+
const dynamicColumns = shown.flatMap(({ label }) => metrics.map((metric) => columnLabel(label, metric.alias)));
|
|
4444
|
+
const totalColumns = metrics.map((metric) => columnLabel("Total", metric.alias));
|
|
4445
|
+
const outputColumns = [...dynamicColumns, ...totalColumns];
|
|
4446
|
+
const ordered = [...table.values()].map((group) => ({
|
|
4447
|
+
key: group.key,
|
|
4448
|
+
values: Object.fromEntries(metrics.map((metric, index) => [totalColumns[index], finish(metric, group.totals[index])]))
|
|
4449
|
+
}));
|
|
4450
|
+
const keyIndexOf = (key) => {
|
|
4451
|
+
const column = metrics.some((metric) => metric.alias === key) ? void 0 : resolveColumn(view, key, "orderBy.key");
|
|
4452
|
+
return column === void 0 ? -1 : rowSpecs.findIndex((spec) => spec.column.index === column.index);
|
|
4453
|
+
};
|
|
4454
|
+
const comparators = [];
|
|
4455
|
+
for (const entry of query.order) {
|
|
4456
|
+
const metric = metrics.find((candidate) => candidate.alias === entry.key);
|
|
4457
|
+
const metricIndex = metric === void 0 ? -1 : metrics.indexOf(metric);
|
|
4458
|
+
const groupIndex = metricIndex === -1 ? keyIndexOf(entry.key) : -1;
|
|
4459
|
+
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));
|
|
4460
|
+
}
|
|
4461
|
+
if (comparators.length === 0 && rowSpecs.length > 0) {
|
|
4462
|
+
comparators.push((a, b) => compareNullable(a.values[totalColumns[0]] ?? null, b.values[totalColumns[0]] ?? null, true));
|
|
4463
|
+
for (let index = 0; index < rowSpecs.length; index += 1) comparators.push((a, b) => compareNullable(a.key[index] ?? null, b.key[index] ?? null, false));
|
|
4464
|
+
}
|
|
4465
|
+
ordered.sort((a, b) => {
|
|
4466
|
+
for (const compare of comparators) {
|
|
4467
|
+
const order = compare(a, b);
|
|
4468
|
+
if (order !== 0) return order;
|
|
4469
|
+
}
|
|
4470
|
+
return 0;
|
|
4471
|
+
});
|
|
4472
|
+
const names = rowSpecs.map((spec) => spec.name);
|
|
4473
|
+
const groups = ordered.slice(0, query.limit).map((entry) => {
|
|
4474
|
+
const original = table.get(idOf(entry.key));
|
|
4475
|
+
const values = {};
|
|
4476
|
+
for (const [index, metric] of metrics.entries()) {
|
|
4477
|
+
for (const [columnIndex, { id }] of shown.entries()) {
|
|
4478
|
+
const accs = original.cells.get(id);
|
|
4479
|
+
values[dynamicColumns[columnIndex * metrics.length + index]] = accs === void 0 ? null : finish(metric, accs[index]);
|
|
4480
|
+
}
|
|
4481
|
+
values[totalColumns[index]] = finish(metric, original.totals[index]);
|
|
4482
|
+
}
|
|
4483
|
+
return {
|
|
4484
|
+
key: Object.fromEntries(entry.key.map((part, index) => {
|
|
4485
|
+
if (part === null) return [names[index], null];
|
|
4486
|
+
const cut = complete ? {
|
|
4487
|
+
text: part,
|
|
4488
|
+
clipped: false
|
|
4489
|
+
} : clip$1(part);
|
|
4490
|
+
if (cut.clipped) cellsTruncated = true;
|
|
4491
|
+
return [names[index], cut.text];
|
|
4492
|
+
})),
|
|
4493
|
+
values
|
|
4494
|
+
};
|
|
4495
|
+
});
|
|
4496
|
+
const skippedOut = {};
|
|
4497
|
+
for (const [alias, { empty, nonNumeric }] of skipped) {
|
|
4498
|
+
if (empty === 0 && nonNumeric === 0) continue;
|
|
4499
|
+
skippedOut[alias] = {
|
|
4500
|
+
...empty > 0 ? { empty } : {},
|
|
4501
|
+
...nonNumeric > 0 ? { nonNumeric } : {}
|
|
4502
|
+
};
|
|
4503
|
+
}
|
|
4504
|
+
return {
|
|
4505
|
+
dataRows,
|
|
4506
|
+
matchedRows: matched,
|
|
4507
|
+
uncomparable: Object.fromEntries(query.uncomparable),
|
|
4508
|
+
groups,
|
|
4509
|
+
totalGroups: table.size,
|
|
4510
|
+
columns: outputColumns,
|
|
4511
|
+
...complete || descriptors.length <= QUERY_LIMITS.maxPivotColumns ? {} : { pivotColumnsTruncated: true },
|
|
4512
|
+
...Object.keys(skippedOut).length > 0 ? { skipped: skippedOut } : {},
|
|
4513
|
+
...unbucketed.size > 0 ? { unbucketed: Object.fromEntries(unbucketed) } : {},
|
|
4514
|
+
cellsTruncated
|
|
4515
|
+
};
|
|
4516
|
+
}
|
|
4517
|
+
async function runRows(view, query, complete = false) {
|
|
4518
|
+
const matched = [];
|
|
4519
|
+
let dataRows = 0;
|
|
4520
|
+
for await (const entry of view.data) {
|
|
4521
|
+
dataRows += 1;
|
|
4522
|
+
if (query.filters.every((filter) => filter.test(entry.cells))) matched.push(entry);
|
|
4523
|
+
}
|
|
4040
4524
|
if (query.order.length > 0) {
|
|
4041
4525
|
const keys = query.order.map((entry) => ({
|
|
4042
4526
|
index: resolveColumn(view, entry.key, "orderBy.key").index,
|
|
@@ -4058,12 +4542,17 @@ function runRows(view, query) {
|
|
|
4058
4542
|
const rows = matched.slice(query.offset, query.offset + query.limit).map((entry) => ({
|
|
4059
4543
|
row: entry.row,
|
|
4060
4544
|
cells: query.columns.map((column) => {
|
|
4061
|
-
const
|
|
4545
|
+
const raw = (entry.cells[column.index] ?? "").trim();
|
|
4546
|
+
const cut = complete ? {
|
|
4547
|
+
text: raw,
|
|
4548
|
+
clipped: false
|
|
4549
|
+
} : clip$1(raw);
|
|
4062
4550
|
if (cut.clipped) cellsTruncated = true;
|
|
4063
4551
|
return cut.text;
|
|
4064
4552
|
})
|
|
4065
4553
|
}));
|
|
4066
4554
|
return {
|
|
4555
|
+
dataRows,
|
|
4067
4556
|
matchedRows: matched.length,
|
|
4068
4557
|
uncomparable: Object.fromEntries(query.uncomparable),
|
|
4069
4558
|
rows,
|
|
@@ -4072,13 +4561,220 @@ function runRows(view, query) {
|
|
|
4072
4561
|
};
|
|
4073
4562
|
}
|
|
4074
4563
|
|
|
4564
|
+
//#endregion
|
|
4565
|
+
//#region ../agent/src/table/export.ts
|
|
4566
|
+
const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4567
|
+
function fail(message) {
|
|
4568
|
+
throw new WebSkillError("VALIDATION_FAILED", message);
|
|
4569
|
+
}
|
|
4570
|
+
function onlyKeys(value, allowed, path) {
|
|
4571
|
+
const extra = Object.keys(value).filter((key) => !allowed.includes(key));
|
|
4572
|
+
if (extra.length > 0) fail(`${path} has unsupported field(s): ${extra.join(", ")}.`);
|
|
4573
|
+
}
|
|
4574
|
+
function sheetName(value, path) {
|
|
4575
|
+
if (typeof value !== "string" || value.trim() === "") fail(`${path} must be a non-empty sheet name.`);
|
|
4576
|
+
return value.trim();
|
|
4577
|
+
}
|
|
4578
|
+
function literalRows(value, path) {
|
|
4579
|
+
if (!Array.isArray(value)) fail(`${path} must be an array of rows.`);
|
|
4580
|
+
return value.map((row, rowIndex) => {
|
|
4581
|
+
if (!Array.isArray(row)) fail(`${path}[${rowIndex}] must be an array of cells.`);
|
|
4582
|
+
return row.map((cell, columnIndex) => {
|
|
4583
|
+
if (cell !== null && ![
|
|
4584
|
+
"string",
|
|
4585
|
+
"number",
|
|
4586
|
+
"boolean"
|
|
4587
|
+
].includes(typeof cell)) fail(`${path}[${rowIndex}][${columnIndex}] must be a string, number, boolean or null.`);
|
|
4588
|
+
return cell;
|
|
4589
|
+
});
|
|
4590
|
+
});
|
|
4591
|
+
}
|
|
4592
|
+
function parseExportFile(raw) {
|
|
4593
|
+
if (raw === void 0) return void 0;
|
|
4594
|
+
if (!isRecord$1(raw)) fail("\"exportFile\" must be an object with a \"fileName\".");
|
|
4595
|
+
onlyKeys(raw, [
|
|
4596
|
+
"fileName",
|
|
4597
|
+
"sheetName",
|
|
4598
|
+
"additionalSheets"
|
|
4599
|
+
], "\"exportFile\"");
|
|
4600
|
+
const additionalRaw = raw["additionalSheets"];
|
|
4601
|
+
if (additionalRaw !== void 0 && (!Array.isArray(additionalRaw) || additionalRaw.length === 0)) fail("\"exportFile.additionalSheets\" must be a non-empty array when present.");
|
|
4602
|
+
const additionalSheets = (additionalRaw ?? []).map((entry, index) => {
|
|
4603
|
+
const path = `exportFile.additionalSheets[${index}]`;
|
|
4604
|
+
if (!isRecord$1(entry)) fail(`"${path}" must be an object.`);
|
|
4605
|
+
const name = sheetName(entry["name"], `"${path}.name"`);
|
|
4606
|
+
if ("query" in entry) {
|
|
4607
|
+
onlyKeys(entry, ["name", "query"], `"${path}"`);
|
|
4608
|
+
if (!isRecord$1(entry["query"])) fail(`"${path}.query" must be an aggregate or rows query object.`);
|
|
4609
|
+
return {
|
|
4610
|
+
name,
|
|
4611
|
+
query: entry["query"]
|
|
4612
|
+
};
|
|
4613
|
+
}
|
|
4614
|
+
onlyKeys(entry, [
|
|
4615
|
+
"name",
|
|
4616
|
+
"columns",
|
|
4617
|
+
"rows"
|
|
4618
|
+
], `"${path}"`);
|
|
4619
|
+
const columns = entry["columns"];
|
|
4620
|
+
if (columns !== void 0 && (!Array.isArray(columns) || columns.some((column) => typeof column !== "string"))) fail(`"${path}.columns" must be an array of header strings.`);
|
|
4621
|
+
return {
|
|
4622
|
+
name,
|
|
4623
|
+
...columns !== void 0 ? { columns } : {},
|
|
4624
|
+
rows: literalRows(entry["rows"], `${path}.rows`)
|
|
4625
|
+
};
|
|
4626
|
+
});
|
|
4627
|
+
return {
|
|
4628
|
+
fileName: resolveFileName$1(raw["fileName"], "query_table"),
|
|
4629
|
+
sheetName: raw["sheetName"] === void 0 ? "Query result" : sheetName(raw["sheetName"], "\"exportFile.sheetName\""),
|
|
4630
|
+
additionalSheets
|
|
4631
|
+
};
|
|
4632
|
+
}
|
|
4633
|
+
const CELL_SCHEMA = { anyOf: [
|
|
4634
|
+
{ type: "string" },
|
|
4635
|
+
{ type: "number" },
|
|
4636
|
+
{ type: "boolean" },
|
|
4637
|
+
{ type: "null" }
|
|
4638
|
+
] };
|
|
4639
|
+
function createExportFileSchema(queryProperties) {
|
|
4640
|
+
const nestedQueryProperties = Object.fromEntries(Object.entries(queryProperties).filter(([key]) => ![
|
|
4641
|
+
"id",
|
|
4642
|
+
"limit",
|
|
4643
|
+
"offset"
|
|
4644
|
+
].includes(key)).map(([key, value]) => key === "op" ? [key, {
|
|
4645
|
+
...value,
|
|
4646
|
+
enum: ["aggregate", "rows"],
|
|
4647
|
+
description: "The query operation for this sheet."
|
|
4648
|
+
}] : [key, value]));
|
|
4649
|
+
return {
|
|
4650
|
+
type: "object",
|
|
4651
|
+
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.",
|
|
4652
|
+
properties: {
|
|
4653
|
+
fileName: {
|
|
4654
|
+
type: "string",
|
|
4655
|
+
description: "Workbook file name, for example \"contract-analysis.xlsx\"."
|
|
4656
|
+
},
|
|
4657
|
+
sheetName: {
|
|
4658
|
+
type: "string",
|
|
4659
|
+
description: "Name of the primary sheet produced by the top-level query. Defaults to \"Query result\"."
|
|
4660
|
+
},
|
|
4661
|
+
additionalSheets: {
|
|
4662
|
+
type: "array",
|
|
4663
|
+
minItems: 1,
|
|
4664
|
+
maxItems: SPREADSHEET_LIMITS.sheets - 1,
|
|
4665
|
+
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.",
|
|
4666
|
+
items: { anyOf: [{
|
|
4667
|
+
type: "object",
|
|
4668
|
+
properties: {
|
|
4669
|
+
name: {
|
|
4670
|
+
type: "string",
|
|
4671
|
+
description: "Output sheet name."
|
|
4672
|
+
},
|
|
4673
|
+
query: {
|
|
4674
|
+
type: "object",
|
|
4675
|
+
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.",
|
|
4676
|
+
properties: nestedQueryProperties,
|
|
4677
|
+
required: ["op"],
|
|
4678
|
+
additionalProperties: false
|
|
4679
|
+
}
|
|
4680
|
+
},
|
|
4681
|
+
required: ["name", "query"],
|
|
4682
|
+
additionalProperties: false
|
|
4683
|
+
}, {
|
|
4684
|
+
type: "object",
|
|
4685
|
+
description: "A small literal notes or definitions sheet, not a way to transport large query results.",
|
|
4686
|
+
properties: {
|
|
4687
|
+
name: {
|
|
4688
|
+
type: "string",
|
|
4689
|
+
description: "Output sheet name."
|
|
4690
|
+
},
|
|
4691
|
+
columns: {
|
|
4692
|
+
type: "array",
|
|
4693
|
+
maxItems: SPREADSHEET_LIMITS.columnsPerSheet,
|
|
4694
|
+
items: { type: "string" }
|
|
4695
|
+
},
|
|
4696
|
+
rows: {
|
|
4697
|
+
type: "array",
|
|
4698
|
+
maxItems: SPREADSHEET_LIMITS.rowsPerSheet,
|
|
4699
|
+
items: {
|
|
4700
|
+
type: "array",
|
|
4701
|
+
maxItems: SPREADSHEET_LIMITS.columnsPerSheet,
|
|
4702
|
+
items: CELL_SCHEMA
|
|
4703
|
+
}
|
|
4704
|
+
}
|
|
4705
|
+
},
|
|
4706
|
+
required: ["name", "rows"],
|
|
4707
|
+
additionalProperties: false
|
|
4708
|
+
}] }
|
|
4709
|
+
}
|
|
4710
|
+
},
|
|
4711
|
+
required: ["fileName"],
|
|
4712
|
+
additionalProperties: false
|
|
4713
|
+
};
|
|
4714
|
+
}
|
|
4715
|
+
function sheetFromQuery(query, output, name) {
|
|
4716
|
+
if (output.groups !== void 0) {
|
|
4717
|
+
const valueColumns = query.pivot.length > 0 ? output.columns ?? [] : query.metrics.map((metric) => metric.alias);
|
|
4718
|
+
return {
|
|
4719
|
+
name,
|
|
4720
|
+
columns: [...query.groups.map((group) => group.name), ...valueColumns],
|
|
4721
|
+
rows: output.groups.map((group) => [...query.groups.map((dimension) => group.key[dimension.name] ?? null), ...valueColumns.map((column) => group.values[column] ?? null)])
|
|
4722
|
+
};
|
|
4723
|
+
}
|
|
4724
|
+
return {
|
|
4725
|
+
name,
|
|
4726
|
+
columns: output.columns ?? [],
|
|
4727
|
+
rows: (output.rows ?? []).map((row) => row.cells)
|
|
4728
|
+
};
|
|
4729
|
+
}
|
|
4730
|
+
async function exportQuerySheet(workbook, args, name) {
|
|
4731
|
+
const view = isStreamingTableWorkbook(workbook) ? await buildStreamingView(workbook, args["sheet"], args["headerRow"]) : buildView(workbook, args["sheet"], args["headerRow"]);
|
|
4732
|
+
const query = parseQuery(args, view);
|
|
4733
|
+
const computation = {
|
|
4734
|
+
...query,
|
|
4735
|
+
limit: Number.MAX_SAFE_INTEGER,
|
|
4736
|
+
offset: 0,
|
|
4737
|
+
limitClamped: false
|
|
4738
|
+
};
|
|
4739
|
+
const output = computation.op === "aggregate" ? await runAggregate(view, computation, true) : await runRows(view, computation, true);
|
|
4740
|
+
return {
|
|
4741
|
+
sheet: sheetFromQuery(query, output, name),
|
|
4742
|
+
matchedRows: output.matchedRows,
|
|
4743
|
+
...output.totalGroups !== void 0 ? { totalGroups: output.totalGroups } : {}
|
|
4744
|
+
};
|
|
4745
|
+
}
|
|
4746
|
+
function captureTableQueryArgs(args) {
|
|
4747
|
+
const exportFile = args["exportFile"];
|
|
4748
|
+
if (!isRecord$1(exportFile)) return args;
|
|
4749
|
+
const sheets = exportFile["additionalSheets"];
|
|
4750
|
+
if (!Array.isArray(sheets)) return args;
|
|
4751
|
+
let redacted = false;
|
|
4752
|
+
const captured = sheets.map((sheet) => {
|
|
4753
|
+
if (!isRecord$1(sheet) || !Array.isArray(sheet["rows"])) return sheet;
|
|
4754
|
+
redacted = true;
|
|
4755
|
+
const summary = {
|
|
4756
|
+
...sheet,
|
|
4757
|
+
rowCount: sheet["rows"].length
|
|
4758
|
+
};
|
|
4759
|
+
delete summary["rows"];
|
|
4760
|
+
return summary;
|
|
4761
|
+
});
|
|
4762
|
+
return redacted ? {
|
|
4763
|
+
...args,
|
|
4764
|
+
exportFile: {
|
|
4765
|
+
...exportFile,
|
|
4766
|
+
additionalSheets: captured
|
|
4767
|
+
}
|
|
4768
|
+
} : args;
|
|
4769
|
+
}
|
|
4770
|
+
|
|
4075
4771
|
//#endregion
|
|
4076
4772
|
//#region ../agent/src/table/toolSource.ts
|
|
4077
4773
|
/** @experimental */
|
|
4078
4774
|
const QUERY_TABLE_TOOL = "query_table";
|
|
4079
|
-
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.";
|
|
4775
|
+
const DESCRIPTION$2 = "Run exact statistics over an Excel (.xlsx/.xls) 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.";
|
|
4080
4776
|
const DESCRIPTION_WITH_DOWNLOADS = DESCRIPTION$2.replace("the user attached,", "the user attached or downloaded,");
|
|
4081
|
-
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.";
|
|
4777
|
+
const SYSTEM_PROMPT$3 = "query_table computes over every row of an attached .xlsx or .xls 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.";
|
|
4082
4778
|
const SYSTEM_PROMPT_WITH_DOWNLOADS = `${SYSTEM_PROMPT$3} A downloaded workbook\u2019s id from list_downloaded_files works too.`;
|
|
4083
4779
|
const ID_DESCRIPTION = "The attachment id from the message that carried the workbook.";
|
|
4084
4780
|
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.`;
|
|
@@ -4160,6 +4856,24 @@ const INPUT_SCHEMA$2 = {
|
|
|
4160
4856
|
additionalProperties: false
|
|
4161
4857
|
}
|
|
4162
4858
|
},
|
|
4859
|
+
pivotBy: {
|
|
4860
|
+
type: "array",
|
|
4861
|
+
maxItems: QUERY_LIMITS.maxPivotBy,
|
|
4862
|
+
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.",
|
|
4863
|
+
items: {
|
|
4864
|
+
type: "object",
|
|
4865
|
+
properties: {
|
|
4866
|
+
column: COLUMN,
|
|
4867
|
+
bucket: {
|
|
4868
|
+
type: "string",
|
|
4869
|
+
enum: [...BUCKETS],
|
|
4870
|
+
description: "\"value\" (default) groups by the cell text; year/month/day bucket ISO date cells."
|
|
4871
|
+
}
|
|
4872
|
+
},
|
|
4873
|
+
required: ["column"],
|
|
4874
|
+
additionalProperties: false
|
|
4875
|
+
}
|
|
4876
|
+
},
|
|
4163
4877
|
metrics: {
|
|
4164
4878
|
type: "array",
|
|
4165
4879
|
maxItems: QUERY_LIMITS.maxMetrics,
|
|
@@ -4203,7 +4917,7 @@ const INPUT_SCHEMA$2 = {
|
|
|
4203
4917
|
limit: {
|
|
4204
4918
|
type: "integer",
|
|
4205
4919
|
minimum: 1,
|
|
4206
|
-
description: `
|
|
4920
|
+
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.`
|
|
4207
4921
|
},
|
|
4208
4922
|
offset: {
|
|
4209
4923
|
type: "integer",
|
|
@@ -4248,10 +4962,17 @@ function fit(head, field, items, more) {
|
|
|
4248
4962
|
* @experimental
|
|
4249
4963
|
*/
|
|
4250
4964
|
function createTableQueryToolSource(options) {
|
|
4251
|
-
const
|
|
4965
|
+
const schema = options.writeArtifact === void 0 ? INPUT_SCHEMA$2 : {
|
|
4252
4966
|
...INPUT_SCHEMA$2,
|
|
4253
4967
|
properties: {
|
|
4254
4968
|
...INPUT_SCHEMA$2.properties,
|
|
4969
|
+
exportFile: createExportFileSchema(INPUT_SCHEMA$2.properties ?? {})
|
|
4970
|
+
}
|
|
4971
|
+
};
|
|
4972
|
+
const schemaWithDownloads = {
|
|
4973
|
+
...schema,
|
|
4974
|
+
properties: {
|
|
4975
|
+
...schema.properties,
|
|
4255
4976
|
id: {
|
|
4256
4977
|
type: "string",
|
|
4257
4978
|
description: ID_DESCRIPTION_WITH_DOWNLOADS
|
|
@@ -4259,6 +4980,7 @@ function createTableQueryToolSource(options) {
|
|
|
4259
4980
|
}
|
|
4260
4981
|
};
|
|
4261
4982
|
const downloads = () => options.downloadedFiles?.() === true;
|
|
4983
|
+
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.";
|
|
4262
4984
|
return {
|
|
4263
4985
|
kind: "table-query",
|
|
4264
4986
|
listToolSpecs: () => Promise.resolve([downloads() ? {
|
|
@@ -4268,11 +4990,12 @@ function createTableQueryToolSource(options) {
|
|
|
4268
4990
|
} : {
|
|
4269
4991
|
name: QUERY_TABLE_TOOL,
|
|
4270
4992
|
description: DESCRIPTION$2,
|
|
4271
|
-
inputSchema:
|
|
4993
|
+
inputSchema: schema
|
|
4272
4994
|
}]),
|
|
4273
|
-
systemPrompt: () => Promise.resolve(downloads() ? SYSTEM_PROMPT_WITH_DOWNLOADS : SYSTEM_PROMPT$3),
|
|
4995
|
+
systemPrompt: () => Promise.resolve((downloads() ? SYSTEM_PROMPT_WITH_DOWNLOADS : SYSTEM_PROMPT$3) + exportPrompt),
|
|
4274
4996
|
canHandle: (name) => name === QUERY_TABLE_TOOL,
|
|
4275
4997
|
argCaptureTrust: (name) => name === "query_table" ? { tier: "reviewed" } : void 0,
|
|
4998
|
+
captureArgs: (_name, args) => captureTableQueryArgs(args),
|
|
4276
4999
|
call: async (name, args, context) => {
|
|
4277
5000
|
try {
|
|
4278
5001
|
if (name !== "query_table") throw new WebSkillError("TOOL_NOT_FOUND", `Unknown tool ${name}.`);
|
|
@@ -4288,26 +5011,83 @@ function createTableQueryToolSource(options) {
|
|
|
4288
5011
|
const loaded = await options.tables.load(id, context);
|
|
4289
5012
|
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.`);
|
|
4290
5013
|
const { workbook, name } = loaded;
|
|
4291
|
-
|
|
5014
|
+
const op = args["op"];
|
|
5015
|
+
const exportRequest = parseExportFile(args["exportFile"]);
|
|
5016
|
+
if (exportRequest !== void 0 && op === "describe") throw new WebSkillError("VALIDATION_FAILED", "\"exportFile\" applies to aggregate or rows, not describe.");
|
|
5017
|
+
if (exportRequest !== void 0 && options.writeArtifact === void 0) throw new WebSkillError("TOOL_UNSUPPORTED", "This host cannot write query results as Excel files.");
|
|
5018
|
+
const profileInput = {
|
|
5019
|
+
name,
|
|
5020
|
+
id,
|
|
5021
|
+
...args["sheet"] !== void 0 ? { sheet: args["sheet"] } : {},
|
|
5022
|
+
...args["headerRow"] !== void 0 ? { headerRow: args["headerRow"] } : {}
|
|
5023
|
+
};
|
|
5024
|
+
if (op === "describe") return {
|
|
4292
5025
|
ok: true,
|
|
4293
5026
|
content: [{
|
|
4294
5027
|
type: "text",
|
|
4295
|
-
text:
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
|
|
4299
|
-
...
|
|
4300
|
-
|
|
5028
|
+
text: isStreamingTableWorkbook(workbook) ? await profileStreamingTable({
|
|
5029
|
+
...profileInput,
|
|
5030
|
+
workbook
|
|
5031
|
+
}) : profileTable({
|
|
5032
|
+
...profileInput,
|
|
5033
|
+
workbook
|
|
4301
5034
|
})
|
|
4302
5035
|
}]
|
|
4303
5036
|
};
|
|
4304
|
-
|
|
5037
|
+
if (exportRequest !== void 0) {
|
|
5038
|
+
const main = await exportQuerySheet(workbook, args, exportRequest.sheetName);
|
|
5039
|
+
const spec = { sheets: [main.sheet] };
|
|
5040
|
+
const sheetSummaries = [{
|
|
5041
|
+
name: exportRequest.sheetName,
|
|
5042
|
+
rowCount: main.sheet.rows.length,
|
|
5043
|
+
matchedRows: main.matchedRows,
|
|
5044
|
+
...main.totalGroups !== void 0 ? { totalGroups: main.totalGroups } : {}
|
|
5045
|
+
}];
|
|
5046
|
+
for (const additional of exportRequest.additionalSheets) if ("query" in additional) {
|
|
5047
|
+
const result = await exportQuerySheet(workbook, additional.query, additional.name);
|
|
5048
|
+
spec.sheets.push(result.sheet);
|
|
5049
|
+
sheetSummaries.push({
|
|
5050
|
+
name: additional.name,
|
|
5051
|
+
rowCount: result.sheet.rows.length,
|
|
5052
|
+
matchedRows: result.matchedRows,
|
|
5053
|
+
...result.totalGroups !== void 0 ? { totalGroups: result.totalGroups } : {}
|
|
5054
|
+
});
|
|
5055
|
+
} else {
|
|
5056
|
+
spec.sheets.push({
|
|
5057
|
+
name: additional.name,
|
|
5058
|
+
...additional.columns !== void 0 ? { columns: additional.columns } : {},
|
|
5059
|
+
rows: additional.rows
|
|
5060
|
+
});
|
|
5061
|
+
sheetSummaries.push({
|
|
5062
|
+
name: additional.name,
|
|
5063
|
+
rowCount: additional.rows.length
|
|
5064
|
+
});
|
|
5065
|
+
}
|
|
5066
|
+
const bytes = await encodeSpreadsheet(spec);
|
|
5067
|
+
const artifact = await options.writeArtifact(exportRequest.fileName, bytes, SPREADSHEET_MIME_TYPE, context);
|
|
5068
|
+
return {
|
|
5069
|
+
ok: true,
|
|
5070
|
+
content: [{
|
|
5071
|
+
type: "json",
|
|
5072
|
+
data: {
|
|
5073
|
+
path: artifact.path,
|
|
5074
|
+
size: artifact.size,
|
|
5075
|
+
sheetCount: spec.sheets.length,
|
|
5076
|
+
rowCount: spec.sheets.reduce((total, sheet) => total + sheet.rows.length, 0),
|
|
5077
|
+
matchedRows: main.matchedRows,
|
|
5078
|
+
...main.totalGroups !== void 0 ? { totalGroups: main.totalGroups } : {},
|
|
5079
|
+
...exportRequest.additionalSheets.length > 0 ? { sheets: sheetSummaries } : {}
|
|
5080
|
+
}
|
|
5081
|
+
}]
|
|
5082
|
+
};
|
|
5083
|
+
}
|
|
5084
|
+
const view = isStreamingTableWorkbook(workbook) ? await buildStreamingView(workbook, args["sheet"], args["headerRow"]) : buildView(workbook, args["sheet"], args["headerRow"]);
|
|
4305
5085
|
const query = parseQuery(args, view);
|
|
4306
|
-
const output = query.op === "aggregate" ? runAggregate(view, query) : runRows(view, query);
|
|
5086
|
+
const output = query.op === "aggregate" ? await runAggregate(view, query, false) : await runRows(view, query, false);
|
|
4307
5087
|
const head = {
|
|
4308
5088
|
sheet: view.sheetName,
|
|
4309
5089
|
headerRow: view.headerRow,
|
|
4310
|
-
dataRows:
|
|
5090
|
+
dataRows: output.dataRows,
|
|
4311
5091
|
matchedRows: output.matchedRows,
|
|
4312
5092
|
...output.totalGroups !== void 0 ? { totalGroups: output.totalGroups } : {},
|
|
4313
5093
|
...output.columns !== void 0 ? { columns: output.columns } : {},
|
|
@@ -4315,10 +5095,11 @@ function createTableQueryToolSource(options) {
|
|
|
4315
5095
|
...output.unbucketed !== void 0 ? { unbucketed: output.unbucketed } : {},
|
|
4316
5096
|
...Object.keys(output.uncomparable).length > 0 ? { uncomparable: output.uncomparable } : {},
|
|
4317
5097
|
...output.cellsTruncated ? { cellsTruncated: true } : {},
|
|
5098
|
+
...output.pivotColumnsTruncated ? { pivotColumnsTruncated: true } : {},
|
|
4318
5099
|
...query.limitClamped ? { limitClamped: QUERY_LIMITS.maxRows } : {}
|
|
4319
5100
|
};
|
|
4320
5101
|
const items = output.groups ?? output.rows ?? [];
|
|
4321
|
-
const more = output.groups !== void 0 ? (output.totalGroups ?? 0) > output.groups.length : query.offset + items.length < output.matchedRows;
|
|
5102
|
+
const more = output.pivotColumnsTruncated === true || (output.groups !== void 0 ? (output.totalGroups ?? 0) > output.groups.length : query.offset + items.length < output.matchedRows);
|
|
4322
5103
|
return {
|
|
4323
5104
|
ok: true,
|
|
4324
5105
|
content: [{
|
|
@@ -4593,7 +5374,7 @@ function createTemplateToolSource(options) {
|
|
|
4593
5374
|
if (typeof raw !== "string" || raw === "") throw new WebSkillError("TOOL_UNSUPPORTED", `${tool} requires the "template" id of an attachment or download.`);
|
|
4594
5375
|
const file = await files.read(raw, context);
|
|
4595
5376
|
if (file === void 0) throw new WebSkillError("TOOL_UNSUPPORTED", `No readable file with id "${raw}". It is neither an attachment of this conversation nor a download id that is still valid — a download id may have expired; list the downloads again to get a fresh one.`);
|
|
4596
|
-
const format = sniffDocumentFormat(file.bytes, file.name);
|
|
5377
|
+
const format = file.bytes[0] === 80 && file.bytes[1] === 75 ? sniffDocumentFormat(file.bytes, file.name) : void 0;
|
|
4597
5378
|
if (format !== "xlsx" && format !== "docx") throw new WebSkillError("TEMPLATE_UNSUPPORTED", `"${file.name}" is not an Excel (.xlsx) or Word (.docx) file, so it cannot be used as a template.`);
|
|
4598
5379
|
if ((format === "xlsx" ? xlsx : docx) === void 0) throw new WebSkillError("TEMPLATE_UNSUPPORTED", `This environment cannot fill ${format} templates.`);
|
|
4599
5380
|
return {
|
|
@@ -5159,4 +5940,4 @@ function createDwgToolSource(options) {
|
|
|
5159
5940
|
}
|
|
5160
5941
|
|
|
5161
5942
|
//#endregion
|
|
5162
|
-
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 };
|
|
5943
|
+
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 };
|