@qvac/harness 0.8.0 → 0.8.5
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/CHANGELOG.md +43 -1
- package/dist-lib/lib/docx/index.d.ts +1 -1
- package/dist-lib/lib/docx/index.js +16 -24
- package/dist-lib/lib/office/extract.d.ts +9 -0
- package/dist-lib/lib/office/extract.js +19 -0
- package/dist-lib/lib/office/index.d.ts +1 -1
- package/dist-lib/lib/office/index.js +74 -19
- package/dist-lib/lib/tools/exec/adapters/python/module-distributions.js +0 -3
- package/dist-lib/lib/tools/exec/adapters/python/python-adapter.d.ts +7 -50
- package/dist-lib/lib/tools/exec/adapters/python/python-adapter.js +38 -695
- package/dist-lib/lib/tools/exec/adapters/python/run-session.js +1 -85
- package/dist-lib/lib/tools/exec.js +9 -20
- package/dist-lib/lib/tools/office/ops.d.ts +13 -0
- package/dist-lib/lib/tools/office/ops.js +100 -0
- package/dist-lib/lib/tools/office.d.ts +12 -0
- package/dist-lib/lib/tools/office.js +486 -0
- package/package.json +10 -6
- package/wheels/manifest.json +1 -160
- package/worker/harness-sidecar.mjs +3 -1
- package/dist-lib/lib/office/pptx.d.ts +0 -1
- package/dist-lib/lib/office/pptx.js +0 -26
- package/dist-lib/lib/office/python.d.ts +0 -8
- package/dist-lib/lib/office/python.js +0 -32
- package/dist-lib/lib/office/xlsx.d.ts +0 -1
- package/dist-lib/lib/office/xlsx.js +0 -26
- package/dist-lib/lib/tools/exec/adapters/python/create-effect.d.ts +0 -7
- package/dist-lib/lib/tools/exec/adapters/python/create-effect.js +0 -318
- package/wheels/cffi-2.0.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl +0 -0
- package/wheels/cryptography-47.0.0-cp314-abi3-pyemscripten_2026_0_wasm32.whl +0 -0
- package/wheels/defusedxml-0.7.1-py2.py3-none-any.whl +0 -0
- package/wheels/et_xmlfile-2.0.0-py3-none-any.whl +0 -0
- package/wheels/fonttools-4.62.1-py3-none-any.whl +0 -0
- package/wheels/fpdf2-2.8.8-py3-none-any.whl +0 -0
- package/wheels/lxml-6.0.2-cp314-cp314-pyemscripten_2026_0_wasm32.whl +0 -0
- package/wheels/openpyxl-3.1.5-py2.py3-none-any.whl +0 -0
- package/wheels/pillow-12.2.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl +0 -0
- package/wheels/pycparser-3.0-py3-none-any.whl +0 -0
- package/wheels/pypdf-6.15.0-py3-none-any.whl +0 -0
- package/wheels/python_docx-1.2.0-py3-none-any.whl +0 -0
- package/wheels/python_pptx-1.0.2-py3-none-any.whl +0 -0
- package/wheels/six-1.17.0-py2.py3-none-any.whl +0 -0
- package/wheels/typing_extensions-4.15.0-py3-none-any.whl +0 -0
- package/wheels/xlsxwriter-3.2.9-py3-none-any.whl +0 -0
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
import b4a from 'b4a';
|
|
2
|
+
import { errorMessage, isRecord } from '../guards.js';
|
|
3
|
+
import { docx, pptx, units, xlsx } from '../office/engine/index.js';
|
|
4
|
+
import { stageInputs } from './exec/adapters/python/attachments.js';
|
|
5
|
+
import { checkOutputBytes, RUN_OUTPUT_BUDGET_BYTES } from './exec/adapters/python/output-policy.js';
|
|
6
|
+
import { DOCX_OPS, opArguments, opKeys, PPTX_OPS, withAliases, XLSX_OPS } from './office/ops.js';
|
|
7
|
+
const READ_CELL_LIMIT = 10_000;
|
|
8
|
+
const attachedThisTurn = new WeakMap();
|
|
9
|
+
const FIRST_SHEET = /^sheet ?1$/i;
|
|
10
|
+
const SHAPE_ROLES = new Set(['title', 'subtitle', 'body']);
|
|
11
|
+
const HOISTED = ['attachmentId', 'output'];
|
|
12
|
+
const WHERE_IDS_ARE = 'copy the id from the [Attached file "…" — attachmentId: …] line or an earlier tool result';
|
|
13
|
+
function isObject(value) {
|
|
14
|
+
return isRecord(value) && !Array.isArray(value);
|
|
15
|
+
}
|
|
16
|
+
export function shapeFor(deck, op) {
|
|
17
|
+
const { shape, slide } = op;
|
|
18
|
+
if (typeof shape !== 'string' || SHAPE_ROLES.has(shape) || typeof slide !== 'number') {
|
|
19
|
+
return op;
|
|
20
|
+
}
|
|
21
|
+
if (/^\d+$/.test(shape)) {
|
|
22
|
+
return { ...op, shape: Number(shape) };
|
|
23
|
+
}
|
|
24
|
+
const wanted = shape.trim().toLowerCase();
|
|
25
|
+
const found = deck.read()[slide]?.shapes.find((entry) => entry.name.toLowerCase() === wanted);
|
|
26
|
+
return found && found.id !== null ? { ...op, shape: found.id } : op;
|
|
27
|
+
}
|
|
28
|
+
export function sheetFor(book, op, created) {
|
|
29
|
+
if (typeof op.sheet !== 'string')
|
|
30
|
+
return op;
|
|
31
|
+
const sheets = book.sheets();
|
|
32
|
+
if (sheets.some((sheet) => sheet.name === op.sheet)) {
|
|
33
|
+
return op;
|
|
34
|
+
}
|
|
35
|
+
const only = sheets.length === 1 ? sheets[0] : undefined;
|
|
36
|
+
if (!only) {
|
|
37
|
+
return op;
|
|
38
|
+
}
|
|
39
|
+
if (FIRST_SHEET.test(op.sheet)) {
|
|
40
|
+
return { ...op, sheet: only.name };
|
|
41
|
+
}
|
|
42
|
+
if (created && FIRST_SHEET.test(only.name) && book.read(only.index).rows.length === 0) {
|
|
43
|
+
book.renameSheet(only.index, op.sheet);
|
|
44
|
+
}
|
|
45
|
+
return op;
|
|
46
|
+
}
|
|
47
|
+
function readWorkbook(book) {
|
|
48
|
+
const sheets = book.sheets();
|
|
49
|
+
const read = [];
|
|
50
|
+
let budget = READ_CELL_LIMIT;
|
|
51
|
+
for (const sheet of sheets) {
|
|
52
|
+
if (sheet.kind !== 'worksheet')
|
|
53
|
+
continue;
|
|
54
|
+
if (budget <= 0) {
|
|
55
|
+
read.push({ name: sheet.name, range: sheet.range ?? null, truncated: true });
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const contents = book.read(sheet.index, { limit: budget });
|
|
59
|
+
budget -= contents.rows.reduce((cells, row) => cells + row.length, 0);
|
|
60
|
+
read.push(contents);
|
|
61
|
+
}
|
|
62
|
+
return { sheets, read };
|
|
63
|
+
}
|
|
64
|
+
const PPTX_FORMAT = {
|
|
65
|
+
name: 'pptx',
|
|
66
|
+
noun: 'PowerPoint deck',
|
|
67
|
+
ops: PPTX_OPS,
|
|
68
|
+
createKeys: ['size', 'title', 'fonts', 'colors'],
|
|
69
|
+
open: (bytes) => pptx.open(bytes),
|
|
70
|
+
create: (options) => pptx.create(options),
|
|
71
|
+
read: (deck) => deck.read(),
|
|
72
|
+
count: (deck) => ({ slides: deck.slideCount }),
|
|
73
|
+
prepare: (deck, op) => shapeFor(deck, op)
|
|
74
|
+
};
|
|
75
|
+
const DOCX_FORMAT = {
|
|
76
|
+
name: 'docx',
|
|
77
|
+
noun: 'Word document',
|
|
78
|
+
ops: DOCX_OPS,
|
|
79
|
+
createKeys: ['title', 'page', 'size', 'fonts'],
|
|
80
|
+
open: (bytes) => docx.open(bytes),
|
|
81
|
+
create: (options) => docx.create(options),
|
|
82
|
+
read: (doc) => doc.read(),
|
|
83
|
+
count: (doc) => ({ blocks: doc.blockCount })
|
|
84
|
+
};
|
|
85
|
+
const XLSX_FORMAT = {
|
|
86
|
+
name: 'xlsx',
|
|
87
|
+
noun: 'Excel workbook',
|
|
88
|
+
ops: XLSX_OPS,
|
|
89
|
+
createKeys: ['sheets', 'title'],
|
|
90
|
+
open: (bytes) => xlsx.open(bytes),
|
|
91
|
+
create: (options) => xlsx.create(options),
|
|
92
|
+
read: readWorkbook,
|
|
93
|
+
count: (book) => ({ sheets: book.sheets().length }),
|
|
94
|
+
prepare: (book, op, created) => sheetFor(book, op, created),
|
|
95
|
+
beforeSave: (book) => book.hasMacros
|
|
96
|
+
? 'the workbook has macros, which cannot be saved as .xlsx — add a removeMacros op'
|
|
97
|
+
: null
|
|
98
|
+
};
|
|
99
|
+
export function normalizeArgs(args) {
|
|
100
|
+
if (!Array.isArray(args.ops))
|
|
101
|
+
return args;
|
|
102
|
+
const lifted = { ...args };
|
|
103
|
+
const ops = [];
|
|
104
|
+
for (const entry of args.ops) {
|
|
105
|
+
const op = typeof entry === 'string' ? { op: entry } : entry;
|
|
106
|
+
if (!isObject(op)) {
|
|
107
|
+
ops.push(op);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const rest = { ...op };
|
|
111
|
+
for (const key of HOISTED) {
|
|
112
|
+
if (key in rest && lifted[key] === undefined) {
|
|
113
|
+
lifted[key] = rest[key];
|
|
114
|
+
delete rest[key];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (rest.op === 'create' && lifted.create === undefined && lifted.attachmentId === undefined) {
|
|
118
|
+
const { op: _create, ...options } = rest;
|
|
119
|
+
lifted.create = options;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
ops.push(rest);
|
|
123
|
+
}
|
|
124
|
+
lifted.ops = ops;
|
|
125
|
+
return lifted;
|
|
126
|
+
}
|
|
127
|
+
function parseOp(format, op, i) {
|
|
128
|
+
if (!isObject(op) || typeof op.op !== 'string') {
|
|
129
|
+
return `ops[${i}] must be { op, ...args }`;
|
|
130
|
+
}
|
|
131
|
+
const spec = format.ops[op.op];
|
|
132
|
+
if (!spec) {
|
|
133
|
+
return `ops[${i}]: unknown op "${op.op}"; ops are ${Object.keys(format.ops).join(', ')}`;
|
|
134
|
+
}
|
|
135
|
+
const keys = opKeys(spec);
|
|
136
|
+
const unknown = Object.keys(op).filter((key) => key !== 'op' && !keys.includes(key));
|
|
137
|
+
if (!unknown.length) {
|
|
138
|
+
return { name: op.op, spec, args: withAliases(spec, op) };
|
|
139
|
+
}
|
|
140
|
+
if (op.op === 'sheets' && 'name' in op && format.ops.addSheet) {
|
|
141
|
+
return `ops[${i}] sheets lists the sheets and takes no arguments; to add a sheet use { op: "addSheet", name }`;
|
|
142
|
+
}
|
|
143
|
+
const takes = keys.length ? `it takes ${keys.join(', ')}` : 'it takes no arguments';
|
|
144
|
+
return `ops[${i}] ${op.op} does not take ${unknown.join(', ')}; ${takes}`;
|
|
145
|
+
}
|
|
146
|
+
function parseOps(format, ops) {
|
|
147
|
+
if (!Array.isArray(ops)) {
|
|
148
|
+
return 'ops must be an array of { op, ...args }';
|
|
149
|
+
}
|
|
150
|
+
const parsed = [];
|
|
151
|
+
for (const [i, op] of ops.entries()) {
|
|
152
|
+
const result = parseOp(format, op, i);
|
|
153
|
+
if (typeof result === 'string') {
|
|
154
|
+
return result;
|
|
155
|
+
}
|
|
156
|
+
parsed.push(result);
|
|
157
|
+
}
|
|
158
|
+
return parsed;
|
|
159
|
+
}
|
|
160
|
+
function argumentError(format, args) {
|
|
161
|
+
const { attachmentId, create, output } = args;
|
|
162
|
+
if (attachmentId !== undefined && (typeof attachmentId !== 'string' || !attachmentId)) {
|
|
163
|
+
return 'attachmentId must be a non-empty string';
|
|
164
|
+
}
|
|
165
|
+
if (create !== undefined && !isObject(create)) {
|
|
166
|
+
return 'create must be an object';
|
|
167
|
+
}
|
|
168
|
+
if (attachmentId !== undefined && create !== undefined) {
|
|
169
|
+
return `pass attachmentId to open a ${format.noun}, or create to start one — not both`;
|
|
170
|
+
}
|
|
171
|
+
const unknownCreate = Object.keys(create ?? {}).filter((key) => !format.createKeys.includes(key));
|
|
172
|
+
if (unknownCreate.length) {
|
|
173
|
+
return `create does not take ${unknownCreate.join(', ')}; it takes ${format.createKeys.join(', ')}`;
|
|
174
|
+
}
|
|
175
|
+
if (output !== undefined && (typeof output !== 'string' || !output)) {
|
|
176
|
+
return 'output must be a file name';
|
|
177
|
+
}
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
function parseRequest(format, raw) {
|
|
181
|
+
const args = normalizeArgs(raw);
|
|
182
|
+
const error = argumentError(format, args);
|
|
183
|
+
if (error) {
|
|
184
|
+
return error;
|
|
185
|
+
}
|
|
186
|
+
const ops = parseOps(format, args.ops ?? []);
|
|
187
|
+
if (typeof ops === 'string') {
|
|
188
|
+
return ops;
|
|
189
|
+
}
|
|
190
|
+
const attachmentId = typeof args.attachmentId === 'string' ? args.attachmentId : undefined;
|
|
191
|
+
if (attachmentId === undefined && !ops.length) {
|
|
192
|
+
return `nothing to read: pass the attachmentId of a ${format.noun} (${WHERE_IDS_ARE}), or ops to build one`;
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
attachmentId,
|
|
196
|
+
create: isObject(args.create) ? args.create : undefined,
|
|
197
|
+
ops,
|
|
198
|
+
output: typeof args.output === 'string' ? args.output : undefined
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
async function sourceDocument(format, request, ctx) {
|
|
202
|
+
if (request.attachmentId === undefined) {
|
|
203
|
+
return { document: format.create(request.create ?? {}), fileName: null };
|
|
204
|
+
}
|
|
205
|
+
const found = await ctx.fetchAttachment?.({
|
|
206
|
+
attachmentId: request.attachmentId,
|
|
207
|
+
signal: ctx.signal
|
|
208
|
+
});
|
|
209
|
+
if (!found || !('bytes' in found)) {
|
|
210
|
+
return {
|
|
211
|
+
error: `attachment ${request.attachmentId} not found in this chat; ${WHERE_IDS_ARE}, not a file name`
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
return { document: format.open(found.bytes), fileName: found.fileName };
|
|
215
|
+
}
|
|
216
|
+
export function revisedName(format, fileName) {
|
|
217
|
+
const dot = fileName.lastIndexOf('.');
|
|
218
|
+
const stem = dot > 0 ? fileName.slice(0, dot) : fileName;
|
|
219
|
+
if (format === 'pptx') {
|
|
220
|
+
const version = /^(.*)-v(\d+)$/.exec(stem);
|
|
221
|
+
return version ? `${version[1]}-v${Number(version[2]) + 1}.pptx` : `${stem}-v2.pptx`;
|
|
222
|
+
}
|
|
223
|
+
const revised = /^(.*)_revised(?:-(\d+))?$/.exec(stem);
|
|
224
|
+
if (!revised) {
|
|
225
|
+
return `${stem}_revised.${format}`;
|
|
226
|
+
}
|
|
227
|
+
return `${revised[1]}_revised-${Number(revised[2] ?? 1) + 1}.${format}`;
|
|
228
|
+
}
|
|
229
|
+
function outputFor(format, output, input) {
|
|
230
|
+
if (!input)
|
|
231
|
+
return output;
|
|
232
|
+
if (output && output.toLowerCase() !== input.toLowerCase())
|
|
233
|
+
return output;
|
|
234
|
+
return revisedName(format, input);
|
|
235
|
+
}
|
|
236
|
+
async function imageId(ctx, id) {
|
|
237
|
+
const found = await ctx.fetchAttachment?.({ attachmentId: id, meta: true, signal: ctx.signal });
|
|
238
|
+
if (found) {
|
|
239
|
+
return id;
|
|
240
|
+
}
|
|
241
|
+
const latest = await ctx.fetchAttachment?.({ meta: true, signal: ctx.signal });
|
|
242
|
+
return latest && latest.fileName.toLowerCase() === id.toLowerCase() ? undefined : id;
|
|
243
|
+
}
|
|
244
|
+
async function imageInput(op, i, ctx) {
|
|
245
|
+
const image = op.args.image ?? {};
|
|
246
|
+
if (!isObject(image)) {
|
|
247
|
+
return { error: `ops[${i}]: image must be { attachmentId } or {}` };
|
|
248
|
+
}
|
|
249
|
+
const id = image.attachmentId;
|
|
250
|
+
if (id !== undefined && (typeof id !== 'string' || !id)) {
|
|
251
|
+
return { error: `ops[${i}]: image.attachmentId must be a non-empty string` };
|
|
252
|
+
}
|
|
253
|
+
return { attachmentId: id ? await imageId(ctx, id) : undefined, path: `image-${i}.png` };
|
|
254
|
+
}
|
|
255
|
+
async function stagedImages(request, ctx) {
|
|
256
|
+
const wanted = request.ops.flatMap((op, i) => (opKeys(op.spec).includes('image') ? [i] : []));
|
|
257
|
+
const images = new Map();
|
|
258
|
+
if (!wanted.length) {
|
|
259
|
+
return images;
|
|
260
|
+
}
|
|
261
|
+
if (!ctx.fetchAttachment) {
|
|
262
|
+
return { error: 'this device cannot read chat images' };
|
|
263
|
+
}
|
|
264
|
+
const inputs = [];
|
|
265
|
+
for (const i of wanted) {
|
|
266
|
+
const op = request.ops[i];
|
|
267
|
+
const input = op ? await imageInput(op, i, ctx) : { error: `ops[${i}] is missing` };
|
|
268
|
+
if ('error' in input) {
|
|
269
|
+
return input;
|
|
270
|
+
}
|
|
271
|
+
inputs.push(input);
|
|
272
|
+
}
|
|
273
|
+
const staged = await stageInputs(ctx.fetchAttachment, inputs, {
|
|
274
|
+
turnImages: ctx.turnImages,
|
|
275
|
+
signal: ctx.signal
|
|
276
|
+
});
|
|
277
|
+
if ('error' in staged) {
|
|
278
|
+
return staged;
|
|
279
|
+
}
|
|
280
|
+
for (const [n, file] of staged.files.entries()) {
|
|
281
|
+
const at = wanted[n];
|
|
282
|
+
if (at !== undefined) {
|
|
283
|
+
images.set(at, file.bytes);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return images;
|
|
287
|
+
}
|
|
288
|
+
function saveError({ format, document, output }, ctx) {
|
|
289
|
+
if (!output) {
|
|
290
|
+
return `the ops changed the ${format.noun}; name the file in output`;
|
|
291
|
+
}
|
|
292
|
+
if (!output.toLowerCase().endsWith(`.${format.name}`)) {
|
|
293
|
+
return `output must end in .${format.name}, got "${output}"`;
|
|
294
|
+
}
|
|
295
|
+
const refusal = format.beforeSave?.(document);
|
|
296
|
+
if (refusal) {
|
|
297
|
+
return refusal;
|
|
298
|
+
}
|
|
299
|
+
return ctx.attach ? null : `this device cannot attach a ${format.noun} to the chat`;
|
|
300
|
+
}
|
|
301
|
+
async function attachOutput(job, ctx) {
|
|
302
|
+
const refused = saveError(job, ctx);
|
|
303
|
+
if (refused || !job.output || !ctx.attach) {
|
|
304
|
+
return { error: refused ?? 'nothing to save' };
|
|
305
|
+
}
|
|
306
|
+
const { output, document } = job;
|
|
307
|
+
const delivered = attachedThisTurn.get(ctx.attach) ?? new Map();
|
|
308
|
+
const earlier = delivered.get(output.toLowerCase());
|
|
309
|
+
const bytes = document.save();
|
|
310
|
+
if (earlier && b4a.equals(earlier.bytes, bytes)) {
|
|
311
|
+
return { attachment: earlier.attachment, repeated: true };
|
|
312
|
+
}
|
|
313
|
+
if (earlier) {
|
|
314
|
+
return {
|
|
315
|
+
error: `${earlier.attachment.fileName} is already attached with different content; save this version under a new name in output`
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
const verdict = checkOutputBytes(output, bytes, 0);
|
|
319
|
+
if ('error' in verdict) {
|
|
320
|
+
return verdict;
|
|
321
|
+
}
|
|
322
|
+
const budget = ctx.runBudget;
|
|
323
|
+
if (budget && budget.outputBytesUsed + bytes.byteLength > RUN_OUTPUT_BUDGET_BYTES) {
|
|
324
|
+
return { error: 'run output budget exceeded' };
|
|
325
|
+
}
|
|
326
|
+
const record = await ctx.attach({
|
|
327
|
+
fileName: output,
|
|
328
|
+
mimeType: verdict.mimeType,
|
|
329
|
+
bytes,
|
|
330
|
+
raw: true
|
|
331
|
+
});
|
|
332
|
+
if (budget) {
|
|
333
|
+
budget.outputBytesUsed += bytes.byteLength;
|
|
334
|
+
}
|
|
335
|
+
const attachment = {
|
|
336
|
+
attachmentId: record.id,
|
|
337
|
+
fileName: record.fileName,
|
|
338
|
+
byteLength: record.byteLength
|
|
339
|
+
};
|
|
340
|
+
delivered.set(output.toLowerCase(), { attachment, bytes });
|
|
341
|
+
attachedThisTurn.set(ctx.attach, delivered);
|
|
342
|
+
return { attachment, repeated: false };
|
|
343
|
+
}
|
|
344
|
+
function runOp(document, op, input) {
|
|
345
|
+
const method = Reflect.get(document, op.name);
|
|
346
|
+
if (typeof method !== 'function') {
|
|
347
|
+
throw new Error(`${op.name} is not available on this file`);
|
|
348
|
+
}
|
|
349
|
+
return Reflect.apply(method, document, opArguments(op.spec, input, units.inches));
|
|
350
|
+
}
|
|
351
|
+
function runOps({ format, document, request, images }) {
|
|
352
|
+
const results = [];
|
|
353
|
+
const created = request.attachmentId === undefined;
|
|
354
|
+
for (const [i, op] of request.ops.entries()) {
|
|
355
|
+
const placed = format.prepare?.(document, op.args, created) ?? op.args;
|
|
356
|
+
const input = images.has(i) ? { ...placed, image: images.get(i) } : placed;
|
|
357
|
+
try {
|
|
358
|
+
results.push(runOp(document, op, input));
|
|
359
|
+
}
|
|
360
|
+
catch (err) {
|
|
361
|
+
return { error: `ops[${i}] ${op.name}: ${errorMessage(err)}` };
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return results;
|
|
365
|
+
}
|
|
366
|
+
function countLine(counts) {
|
|
367
|
+
return Object.entries(counts)
|
|
368
|
+
.map(([noun, n]) => `${n} ${n === 1 ? noun.replace(/s$/, '') : noun}`)
|
|
369
|
+
.join(', ');
|
|
370
|
+
}
|
|
371
|
+
function describe(format) {
|
|
372
|
+
const queries = Object.keys(format.ops).filter((name) => format.ops[name]?.query);
|
|
373
|
+
const edits = Object.keys(format.ops).filter((name) => !format.ops[name]?.query);
|
|
374
|
+
const inches = Object.values(format.ops).some((spec) => spec.inches)
|
|
375
|
+
? ' Positions and sizes (x, y, width, height) are inches.'
|
|
376
|
+
: '';
|
|
377
|
+
return (`Read, edit or create a ${format.noun} (.${format.name}). With attachmentId alone it ` +
|
|
378
|
+
`returns the ${format.noun}'s contents. ops run office engine methods in order, each ` +
|
|
379
|
+
`{ op, ...named args }: ${queries.join(', ')} return data; ${edits.join(', ')} change the ` +
|
|
380
|
+
'file. When anything changed, the result is saved as a chat attachment named output and ' +
|
|
381
|
+
'shown to the user automatically. A failing op saves nothing and its error names what ' +
|
|
382
|
+
`exists, so fix the call from it.${inches}`);
|
|
383
|
+
}
|
|
384
|
+
function parameters(format) {
|
|
385
|
+
return {
|
|
386
|
+
type: 'object',
|
|
387
|
+
properties: {
|
|
388
|
+
attachmentId: {
|
|
389
|
+
type: 'string',
|
|
390
|
+
description: `The ${format.noun} in this chat; omit it to create a new one`
|
|
391
|
+
},
|
|
392
|
+
create: {
|
|
393
|
+
type: 'object',
|
|
394
|
+
description: `Options for a new ${format.noun}: ${format.createKeys.join(', ')}`
|
|
395
|
+
},
|
|
396
|
+
ops: {
|
|
397
|
+
type: 'array',
|
|
398
|
+
description: 'Operations to run in order, each { op, ...named args }',
|
|
399
|
+
items: {
|
|
400
|
+
type: 'object',
|
|
401
|
+
properties: { op: { type: 'string', enum: Object.keys(format.ops) } },
|
|
402
|
+
required: ['op']
|
|
403
|
+
}
|
|
404
|
+
},
|
|
405
|
+
output: {
|
|
406
|
+
type: 'string',
|
|
407
|
+
description: `File name for the saved ${format.noun}, ending in .${format.name}`
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
async function execute(format, args, ctx) {
|
|
413
|
+
const request = parseRequest(format, args);
|
|
414
|
+
if (typeof request === 'string') {
|
|
415
|
+
return { error: request };
|
|
416
|
+
}
|
|
417
|
+
if (request.attachmentId !== undefined && !ctx.fetchAttachment) {
|
|
418
|
+
return { error: 'this device cannot read chat attachments' };
|
|
419
|
+
}
|
|
420
|
+
const images = await stagedImages(request, ctx);
|
|
421
|
+
if ('error' in images) {
|
|
422
|
+
return images;
|
|
423
|
+
}
|
|
424
|
+
let source;
|
|
425
|
+
try {
|
|
426
|
+
source = await sourceDocument(format, request, ctx);
|
|
427
|
+
}
|
|
428
|
+
catch (err) {
|
|
429
|
+
return { error: errorMessage(err) };
|
|
430
|
+
}
|
|
431
|
+
if ('error' in source) {
|
|
432
|
+
return source;
|
|
433
|
+
}
|
|
434
|
+
const { document, fileName } = source;
|
|
435
|
+
if (!request.ops.length) {
|
|
436
|
+
return { document: format.read(document) };
|
|
437
|
+
}
|
|
438
|
+
const results = runOps({ format, document, request, images });
|
|
439
|
+
if ('error' in results) {
|
|
440
|
+
return results;
|
|
441
|
+
}
|
|
442
|
+
if (!document.changed && request.attachmentId !== undefined) {
|
|
443
|
+
return { results };
|
|
444
|
+
}
|
|
445
|
+
const output = outputFor(format.name, request.output, fileName);
|
|
446
|
+
const saved = await attachOutput({ format, document, output }, ctx);
|
|
447
|
+
if ('error' in saved) {
|
|
448
|
+
return saved;
|
|
449
|
+
}
|
|
450
|
+
const { attachment, repeated } = saved;
|
|
451
|
+
const counts = format.count(document);
|
|
452
|
+
const renamed = output !== request.output
|
|
453
|
+
? `Saved as ${output}, so the attached ${fileName} keeps its own name. `
|
|
454
|
+
: '';
|
|
455
|
+
return {
|
|
456
|
+
results,
|
|
457
|
+
attachments: [attachment],
|
|
458
|
+
...counts,
|
|
459
|
+
reply: `${attachment.fileName} — ${countLine(counts)}`,
|
|
460
|
+
next: renamed +
|
|
461
|
+
`Done: ${attachment.fileName} ${repeated ? 'was already' : 'is'} attached to the chat and shown to the user. ` +
|
|
462
|
+
`Do not call ${format.name} again for it. Your answer is the reply value alone, word for word.`
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
function createOfficeTool(format) {
|
|
466
|
+
return {
|
|
467
|
+
name: format.name,
|
|
468
|
+
toolset: null,
|
|
469
|
+
skillGated: true,
|
|
470
|
+
description: describe(format),
|
|
471
|
+
parameters: parameters(format),
|
|
472
|
+
effect: () => 'read',
|
|
473
|
+
validate(args) {
|
|
474
|
+
const request = parseRequest(format, args);
|
|
475
|
+
return typeof request === 'string' ? request : null;
|
|
476
|
+
},
|
|
477
|
+
execute: (args, ctx = {}) => execute(format, args, ctx)
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
export function createOfficeTools() {
|
|
481
|
+
return [
|
|
482
|
+
createOfficeTool(PPTX_FORMAT),
|
|
483
|
+
createOfficeTool(DOCX_FORMAT),
|
|
484
|
+
createOfficeTool(XLSX_FORMAT)
|
|
485
|
+
];
|
|
486
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qvac/harness",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"engines": {
|
|
6
6
|
"bare": ">=1.34.0"
|
|
@@ -210,10 +210,6 @@
|
|
|
210
210
|
"types": "./dist-lib/lib/tools/exec/adapters/python/attachments.d.ts",
|
|
211
211
|
"default": "./dist-lib/lib/tools/exec/adapters/python/attachments.js"
|
|
212
212
|
},
|
|
213
|
-
"./lib/tools/exec/adapters/python/create-effect.ts": {
|
|
214
|
-
"types": "./dist-lib/lib/tools/exec/adapters/python/create-effect.d.ts",
|
|
215
|
-
"default": "./dist-lib/lib/tools/exec/adapters/python/create-effect.js"
|
|
216
|
-
},
|
|
217
213
|
"./lib/tools/exec/adapters/python/deadline-state.ts": {
|
|
218
214
|
"types": "./dist-lib/lib/tools/exec/adapters/python/deadline-state.d.ts",
|
|
219
215
|
"default": "./dist-lib/lib/tools/exec/adapters/python/deadline-state.js"
|
|
@@ -454,6 +450,14 @@
|
|
|
454
450
|
"types": "./dist-lib/lib/tools/model-args.d.ts",
|
|
455
451
|
"default": "./dist-lib/lib/tools/model-args.js"
|
|
456
452
|
},
|
|
453
|
+
"./lib/tools/office.ts": {
|
|
454
|
+
"types": "./dist-lib/lib/tools/office.d.ts",
|
|
455
|
+
"default": "./dist-lib/lib/tools/office.js"
|
|
456
|
+
},
|
|
457
|
+
"./lib/tools/office/ops.ts": {
|
|
458
|
+
"types": "./dist-lib/lib/tools/office/ops.d.ts",
|
|
459
|
+
"default": "./dist-lib/lib/tools/office/ops.js"
|
|
460
|
+
},
|
|
457
461
|
"./lib/tools/operations/builders.ts": {
|
|
458
462
|
"types": "./dist-lib/lib/tools/operations/builders.d.ts",
|
|
459
463
|
"default": "./dist-lib/lib/tools/operations/builders.js"
|
|
@@ -743,7 +747,7 @@
|
|
|
743
747
|
"@qvac/inference": "0.20.0",
|
|
744
748
|
"@qvac/langdetect-text": "0.1.2",
|
|
745
749
|
"@qvac/sdk": "0.20.0",
|
|
746
|
-
"@qvac/skills": "0.1.
|
|
750
|
+
"@qvac/skills": "0.1.12",
|
|
747
751
|
"@xmldom/xmldom": "^0.9.12",
|
|
748
752
|
"asenion": "^0.1.0",
|
|
749
753
|
"b4a": "^1.9.0",
|