bunnyquery 1.5.7 → 1.6.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/bunnyquery.js +141 -7
- package/dist/engine.cjs +142 -6
- package/dist/engine.cjs.map +1 -1
- package/dist/engine.d.mts +33 -1
- package/dist/engine.d.ts +33 -1
- package/dist/engine.mjs +141 -7
- package/dist/engine.mjs.map +1 -1
- package/package.json +1 -1
- package/src/engine/office.ts +25 -0
- package/src/engine/prompts/index.ts +2 -0
- package/src/engine/prompts/indexing_system_prompt.ts +5 -3
- package/src/engine/prompts/indexing_user_message.ts +82 -0
- package/src/engine/requests.ts +75 -17
- package/src/engine/session.ts +65 -0
package/bunnyquery.js
CHANGED
|
@@ -156,12 +156,23 @@
|
|
|
156
156
|
const m = (mime || "").toLowerCase();
|
|
157
157
|
return m === "application/pdf" || m === "application/vnd.ms-excel" || m.includes("spreadsheetml") || m.includes("opendocument.spreadsheet");
|
|
158
158
|
}
|
|
159
|
+
function isImageVisionFile(name, mime) {
|
|
160
|
+
const ext = (name || "").split(".").pop()?.toLowerCase() || "";
|
|
161
|
+
return ext === "pdf" || (mime || "").toLowerCase() === "application/pdf";
|
|
162
|
+
}
|
|
159
163
|
var _extractPlaceholderSeq = 0;
|
|
160
164
|
function makeExtractPlaceholder(seed) {
|
|
161
165
|
_extractPlaceholderSeq += 1;
|
|
162
166
|
const slug = (seed || "file").replace(/[^a-zA-Z0-9]+/g, "_").slice(-48);
|
|
163
167
|
return `{{SKAPI_FILE_CONTENT::${slug}-${_extractPlaceholderSeq}}}`;
|
|
164
168
|
}
|
|
169
|
+
var _renderPlaceholderSeq = 0;
|
|
170
|
+
function makeRenderPlaceholder(seed) {
|
|
171
|
+
_renderPlaceholderSeq += 1;
|
|
172
|
+
const slug = (seed || "file").replace(/[^a-zA-Z0-9]+/g, "_").slice(-48);
|
|
173
|
+
return `{{SKAPI_RENDER::${slug}-${_renderPlaceholderSeq}}}`;
|
|
174
|
+
}
|
|
175
|
+
var RENDER_PAGES_PER_WINDOW = 5;
|
|
165
176
|
function composeUserMessage(text, attachmentUrls) {
|
|
166
177
|
let composed = text;
|
|
167
178
|
if (attachmentUrls.length > 0) {
|
|
@@ -259,12 +270,14 @@ Project description: """${serviceDescription}"""`;
|
|
|
259
270
|
let systemPrompt = `You are a background indexing agent for project ${service}.
|
|
260
271
|
- Image files (.jpg, .jpeg, .png, .gif, .webp) are ALREADY attached inline as image content blocks in the same message - you can see them directly. Do NOT call web_fetch on image URLs; that will fail or return garbage. Just look at the image block and answer.
|
|
261
272
|
- Most files (office documents like .docx/.xlsx/.pptx/.hwp/.hwpx/.ods, and text/data/code files like .csv/.tsv/.json/.xml/.txt/.md and source code) have ALREADY been extracted on the server and included inline in the user message between the "BEGIN FILE CONTENT" / "END FILE CONTENT" markers - read that directly. If the inline content is a "[skapi: ...]" note, the file could not be extracted - index it from its metadata only.
|
|
262
|
-
- BIG
|
|
263
|
-
-
|
|
273
|
+
- BIG SPREADSHEETS / TEXT: the inline content may be only the FIRST part of a large file (it can end with a truncation or "more remains" note). For big spreadsheets and big text/data files READ THE FILE WITH THE readFileContent TOOL: it returns the file ONE WINDOW at a time (spreadsheets as coordinate-tagged grid rows, text as a range of characters). Pass the file's storage path. After each window: datafy it into records and SAVE them, THEN if the window says MORE REMAINS call readFileContent again with the cursor it gives you. Repeat until it says END OF FILE, so the WHOLE file is indexed - never stop after the first window. (Do NOT call readFileContent on a PDF - see the next line.)
|
|
274
|
+
- PDFs (scanned or not): you do NOT read a PDF with a tool or a URL. Its pages are RENDERED and embedded directly in the user message as IMAGE blocks, a WINDOW of pages at a time. LOOK at the embedded page images and datafy every one. The note beside them tells you whether MORE pages remain: if so, save this window's records and stop (a follow-up pass shows the next window automatically); only when the note says it was the LAST window is the PDF fully seen. Do NOT call readFileContent or web_fetch for a PDF.
|
|
275
|
+
- VISION: when the message (a readFileContent window, an embedded PDF page, or an inline attachment) includes IMAGES - scanned/rendered PDF pages, or photos embedded in a spreadsheet next to a row/block - LOOK at them and capture what they show as record data (the reading/values in a scanned table, the part/defect/condition visible in a photo). The image IS part of the data; correlate each photo with its labelled block ("PHOTO A3" markers tie a photo to that grid row).
|
|
264
276
|
- Whatever the file type, use the file's storage path (the "storage path" metadata line) as the "src::" unique_id - never the inline content or a temporary URL.
|
|
265
277
|
- TABULAR data (any spreadsheet - .csv/.tsv/.xlsx/.xls/.ods, or sheet-like rows): you MUST save EVERY data row as its own record (ONE record per row) with that row's actual column values in the record's "data", keyed by the header names, in a dedicated table (e.g. "spreadsheet_rows"). Do NOT summarize, sample only a few rows, or save just file metadata - index the whole sheet, paging through it with readFileContent when it is large. Make MULTIPLE postRecords calls in batches (e.g. 30-50 rows per call) rather than one oversized call. This per-row completeness OVERRIDES brevity. ALSO save one file-level summary record (file name, sheet name(s), column headers, total row count, overall summary) - this is the record that carries the file's "src::" unique_id - and link EVERY per-row record to it via reference (set each row record's reference to that src:: file record; the row records themselves do NOT carry a src:: unique_id). The per-row records AND this reference linkage are BOTH mandatory: the linkage is what lets the whole sheet be found and cleaned up together when the file is re-indexed.
|
|
266
278
|
- EPUB / e-books / long-form books (.epub or any book-length prose, provided inline in reading order with chapter headings preserved): you MUST save ONE record per CHAPTER (or, when chapters are unclear, per major section/topic) in a dedicated table (e.g. "book_chapters") - never collapse the whole book into a single record. Each chapter record's "data" must capture the chapter title plus its order/number AND a substantive summary of that chapter's content (key events, arguments, characters, places, concepts, terms, notable quotes). Apply AS MANY relevant tags as possible to EVERY chapter record (characters, locations, themes, topics, key concepts, key terms, dates, named entities) so the book is easy to SEARCH and cross-reference later - this is the whole point. ALSO save one book-level record (title, author, language, overall summary, chapter list / table of contents, genre/subjects) and link each chapter record to it via reference. This per-chapter completeness OVERRIDES brevity; human-readable summaries only, never raw/binary bytes.
|
|
267
|
-
- This is a background indexing task: do ALL the MCP saving FIRST, never reply mid-task, and never ask the user questions. Always use the MCP tools to save what you learn - be exhaustive about meaning (and, for tabular data, about every row). SAVE AS YOU GO: persist each window's records before reading the next, so progress is never lost. If the file is so large you cannot finish in one turn, still save everything you have read so far
|
|
279
|
+
- This is a background indexing task: do ALL the MCP saving FIRST, never reply mid-task, and never ask the user questions. Always use the MCP tools to save what you learn - be exhaustive about meaning (and, for tabular data, about every row). SAVE AS YOU GO: persist each window's records before reading the next, so progress is never lost. If the file is so large you cannot finish in one turn, still save everything you have read so far; a follow-up pass will automatically continue from where you stopped. Never store raw or binary bytes (base64, blobs); describe them in human-readable text instead.
|
|
280
|
+
- COMPLETION SIGNAL: only when you have fully read and saved the ENTIRE file (for readFileContent files: reached "END OF FILE"; for PDFs: the embedded page-image note said it was the LAST window - with all rows/pages/items saved), end your final message with the token INDEXING_COMPLETE on its own line. If you did NOT finish the whole file (more rows/pages remain), do NOT write that token - leaving it out is how the system knows to run another pass to continue.
|
|
268
281
|
- Only AFTER every save is done, send exactly ONE final message summarizing what you indexed - never just "Indexing complete", and never a raw/base64/binary value or a large pasted dump. Keep it to a few factual sentences or a short markdown bullet list covering: the file name, its content type, each table you wrote to with its record/row count and the key columns/fields or topics captured, and anything that could not be extracted. Follow this shape - Indexed <file name> (<content type>): saved <N> records to <table(s)> capturing <key columns/fields or topics>; could not extract: <gaps, or none>.`;
|
|
269
282
|
if (serviceDescription) {
|
|
270
283
|
systemPrompt += `
|
|
@@ -307,6 +320,48 @@ Read this file with the readFileContent tool, using the storage path above - do
|
|
|
307
320
|
}
|
|
308
321
|
return head + `- temporary URL (fetch this to read the file contents): ${attachment.url}`;
|
|
309
322
|
}
|
|
323
|
+
function buildIndexingRenderMessage(attachment, placeholder, renderFrom) {
|
|
324
|
+
const from = Math.max(0, renderFrom || 0);
|
|
325
|
+
const src = `src::${attachment.storagePath}`;
|
|
326
|
+
const meta = `File metadata:
|
|
327
|
+
- name: ${attachment.name}
|
|
328
|
+
- storage path: ${attachment.storagePath}
|
|
329
|
+
` + (attachment.mime ? `- mime type: ${attachment.mime}
|
|
330
|
+
` : "");
|
|
331
|
+
const datafy = `
|
|
332
|
+
${placeholder}
|
|
333
|
+
|
|
334
|
+
LOOK at each rendered page image in this message and DATAFY what it shows: for EVERY page call postRecords and save records - one record per row / table entry / line item visible on the page (or one record for the page if it is prose), capturing every value you can read (OCR the text, read tables cell by cell, describe any photos/diagrams). Use the storage path above for the "src::" unique_id.
|
|
335
|
+
|
|
336
|
+
The note next to the images tells you whether MORE pages remain after this window. If MORE remain: save this window's records and STOP - do NOT write INDEXING_COMPLETE; another pass shows the next window automatically. Only when the note says this is the LAST window (you have seen the whole file) AND everything is saved, end your message with the token INDEXING_COMPLETE.`;
|
|
337
|
+
if (from === 0) {
|
|
338
|
+
return `A new file has just been uploaded. Index it now.
|
|
339
|
+
|
|
340
|
+
` + meta + `
|
|
341
|
+
This is a PDF. Its pages are delivered to you as RENDERED PAGE IMAGES embedded directly in this message (you do NOT need any tool, URL, or web_fetch to see them). You are shown a WINDOW of pages at a time, starting at page ${from + 1}.
|
|
342
|
+
` + datafy;
|
|
343
|
+
}
|
|
344
|
+
return `CONTINUE indexing a PDF whose previous pass did not finish.
|
|
345
|
+
|
|
346
|
+
` + meta + `
|
|
347
|
+
Records for the earlier pages are ALREADY saved (they reference "${src}"). The NEXT window of rendered page images (starting at page ${from + 1}) is embedded in this message. Datafy each page as before and do NOT re-save pages that are already saved.
|
|
348
|
+
` + datafy;
|
|
349
|
+
}
|
|
350
|
+
function buildIndexingContinueMessage(attachment) {
|
|
351
|
+
const src = `src::${attachment.storagePath}`;
|
|
352
|
+
return `CONTINUE indexing a file whose previous pass did not finish.
|
|
353
|
+
|
|
354
|
+
File metadata:
|
|
355
|
+
- name: ${attachment.name}
|
|
356
|
+
- storage path: ${attachment.storagePath}
|
|
357
|
+
` + (attachment.mime ? `- mime type: ${attachment.mime}
|
|
358
|
+
` : "") + `
|
|
359
|
+
Records for the earlier windows/pages of this file are ALREADY saved (they reference "${src}"). First call getRecords with reference "${src}" to see how far the previous pass got (the furthest page/row/window already saved). Then call readFileContent with the storage path above and a CURSOR that RESUMES just after that point - do NOT start at the beginning. The cursor is derivable from what you already saved:
|
|
360
|
+
- PDF: the cursor is the NUMBER OF PAGES already read (0-based next page). If you saved up to page N, call readFileContent with cursor="N" to get page N+1 onward.
|
|
361
|
+
- Spreadsheet: the cursor is "<sheetIndex>:<nextRow>" (0-based sheet index, 1-based row). If you saved up to row R of sheet S, use cursor="S:R+1".
|
|
362
|
+
- Text: the cursor is the character offset already read.
|
|
363
|
+
Index the REMAINING windows - one record per row/item, looking at any page images or embedded photos - saving as you go until readFileContent reports END OF FILE. Do NOT re-save windows that are already saved. Use the storage path above for the "src::" unique_id. When the ENTIRE file is finally indexed, end your message with the token INDEXING_COMPLETE.`;
|
|
364
|
+
}
|
|
310
365
|
|
|
311
366
|
// src/engine/errors.ts
|
|
312
367
|
function getErrorMessage(input) {
|
|
@@ -808,14 +863,26 @@ Read this file with the readFileContent tool, using the storage path above - do
|
|
|
808
863
|
}
|
|
809
864
|
});
|
|
810
865
|
}
|
|
866
|
+
async function notifyAgentContinueIndexing(info) {
|
|
867
|
+
return notifyAgentSaveAttachment({ ...info, continueIndexing: true });
|
|
868
|
+
}
|
|
811
869
|
async function notifyAgentSaveAttachment(info) {
|
|
812
870
|
const { platform, service, owner, attachment, parsedContent } = info;
|
|
813
|
-
const
|
|
814
|
-
const
|
|
871
|
+
const continuing = !!info.continueIndexing;
|
|
872
|
+
const visionFile = !parsedContent && isImageVisionFile(attachment.name, attachment.mime);
|
|
873
|
+
const renderFrom = Math.max(0, info.renderFrom || 0);
|
|
874
|
+
const renderPlaceholder = visionFile ? makeRenderPlaceholder(attachment.storagePath) : void 0;
|
|
875
|
+
const skapiRender = visionFile && renderPlaceholder ? {
|
|
876
|
+
_skapi_render: [
|
|
877
|
+
{ path: attachment.storagePath, from: renderFrom, count: RENDER_PAGES_PER_WINDOW, placeholder: renderPlaceholder, name: attachment.name, mime: attachment.mime }
|
|
878
|
+
]
|
|
879
|
+
} : {};
|
|
880
|
+
const pagedRead = !visionFile && (continuing || !parsedContent && isPagedReadFile(attachment.name, attachment.mime));
|
|
881
|
+
const serverExtract = !visionFile && !continuing && !parsedContent && !pagedRead && isServerExtractable(attachment.name, attachment.mime);
|
|
815
882
|
const placeholder = serverExtract ? makeExtractPlaceholder(attachment.storagePath) : void 0;
|
|
816
883
|
const extractContent = serverExtract && placeholder ? [{ path: attachment.storagePath, placeholder, name: attachment.name, mime: attachment.mime }] : void 0;
|
|
817
884
|
const skapiExtract = extractContent && extractContent.length ? { _skapi_extract: extractContent } : {};
|
|
818
|
-
const userMessage = buildIndexingUserMessage(
|
|
885
|
+
const userMessage = visionFile && renderPlaceholder ? buildIndexingRenderMessage(attachment, renderPlaceholder, renderFrom) : continuing ? buildIndexingContinueMessage(attachment) : buildIndexingUserMessage(
|
|
819
886
|
attachment,
|
|
820
887
|
parsedContent ? { inlineContent: parsedContent } : placeholder ? { inlineContentPlaceholder: placeholder } : pagedRead ? { pagedRead: true } : void 0
|
|
821
888
|
);
|
|
@@ -843,6 +910,7 @@ Read this file with the readFileContent tool, using the storage path above - do
|
|
|
843
910
|
model: resolvedModel2,
|
|
844
911
|
max_output_tokens: MAX_TOKENS,
|
|
845
912
|
...skapiExtract,
|
|
913
|
+
...skapiRender,
|
|
846
914
|
input: [
|
|
847
915
|
{ role: "system", content: systemPrompt },
|
|
848
916
|
{
|
|
@@ -887,6 +955,7 @@ Read this file with the readFileContent tool, using the storage path above - do
|
|
|
887
955
|
model: resolvedModel,
|
|
888
956
|
max_tokens: MAX_TOKENS,
|
|
889
957
|
...skapiExtract,
|
|
958
|
+
...skapiRender,
|
|
890
959
|
system: [
|
|
891
960
|
{
|
|
892
961
|
type: "text",
|
|
@@ -958,6 +1027,9 @@ Read this file with the readFileContent tool, using the storage path above - do
|
|
|
958
1027
|
return "";
|
|
959
1028
|
}
|
|
960
1029
|
var BG_INDEXING_QUEUE_SUFFIX = "-bg";
|
|
1030
|
+
var INDEXING_COMPLETE_MARKER = "INDEXING_COMPLETE";
|
|
1031
|
+
var MAX_INDEXING_RESUME_PASSES = 6;
|
|
1032
|
+
var MAX_VISION_RESUME_PASSES = 40;
|
|
961
1033
|
async function getChatHistory(params, fetchOptions) {
|
|
962
1034
|
const url = params.platform === "claude" ? ANTHROPIC_MESSAGES_API_URL : OPENAI_RESPONSES_API_URL;
|
|
963
1035
|
const p = Object.assign(
|
|
@@ -1766,6 +1838,7 @@ Read this file with the readFileContent tool, using the storage path above - do
|
|
|
1766
1838
|
this.historyItemPolls.delete(itemId);
|
|
1767
1839
|
var isErr = isErrorResponseBody(response);
|
|
1768
1840
|
var answer = isErr ? getErrorMessage(response) : ((platform === "openai" ? extractOpenAIText(response) : extractClaudeText(response)) || "").trim();
|
|
1841
|
+
if (!isErr && answer) answer = answer.split(INDEXING_COMPLETE_MARKER).join("").trim();
|
|
1769
1842
|
var idx = this.state.messages.findIndex(function(m) {
|
|
1770
1843
|
return m.isPending && m._serverItemId === itemId;
|
|
1771
1844
|
});
|
|
@@ -1839,8 +1912,10 @@ Read this file with the readFileContent tool, using the storage path above - do
|
|
|
1839
1912
|
if (!self.historyItemPolls.has(entry.id) && typeof entry.poll === "function") {
|
|
1840
1913
|
self.historyItemPolls.set(entry.id, true);
|
|
1841
1914
|
var capturedId = entry.id, capturedPlat = plat;
|
|
1915
|
+
var capturedEntry = entry;
|
|
1842
1916
|
entry.poll({ latency: POLL_INTERVAL }).then(function(response) {
|
|
1843
1917
|
self.handleHistoryItemResolution(capturedId, response, capturedPlat);
|
|
1918
|
+
self.maybeResumeIndexing(capturedEntry, response, capturedPlat);
|
|
1844
1919
|
}).catch(function(err) {
|
|
1845
1920
|
self.historyItemPolls.delete(capturedId);
|
|
1846
1921
|
var isNotExists = err && (err.code === "NOT_EXISTS" || err.body && err.body.code === "NOT_EXISTS");
|
|
@@ -1863,6 +1938,65 @@ Read this file with the readFileContent tool, using the storage path above - do
|
|
|
1863
1938
|
});
|
|
1864
1939
|
this.promoteNextBgQueuedToRunning();
|
|
1865
1940
|
}
|
|
1941
|
+
// Resume-across-passes: if a background INDEXING task for a paged file (spreadsheet or
|
|
1942
|
+
// PDF) finished WITHOUT the completion marker, the agent ran out of room before reading
|
|
1943
|
+
// the whole file - dispatch a CONTINUE pass (up to a cap) that resumes from where the
|
|
1944
|
+
// already-saved records leave off. Additive + guarded so it never loops forever and
|
|
1945
|
+
// never breaks the resolution path.
|
|
1946
|
+
maybeResumeIndexing(entry, response, platform) {
|
|
1947
|
+
var self = this;
|
|
1948
|
+
try {
|
|
1949
|
+
if (!entry || !entry.storagePath) return;
|
|
1950
|
+
if (!isPagedReadFile(entry.filename, entry.mime)) return;
|
|
1951
|
+
if (isErrorResponseBody(response)) return;
|
|
1952
|
+
var answer = (platform === "openai" ? extractOpenAIText(response) : extractClaudeText(response)) || "";
|
|
1953
|
+
if (answer.indexOf(INDEXING_COMPLETE_MARKER) !== -1) return;
|
|
1954
|
+
var pass = (entry.resumePass || 0) + 1;
|
|
1955
|
+
var isVision = isImageVisionFile(entry.filename, entry.mime);
|
|
1956
|
+
var maxPasses = isVision ? MAX_VISION_RESUME_PASSES : MAX_INDEXING_RESUME_PASSES;
|
|
1957
|
+
if (pass > maxPasses) return;
|
|
1958
|
+
var id = this.host.getIdentity();
|
|
1959
|
+
if (!id || id.platform === "none" || id.serviceId !== entry.serviceId) return;
|
|
1960
|
+
var renderFrom = isVision ? pass * RENDER_PAGES_PER_WINDOW : void 0;
|
|
1961
|
+
notifyAgentContinueIndexing({
|
|
1962
|
+
platform: id.platform,
|
|
1963
|
+
model: id.model,
|
|
1964
|
+
service: id.serviceId,
|
|
1965
|
+
owner: id.owner,
|
|
1966
|
+
userId: id.userId || id.serviceId,
|
|
1967
|
+
serviceName: id.serviceName,
|
|
1968
|
+
serviceDescription: id.serviceDescription,
|
|
1969
|
+
renderFrom,
|
|
1970
|
+
attachment: {
|
|
1971
|
+
name: entry.filename,
|
|
1972
|
+
storagePath: entry.storagePath,
|
|
1973
|
+
mime: entry.mime,
|
|
1974
|
+
size: entry.size,
|
|
1975
|
+
url: ""
|
|
1976
|
+
}
|
|
1977
|
+
}).then(function(ack) {
|
|
1978
|
+
if (ack && typeof ack.id === "string") {
|
|
1979
|
+
self.bgTaskQueue.push({
|
|
1980
|
+
serviceId: id.serviceId,
|
|
1981
|
+
platform: id.platform,
|
|
1982
|
+
id: ack.id,
|
|
1983
|
+
filename: entry.filename,
|
|
1984
|
+
storagePath: entry.storagePath,
|
|
1985
|
+
isReindex: entry.isReindex,
|
|
1986
|
+
mime: entry.mime,
|
|
1987
|
+
size: entry.size,
|
|
1988
|
+
status: ack.status === "running" ? "running" : "pending",
|
|
1989
|
+
poll: ack.poll,
|
|
1990
|
+
resumePass: pass
|
|
1991
|
+
});
|
|
1992
|
+
self.drainBgTaskQueue();
|
|
1993
|
+
}
|
|
1994
|
+
}, function(e) {
|
|
1995
|
+
console.error("[chat-engine] resume-indexing dispatch failed", e);
|
|
1996
|
+
});
|
|
1997
|
+
} catch (e) {
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
1866
2000
|
// --- history fetch + pagination --------------------------------------
|
|
1867
2001
|
// Initial load (fetchMore=false) replaces the list (with in-flight rescue +
|
|
1868
2002
|
// cancelled-merge) and attaches polls to running/pending items; pagination
|
|
@@ -2228,7 +2362,7 @@ Read this file with the readFileContent tool, using the storage path above - do
|
|
|
2228
2362
|
(function() {
|
|
2229
2363
|
var MCP_PROD = "https://mcp.broadwayinc.computer";
|
|
2230
2364
|
var MCP_DEV = "https://mcp-dev.broadwayinc.computer";
|
|
2231
|
-
var BQ_VERSION = "1.
|
|
2365
|
+
var BQ_VERSION = "1.6.0" ;
|
|
2232
2366
|
var ATTACHMENT_URL_EXPIRES_SECONDS = 600;
|
|
2233
2367
|
var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
2234
2368
|
var GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
|
package/dist/engine.cjs
CHANGED
|
@@ -162,12 +162,23 @@ function isPagedReadFile(name, mime) {
|
|
|
162
162
|
const m = (mime || "").toLowerCase();
|
|
163
163
|
return m === "application/pdf" || m === "application/vnd.ms-excel" || m.includes("spreadsheetml") || m.includes("opendocument.spreadsheet");
|
|
164
164
|
}
|
|
165
|
+
function isImageVisionFile(name, mime) {
|
|
166
|
+
const ext = (name || "").split(".").pop()?.toLowerCase() || "";
|
|
167
|
+
return ext === "pdf" || (mime || "").toLowerCase() === "application/pdf";
|
|
168
|
+
}
|
|
165
169
|
var _extractPlaceholderSeq = 0;
|
|
166
170
|
function makeExtractPlaceholder(seed) {
|
|
167
171
|
_extractPlaceholderSeq += 1;
|
|
168
172
|
const slug = (seed || "file").replace(/[^a-zA-Z0-9]+/g, "_").slice(-48);
|
|
169
173
|
return `{{SKAPI_FILE_CONTENT::${slug}-${_extractPlaceholderSeq}}}`;
|
|
170
174
|
}
|
|
175
|
+
var _renderPlaceholderSeq = 0;
|
|
176
|
+
function makeRenderPlaceholder(seed) {
|
|
177
|
+
_renderPlaceholderSeq += 1;
|
|
178
|
+
const slug = (seed || "file").replace(/[^a-zA-Z0-9]+/g, "_").slice(-48);
|
|
179
|
+
return `{{SKAPI_RENDER::${slug}-${_renderPlaceholderSeq}}}`;
|
|
180
|
+
}
|
|
181
|
+
var RENDER_PAGES_PER_WINDOW = 5;
|
|
171
182
|
function composeUserMessage(text, attachmentUrls) {
|
|
172
183
|
let composed = text;
|
|
173
184
|
if (attachmentUrls.length > 0) {
|
|
@@ -265,12 +276,14 @@ function buildIndexingSystemPrompt(params) {
|
|
|
265
276
|
let systemPrompt = `You are a background indexing agent for project ${service}.
|
|
266
277
|
- Image files (.jpg, .jpeg, .png, .gif, .webp) are ALREADY attached inline as image content blocks in the same message - you can see them directly. Do NOT call web_fetch on image URLs; that will fail or return garbage. Just look at the image block and answer.
|
|
267
278
|
- Most files (office documents like .docx/.xlsx/.pptx/.hwp/.hwpx/.ods, and text/data/code files like .csv/.tsv/.json/.xml/.txt/.md and source code) have ALREADY been extracted on the server and included inline in the user message between the "BEGIN FILE CONTENT" / "END FILE CONTENT" markers - read that directly. If the inline content is a "[skapi: ...]" note, the file could not be extracted - index it from its metadata only.
|
|
268
|
-
- BIG
|
|
269
|
-
-
|
|
279
|
+
- BIG SPREADSHEETS / TEXT: the inline content may be only the FIRST part of a large file (it can end with a truncation or "more remains" note). For big spreadsheets and big text/data files READ THE FILE WITH THE readFileContent TOOL: it returns the file ONE WINDOW at a time (spreadsheets as coordinate-tagged grid rows, text as a range of characters). Pass the file's storage path. After each window: datafy it into records and SAVE them, THEN if the window says MORE REMAINS call readFileContent again with the cursor it gives you. Repeat until it says END OF FILE, so the WHOLE file is indexed - never stop after the first window. (Do NOT call readFileContent on a PDF - see the next line.)
|
|
280
|
+
- PDFs (scanned or not): you do NOT read a PDF with a tool or a URL. Its pages are RENDERED and embedded directly in the user message as IMAGE blocks, a WINDOW of pages at a time. LOOK at the embedded page images and datafy every one. The note beside them tells you whether MORE pages remain: if so, save this window's records and stop (a follow-up pass shows the next window automatically); only when the note says it was the LAST window is the PDF fully seen. Do NOT call readFileContent or web_fetch for a PDF.
|
|
281
|
+
- VISION: when the message (a readFileContent window, an embedded PDF page, or an inline attachment) includes IMAGES - scanned/rendered PDF pages, or photos embedded in a spreadsheet next to a row/block - LOOK at them and capture what they show as record data (the reading/values in a scanned table, the part/defect/condition visible in a photo). The image IS part of the data; correlate each photo with its labelled block ("PHOTO A3" markers tie a photo to that grid row).
|
|
270
282
|
- Whatever the file type, use the file's storage path (the "storage path" metadata line) as the "src::" unique_id - never the inline content or a temporary URL.
|
|
271
283
|
- TABULAR data (any spreadsheet - .csv/.tsv/.xlsx/.xls/.ods, or sheet-like rows): you MUST save EVERY data row as its own record (ONE record per row) with that row's actual column values in the record's "data", keyed by the header names, in a dedicated table (e.g. "spreadsheet_rows"). Do NOT summarize, sample only a few rows, or save just file metadata - index the whole sheet, paging through it with readFileContent when it is large. Make MULTIPLE postRecords calls in batches (e.g. 30-50 rows per call) rather than one oversized call. This per-row completeness OVERRIDES brevity. ALSO save one file-level summary record (file name, sheet name(s), column headers, total row count, overall summary) - this is the record that carries the file's "src::" unique_id - and link EVERY per-row record to it via reference (set each row record's reference to that src:: file record; the row records themselves do NOT carry a src:: unique_id). The per-row records AND this reference linkage are BOTH mandatory: the linkage is what lets the whole sheet be found and cleaned up together when the file is re-indexed.
|
|
272
284
|
- EPUB / e-books / long-form books (.epub or any book-length prose, provided inline in reading order with chapter headings preserved): you MUST save ONE record per CHAPTER (or, when chapters are unclear, per major section/topic) in a dedicated table (e.g. "book_chapters") - never collapse the whole book into a single record. Each chapter record's "data" must capture the chapter title plus its order/number AND a substantive summary of that chapter's content (key events, arguments, characters, places, concepts, terms, notable quotes). Apply AS MANY relevant tags as possible to EVERY chapter record (characters, locations, themes, topics, key concepts, key terms, dates, named entities) so the book is easy to SEARCH and cross-reference later - this is the whole point. ALSO save one book-level record (title, author, language, overall summary, chapter list / table of contents, genre/subjects) and link each chapter record to it via reference. This per-chapter completeness OVERRIDES brevity; human-readable summaries only, never raw/binary bytes.
|
|
273
|
-
- This is a background indexing task: do ALL the MCP saving FIRST, never reply mid-task, and never ask the user questions. Always use the MCP tools to save what you learn - be exhaustive about meaning (and, for tabular data, about every row). SAVE AS YOU GO: persist each window's records before reading the next, so progress is never lost. If the file is so large you cannot finish in one turn, still save everything you have read so far
|
|
285
|
+
- This is a background indexing task: do ALL the MCP saving FIRST, never reply mid-task, and never ask the user questions. Always use the MCP tools to save what you learn - be exhaustive about meaning (and, for tabular data, about every row). SAVE AS YOU GO: persist each window's records before reading the next, so progress is never lost. If the file is so large you cannot finish in one turn, still save everything you have read so far; a follow-up pass will automatically continue from where you stopped. Never store raw or binary bytes (base64, blobs); describe them in human-readable text instead.
|
|
286
|
+
- COMPLETION SIGNAL: only when you have fully read and saved the ENTIRE file (for readFileContent files: reached "END OF FILE"; for PDFs: the embedded page-image note said it was the LAST window - with all rows/pages/items saved), end your final message with the token INDEXING_COMPLETE on its own line. If you did NOT finish the whole file (more rows/pages remain), do NOT write that token - leaving it out is how the system knows to run another pass to continue.
|
|
274
287
|
- Only AFTER every save is done, send exactly ONE final message summarizing what you indexed - never just "Indexing complete", and never a raw/base64/binary value or a large pasted dump. Keep it to a few factual sentences or a short markdown bullet list covering: the file name, its content type, each table you wrote to with its record/row count and the key columns/fields or topics captured, and anything that could not be extracted. Follow this shape - Indexed <file name> (<content type>): saved <N> records to <table(s)> capturing <key columns/fields or topics>; could not extract: <gaps, or none>.`;
|
|
275
288
|
if (serviceDescription) {
|
|
276
289
|
systemPrompt += `
|
|
@@ -313,6 +326,48 @@ Read this file with the readFileContent tool, using the storage path above - do
|
|
|
313
326
|
}
|
|
314
327
|
return head + `- temporary URL (fetch this to read the file contents): ${attachment.url}`;
|
|
315
328
|
}
|
|
329
|
+
function buildIndexingRenderMessage(attachment, placeholder, renderFrom) {
|
|
330
|
+
const from = Math.max(0, renderFrom || 0);
|
|
331
|
+
const src = `src::${attachment.storagePath}`;
|
|
332
|
+
const meta = `File metadata:
|
|
333
|
+
- name: ${attachment.name}
|
|
334
|
+
- storage path: ${attachment.storagePath}
|
|
335
|
+
` + (attachment.mime ? `- mime type: ${attachment.mime}
|
|
336
|
+
` : "");
|
|
337
|
+
const datafy = `
|
|
338
|
+
${placeholder}
|
|
339
|
+
|
|
340
|
+
LOOK at each rendered page image in this message and DATAFY what it shows: for EVERY page call postRecords and save records - one record per row / table entry / line item visible on the page (or one record for the page if it is prose), capturing every value you can read (OCR the text, read tables cell by cell, describe any photos/diagrams). Use the storage path above for the "src::" unique_id.
|
|
341
|
+
|
|
342
|
+
The note next to the images tells you whether MORE pages remain after this window. If MORE remain: save this window's records and STOP - do NOT write INDEXING_COMPLETE; another pass shows the next window automatically. Only when the note says this is the LAST window (you have seen the whole file) AND everything is saved, end your message with the token INDEXING_COMPLETE.`;
|
|
343
|
+
if (from === 0) {
|
|
344
|
+
return `A new file has just been uploaded. Index it now.
|
|
345
|
+
|
|
346
|
+
` + meta + `
|
|
347
|
+
This is a PDF. Its pages are delivered to you as RENDERED PAGE IMAGES embedded directly in this message (you do NOT need any tool, URL, or web_fetch to see them). You are shown a WINDOW of pages at a time, starting at page ${from + 1}.
|
|
348
|
+
` + datafy;
|
|
349
|
+
}
|
|
350
|
+
return `CONTINUE indexing a PDF whose previous pass did not finish.
|
|
351
|
+
|
|
352
|
+
` + meta + `
|
|
353
|
+
Records for the earlier pages are ALREADY saved (they reference "${src}"). The NEXT window of rendered page images (starting at page ${from + 1}) is embedded in this message. Datafy each page as before and do NOT re-save pages that are already saved.
|
|
354
|
+
` + datafy;
|
|
355
|
+
}
|
|
356
|
+
function buildIndexingContinueMessage(attachment) {
|
|
357
|
+
const src = `src::${attachment.storagePath}`;
|
|
358
|
+
return `CONTINUE indexing a file whose previous pass did not finish.
|
|
359
|
+
|
|
360
|
+
File metadata:
|
|
361
|
+
- name: ${attachment.name}
|
|
362
|
+
- storage path: ${attachment.storagePath}
|
|
363
|
+
` + (attachment.mime ? `- mime type: ${attachment.mime}
|
|
364
|
+
` : "") + `
|
|
365
|
+
Records for the earlier windows/pages of this file are ALREADY saved (they reference "${src}"). First call getRecords with reference "${src}" to see how far the previous pass got (the furthest page/row/window already saved). Then call readFileContent with the storage path above and a CURSOR that RESUMES just after that point - do NOT start at the beginning. The cursor is derivable from what you already saved:
|
|
366
|
+
- PDF: the cursor is the NUMBER OF PAGES already read (0-based next page). If you saved up to page N, call readFileContent with cursor="N" to get page N+1 onward.
|
|
367
|
+
- Spreadsheet: the cursor is "<sheetIndex>:<nextRow>" (0-based sheet index, 1-based row). If you saved up to row R of sheet S, use cursor="S:R+1".
|
|
368
|
+
- Text: the cursor is the character offset already read.
|
|
369
|
+
Index the REMAINING windows - one record per row/item, looking at any page images or embedded photos - saving as you go until readFileContent reports END OF FILE. Do NOT re-save windows that are already saved. Use the storage path above for the "src::" unique_id. When the ENTIRE file is finally indexed, end your message with the token INDEXING_COMPLETE.`;
|
|
370
|
+
}
|
|
316
371
|
|
|
317
372
|
// src/engine/errors.ts
|
|
318
373
|
function getErrorMessage(input) {
|
|
@@ -817,14 +872,26 @@ async function callOpenAIWithPublicMcp(prompt, service, owner, messages, system,
|
|
|
817
872
|
}
|
|
818
873
|
});
|
|
819
874
|
}
|
|
875
|
+
async function notifyAgentContinueIndexing(info) {
|
|
876
|
+
return notifyAgentSaveAttachment({ ...info, continueIndexing: true });
|
|
877
|
+
}
|
|
820
878
|
async function notifyAgentSaveAttachment(info) {
|
|
821
879
|
const { platform, service, owner, attachment, parsedContent } = info;
|
|
822
|
-
const
|
|
823
|
-
const
|
|
880
|
+
const continuing = !!info.continueIndexing;
|
|
881
|
+
const visionFile = !parsedContent && isImageVisionFile(attachment.name, attachment.mime);
|
|
882
|
+
const renderFrom = Math.max(0, info.renderFrom || 0);
|
|
883
|
+
const renderPlaceholder = visionFile ? makeRenderPlaceholder(attachment.storagePath) : void 0;
|
|
884
|
+
const skapiRender = visionFile && renderPlaceholder ? {
|
|
885
|
+
_skapi_render: [
|
|
886
|
+
{ path: attachment.storagePath, from: renderFrom, count: RENDER_PAGES_PER_WINDOW, placeholder: renderPlaceholder, name: attachment.name, mime: attachment.mime }
|
|
887
|
+
]
|
|
888
|
+
} : {};
|
|
889
|
+
const pagedRead = !visionFile && (continuing || !parsedContent && isPagedReadFile(attachment.name, attachment.mime));
|
|
890
|
+
const serverExtract = !visionFile && !continuing && !parsedContent && !pagedRead && isServerExtractable(attachment.name, attachment.mime);
|
|
824
891
|
const placeholder = serverExtract ? makeExtractPlaceholder(attachment.storagePath) : void 0;
|
|
825
892
|
const extractContent = serverExtract && placeholder ? [{ path: attachment.storagePath, placeholder, name: attachment.name, mime: attachment.mime }] : void 0;
|
|
826
893
|
const skapiExtract = extractContent && extractContent.length ? { _skapi_extract: extractContent } : {};
|
|
827
|
-
const userMessage = buildIndexingUserMessage(
|
|
894
|
+
const userMessage = visionFile && renderPlaceholder ? buildIndexingRenderMessage(attachment, renderPlaceholder, renderFrom) : continuing ? buildIndexingContinueMessage(attachment) : buildIndexingUserMessage(
|
|
828
895
|
attachment,
|
|
829
896
|
parsedContent ? { inlineContent: parsedContent } : placeholder ? { inlineContentPlaceholder: placeholder } : pagedRead ? { pagedRead: true } : void 0
|
|
830
897
|
);
|
|
@@ -852,6 +919,7 @@ async function notifyAgentSaveAttachment(info) {
|
|
|
852
919
|
model: resolvedModel2,
|
|
853
920
|
max_output_tokens: MAX_TOKENS,
|
|
854
921
|
...skapiExtract,
|
|
922
|
+
...skapiRender,
|
|
855
923
|
input: [
|
|
856
924
|
{ role: "system", content: systemPrompt },
|
|
857
925
|
{
|
|
@@ -896,6 +964,7 @@ async function notifyAgentSaveAttachment(info) {
|
|
|
896
964
|
model: resolvedModel,
|
|
897
965
|
max_tokens: MAX_TOKENS,
|
|
898
966
|
...skapiExtract,
|
|
967
|
+
...skapiRender,
|
|
899
968
|
system: [
|
|
900
969
|
{
|
|
901
970
|
type: "text",
|
|
@@ -992,6 +1061,9 @@ async function listOpenAIModels(service, owner) {
|
|
|
992
1061
|
});
|
|
993
1062
|
}
|
|
994
1063
|
var BG_INDEXING_QUEUE_SUFFIX = "-bg";
|
|
1064
|
+
var INDEXING_COMPLETE_MARKER = "INDEXING_COMPLETE";
|
|
1065
|
+
var MAX_INDEXING_RESUME_PASSES = 6;
|
|
1066
|
+
var MAX_VISION_RESUME_PASSES = 40;
|
|
995
1067
|
async function getChatHistory(params, fetchOptions) {
|
|
996
1068
|
const url = params.platform === "claude" ? ANTHROPIC_MESSAGES_API_URL : OPENAI_RESPONSES_API_URL;
|
|
997
1069
|
const p = Object.assign(
|
|
@@ -1800,6 +1872,7 @@ var ChatSession = class {
|
|
|
1800
1872
|
this.historyItemPolls.delete(itemId);
|
|
1801
1873
|
var isErr = isErrorResponseBody(response);
|
|
1802
1874
|
var answer = isErr ? getErrorMessage(response) : ((platform === "openai" ? extractOpenAIText(response) : extractClaudeText(response)) || "").trim();
|
|
1875
|
+
if (!isErr && answer) answer = answer.split(INDEXING_COMPLETE_MARKER).join("").trim();
|
|
1803
1876
|
var idx = this.state.messages.findIndex(function(m) {
|
|
1804
1877
|
return m.isPending && m._serverItemId === itemId;
|
|
1805
1878
|
});
|
|
@@ -1873,8 +1946,10 @@ var ChatSession = class {
|
|
|
1873
1946
|
if (!self.historyItemPolls.has(entry.id) && typeof entry.poll === "function") {
|
|
1874
1947
|
self.historyItemPolls.set(entry.id, true);
|
|
1875
1948
|
var capturedId = entry.id, capturedPlat = plat;
|
|
1949
|
+
var capturedEntry = entry;
|
|
1876
1950
|
entry.poll({ latency: POLL_INTERVAL }).then(function(response) {
|
|
1877
1951
|
self.handleHistoryItemResolution(capturedId, response, capturedPlat);
|
|
1952
|
+
self.maybeResumeIndexing(capturedEntry, response, capturedPlat);
|
|
1878
1953
|
}).catch(function(err) {
|
|
1879
1954
|
self.historyItemPolls.delete(capturedId);
|
|
1880
1955
|
var isNotExists = err && (err.code === "NOT_EXISTS" || err.body && err.body.code === "NOT_EXISTS");
|
|
@@ -1897,6 +1972,65 @@ var ChatSession = class {
|
|
|
1897
1972
|
});
|
|
1898
1973
|
this.promoteNextBgQueuedToRunning();
|
|
1899
1974
|
}
|
|
1975
|
+
// Resume-across-passes: if a background INDEXING task for a paged file (spreadsheet or
|
|
1976
|
+
// PDF) finished WITHOUT the completion marker, the agent ran out of room before reading
|
|
1977
|
+
// the whole file - dispatch a CONTINUE pass (up to a cap) that resumes from where the
|
|
1978
|
+
// already-saved records leave off. Additive + guarded so it never loops forever and
|
|
1979
|
+
// never breaks the resolution path.
|
|
1980
|
+
maybeResumeIndexing(entry, response, platform) {
|
|
1981
|
+
var self = this;
|
|
1982
|
+
try {
|
|
1983
|
+
if (!entry || !entry.storagePath) return;
|
|
1984
|
+
if (!isPagedReadFile(entry.filename, entry.mime)) return;
|
|
1985
|
+
if (isErrorResponseBody(response)) return;
|
|
1986
|
+
var answer = (platform === "openai" ? extractOpenAIText(response) : extractClaudeText(response)) || "";
|
|
1987
|
+
if (answer.indexOf(INDEXING_COMPLETE_MARKER) !== -1) return;
|
|
1988
|
+
var pass = (entry.resumePass || 0) + 1;
|
|
1989
|
+
var isVision = isImageVisionFile(entry.filename, entry.mime);
|
|
1990
|
+
var maxPasses = isVision ? MAX_VISION_RESUME_PASSES : MAX_INDEXING_RESUME_PASSES;
|
|
1991
|
+
if (pass > maxPasses) return;
|
|
1992
|
+
var id = this.host.getIdentity();
|
|
1993
|
+
if (!id || id.platform === "none" || id.serviceId !== entry.serviceId) return;
|
|
1994
|
+
var renderFrom = isVision ? pass * RENDER_PAGES_PER_WINDOW : void 0;
|
|
1995
|
+
notifyAgentContinueIndexing({
|
|
1996
|
+
platform: id.platform,
|
|
1997
|
+
model: id.model,
|
|
1998
|
+
service: id.serviceId,
|
|
1999
|
+
owner: id.owner,
|
|
2000
|
+
userId: id.userId || id.serviceId,
|
|
2001
|
+
serviceName: id.serviceName,
|
|
2002
|
+
serviceDescription: id.serviceDescription,
|
|
2003
|
+
renderFrom,
|
|
2004
|
+
attachment: {
|
|
2005
|
+
name: entry.filename,
|
|
2006
|
+
storagePath: entry.storagePath,
|
|
2007
|
+
mime: entry.mime,
|
|
2008
|
+
size: entry.size,
|
|
2009
|
+
url: ""
|
|
2010
|
+
}
|
|
2011
|
+
}).then(function(ack) {
|
|
2012
|
+
if (ack && typeof ack.id === "string") {
|
|
2013
|
+
self.bgTaskQueue.push({
|
|
2014
|
+
serviceId: id.serviceId,
|
|
2015
|
+
platform: id.platform,
|
|
2016
|
+
id: ack.id,
|
|
2017
|
+
filename: entry.filename,
|
|
2018
|
+
storagePath: entry.storagePath,
|
|
2019
|
+
isReindex: entry.isReindex,
|
|
2020
|
+
mime: entry.mime,
|
|
2021
|
+
size: entry.size,
|
|
2022
|
+
status: ack.status === "running" ? "running" : "pending",
|
|
2023
|
+
poll: ack.poll,
|
|
2024
|
+
resumePass: pass
|
|
2025
|
+
});
|
|
2026
|
+
self.drainBgTaskQueue();
|
|
2027
|
+
}
|
|
2028
|
+
}, function(e) {
|
|
2029
|
+
console.error("[chat-engine] resume-indexing dispatch failed", e);
|
|
2030
|
+
});
|
|
2031
|
+
} catch (e) {
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
1900
2034
|
// --- history fetch + pagination --------------------------------------
|
|
1901
2035
|
// Initial load (fetchMore=false) replaces the list (with in-flight rescue +
|
|
1902
2036
|
// cancelled-merge) and attaches polls to running/pending items; pagination
|
|
@@ -2279,6 +2413,8 @@ exports.TOOL_AND_RESPONSE_BUFFER = TOOL_AND_RESPONSE_BUFFER;
|
|
|
2279
2413
|
exports.buildBoundedChatMessages = buildBoundedChatMessages;
|
|
2280
2414
|
exports.buildChatSystemPrompt = buildChatSystemPrompt;
|
|
2281
2415
|
exports.buildDisplayExpiredAttachmentHref = buildDisplayExpiredAttachmentHref;
|
|
2416
|
+
exports.buildIndexingContinueMessage = buildIndexingContinueMessage;
|
|
2417
|
+
exports.buildIndexingRenderMessage = buildIndexingRenderMessage;
|
|
2282
2418
|
exports.buildIndexingSystemPrompt = buildIndexingSystemPrompt;
|
|
2283
2419
|
exports.buildIndexingUserMessage = buildIndexingUserMessage;
|
|
2284
2420
|
exports.callClaudeWithMcp = callClaudeWithMcp;
|