bunnyquery 1.5.7 → 1.6.2
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 +522 -65
- package/dist/engine.cjs +516 -63
- package/dist/engine.cjs.map +1 -1
- package/dist/engine.d.mts +160 -5
- package/dist/engine.d.ts +160 -5
- package/dist/engine.mjs +511 -64
- package/dist/engine.mjs.map +1 -1
- package/package.json +1 -1
- package/src/engine/config.ts +19 -0
- package/src/engine/host.ts +19 -0
- package/src/engine/index.ts +2 -1
- package/src/engine/office.ts +78 -5
- package/src/engine/prompts/index.ts +5 -0
- package/src/engine/prompts/indexing_system_prompt.ts +5 -3
- package/src/engine/prompts/indexing_user_message.ts +159 -0
- package/src/engine/requests.ts +170 -25
- package/src/engine/session.ts +402 -42
package/package.json
CHANGED
package/src/engine/config.ts
CHANGED
|
@@ -33,6 +33,20 @@ export interface ChatEngineConfig {
|
|
|
33
33
|
* `registerAttachmentParser()`. See attachment_parsers.ts.
|
|
34
34
|
*/
|
|
35
35
|
attachmentParsers?: AttachmentParser[];
|
|
36
|
+
/**
|
|
37
|
+
* Opt in to SERVER-DRIVEN windowed indexing for text/grid files.
|
|
38
|
+
*
|
|
39
|
+
* Off by default, and deliberately so. When on, the client emits a
|
|
40
|
+
* `_skapi_window` directive and the WORKER reads the file one window at a time,
|
|
41
|
+
* continuing until the reader says it is exhausted. When off, the agent pages the
|
|
42
|
+
* file itself with readFileContent, exactly as before.
|
|
43
|
+
*
|
|
44
|
+
* The flag exists because the backend must ship FIRST: a client emitting the
|
|
45
|
+
* directive against a worker that does not strip it leaves an unknown field in the
|
|
46
|
+
* request body, and the provider rejects the whole call with no retry. Keep it off
|
|
47
|
+
* until the worker is deployed, then flip it per environment.
|
|
48
|
+
*/
|
|
49
|
+
windowedIndexing?: boolean;
|
|
36
50
|
}
|
|
37
51
|
|
|
38
52
|
let _config: ChatEngineConfig | null = null;
|
|
@@ -53,6 +67,11 @@ export function chatEngineConfig(): ChatEngineConfig {
|
|
|
53
67
|
return _config;
|
|
54
68
|
}
|
|
55
69
|
|
|
70
|
+
/** True when the consumer has opted in to server-driven windowed indexing. */
|
|
71
|
+
export function windowedIndexingEnabled(): boolean {
|
|
72
|
+
return _config?.windowedIndexing === true;
|
|
73
|
+
}
|
|
74
|
+
|
|
56
75
|
/** Spread helper: `{ ...pollOpt() }` adds `poll` only when configured. */
|
|
57
76
|
export function pollOpt(): { poll?: number } {
|
|
58
77
|
const p = _config?.poll;
|
package/src/engine/host.ts
CHANGED
|
@@ -18,6 +18,18 @@ export interface ChatIdentity {
|
|
|
18
18
|
serviceDescription?: string;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Project context captured at the moment the user hit Send, so a turn whose
|
|
23
|
+
* dispatch is delayed (attachment uploads are awaited first) still reaches the
|
|
24
|
+
* project the question was asked of rather than whichever project the user has
|
|
25
|
+
* navigated to by then. Both fields are identity-derived and must be snapshotted
|
|
26
|
+
* together — the system prompt embeds the service name/description/id.
|
|
27
|
+
*/
|
|
28
|
+
export interface PinnedDispatchContext {
|
|
29
|
+
identity: ChatIdentity;
|
|
30
|
+
systemPrompt: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
21
33
|
export interface ChatMessage {
|
|
22
34
|
role: 'user' | 'assistant';
|
|
23
35
|
content: string; // raw markdown — never HTML (the view parses for display)
|
|
@@ -34,6 +46,13 @@ export interface ChatMessage {
|
|
|
34
46
|
_localId?: string;
|
|
35
47
|
_cancelling?: boolean;
|
|
36
48
|
_cancelError?: string;
|
|
49
|
+
// History cache key (`serviceId#platform`) this bubble was created under.
|
|
50
|
+
// Stamped on LOCALLY-created bubbles only (the optimistic user message and
|
|
51
|
+
// its "Thinking..." placeholder); server-mapped bubbles are identified by
|
|
52
|
+
// _serverItemId instead. The dashboard renders every project through ONE
|
|
53
|
+
// ChatSession singleton, so without this a bubble is unattributable and an
|
|
54
|
+
// in-flight turn from project A gets rescued/cached into project B.
|
|
55
|
+
_ownerKey?: string;
|
|
37
56
|
}
|
|
38
57
|
|
|
39
58
|
export interface ChatState {
|
package/src/engine/index.ts
CHANGED
|
@@ -57,12 +57,13 @@ export {
|
|
|
57
57
|
// Tier-2: the stateful chat orchestration (queue/poll/cancel, typewriter,
|
|
58
58
|
// bg-task drain, resolution). DOM-free; the consumer implements ChatHost.
|
|
59
59
|
export { ChatSession } from './session';
|
|
60
|
-
export type { ChatHost, ChatIdentity, ChatState, ChatMessage } from './host';
|
|
60
|
+
export type { ChatHost, ChatIdentity, ChatState, ChatMessage, PinnedDispatchContext } from './host';
|
|
61
61
|
|
|
62
62
|
export {
|
|
63
63
|
// constants
|
|
64
64
|
POLL_INTERVAL,
|
|
65
65
|
BG_INDEXING_QUEUE_SUFFIX,
|
|
66
|
+
isBgIndexingQueue,
|
|
66
67
|
MCP_NAME,
|
|
67
68
|
DEFAULT_CLAUDE_MODEL,
|
|
68
69
|
DEFAULT_OPENAI_MODEL,
|
package/src/engine/office.ts
CHANGED
|
@@ -101,11 +101,32 @@ export function isServerExtractable(name?: string, mime?: string): boolean {
|
|
|
101
101
|
/** @deprecated renamed to {@link isServerExtractable} (now also covers text files). */
|
|
102
102
|
export const isOfficeFile = isServerExtractable;
|
|
103
103
|
|
|
104
|
-
// Extensions
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
|
|
104
|
+
// Extensions read WINDOW-BY-WINDOW via the readFileContent tool instead of inlined once.
|
|
105
|
+
//
|
|
106
|
+
// Anything NOT in this set falls back to one-shot server extraction, which is capped at
|
|
107
|
+
// MAX_EXTRACTED_CHARS (200k). Measured against real files, that cap was discarding the
|
|
108
|
+
// overwhelming majority of every large non-spreadsheet upload:
|
|
109
|
+
// 5MB .txt -> 4.0% indexed (4.8M characters silently dropped)
|
|
110
|
+
// 4.8MB .json-> 4.2%
|
|
111
|
+
// 1.9M-char Korean .txt -> 10.5%
|
|
112
|
+
// .docx -> 70.6%
|
|
113
|
+
// The truncation was invisible: the agent received a plausible-looking document and had
|
|
114
|
+
// no way to know most of it was missing. Windowing these types is what makes "index the
|
|
115
|
+
// whole file" true rather than aspirational.
|
|
116
|
+
//
|
|
117
|
+
// CSV/TSV specifically must be here rather than in the inline path: the layer now gives
|
|
118
|
+
// them ROW-bounded windows with absolute row numbers, where the character windower used
|
|
119
|
+
// to split rows across boundaries and emit no row numbers at all.
|
|
120
|
+
const PAGED_READ_EXTENSIONS = new Set([
|
|
121
|
+
// grids
|
|
122
|
+
'xls', 'xlsx', 'xlsm', 'ods',
|
|
123
|
+
// delimited text (row-windowed by the layer)
|
|
124
|
+
'csv', 'tsv', 'tab',
|
|
125
|
+
// documents
|
|
126
|
+
'pdf', 'docx', 'pptx',
|
|
127
|
+
// plain text / data / markup
|
|
128
|
+
'txt', 'md', 'markdown', 'log', 'json', 'jsonl', 'ndjson', 'xml', 'yaml', 'yml',
|
|
129
|
+
]);
|
|
109
130
|
|
|
110
131
|
/**
|
|
111
132
|
* True when a file should be indexed by PAGING through readFileContent (spreadsheets and
|
|
@@ -125,6 +146,18 @@ export function isPagedReadFile(name?: string, mime?: string): boolean {
|
|
|
125
146
|
);
|
|
126
147
|
}
|
|
127
148
|
|
|
149
|
+
/**
|
|
150
|
+
* True for files whose content is VISUAL and must be delivered to the model as IMAGE
|
|
151
|
+
* BLOCKS in the message (rendered pages), because tool-result images render on neither
|
|
152
|
+
* provider. The worker renders a page window to image URLs and injects them (`_skapi_render`
|
|
153
|
+
* directive). Currently PDFs (scanned or not); indexed page-window by page-window with
|
|
154
|
+
* resume advancing the window.
|
|
155
|
+
*/
|
|
156
|
+
export function isImageVisionFile(name?: string, mime?: string): boolean {
|
|
157
|
+
const ext = (name || '').split('.').pop()?.toLowerCase() || '';
|
|
158
|
+
return ext === 'pdf' || (mime || '').toLowerCase() === 'application/pdf';
|
|
159
|
+
}
|
|
160
|
+
|
|
128
161
|
// Monotonic counter so placeholders are unique even for same-named files in one
|
|
129
162
|
// request. Token shape must match the worker's _EXTRACT_PLACEHOLDER_RE:
|
|
130
163
|
// {{SKAPI_FILE_CONTENT::<id>}}.
|
|
@@ -135,6 +168,46 @@ export function makeExtractPlaceholder(seed: string): string {
|
|
|
135
168
|
return `{{SKAPI_FILE_CONTENT::${slug}-${_extractPlaceholderSeq}}}`;
|
|
136
169
|
}
|
|
137
170
|
|
|
171
|
+
// Placeholder marking WHERE the worker injects a window of rendered page/photo IMAGE blocks
|
|
172
|
+
// (the `_skapi_render` directive). Distinct token from the text-extraction placeholder.
|
|
173
|
+
let _renderPlaceholderSeq = 0;
|
|
174
|
+
export function makeRenderPlaceholder(seed: string): string {
|
|
175
|
+
_renderPlaceholderSeq += 1;
|
|
176
|
+
const slug = (seed || 'file').replace(/[^a-zA-Z0-9]+/g, '_').slice(-48);
|
|
177
|
+
return `{{SKAPI_RENDER::${slug}-${_renderPlaceholderSeq}}}`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Page/photo images per render window. Must match the server default so the client's
|
|
181
|
+
// resume window (from = pass * PAGES) lines up with what the worker renders.
|
|
182
|
+
export const RENDER_PAGES_PER_WINDOW = 5;
|
|
183
|
+
|
|
184
|
+
// Token the WORKER substitutes with a human description of the next window's position.
|
|
185
|
+
// Shared with the render loop so one substitution path serves both.
|
|
186
|
+
export const WINDOW_CURSOR_TOKEN = '{{RENDER_FROM}}';
|
|
187
|
+
|
|
188
|
+
let _windowPlaceholderSeq = 0;
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Placeholder the worker replaces with ONE window of a file's text/grid content.
|
|
192
|
+
* Distinct token from the render (page-image) and extract (whole-file) placeholders so
|
|
193
|
+
* a stale token from either can never be mistaken for this one.
|
|
194
|
+
*/
|
|
195
|
+
export function makeWindowPlaceholder(seed: string): string {
|
|
196
|
+
_windowPlaceholderSeq += 1;
|
|
197
|
+
const slug = (seed || 'file').replace(/[^a-zA-Z0-9]+/g, '_').slice(-48);
|
|
198
|
+
return `{{SKAPI_WINDOW::${slug}-${_windowPlaceholderSeq}}}`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* True when a file should be read server-side, one window at a time, by the worker.
|
|
203
|
+
* PDFs are excluded: they go through the VISION path, where pages are rendered to
|
|
204
|
+
* images because their text layer is often absent or unreliable.
|
|
205
|
+
*/
|
|
206
|
+
export function isWindowedReadFile(name?: string, mime?: string): boolean {
|
|
207
|
+
if (isImageVisionFile(name, mime)) return false;
|
|
208
|
+
return isPagedReadFile(name, mime);
|
|
209
|
+
}
|
|
210
|
+
|
|
138
211
|
export interface ComposedUserMessage {
|
|
139
212
|
/** Clean display/history copy (attachment links, NO extraction placeholders). */
|
|
140
213
|
composed: string;
|
|
@@ -9,6 +9,11 @@ export { buildChatSystemPrompt, type ChatSystemPromptParams } from './chat_syste
|
|
|
9
9
|
export { buildIndexingSystemPrompt, type IndexingSystemPromptParams } from './indexing_system_prompt';
|
|
10
10
|
export {
|
|
11
11
|
buildIndexingUserMessage,
|
|
12
|
+
buildIndexingContinueMessage,
|
|
13
|
+
buildIndexingRenderMessage,
|
|
14
|
+
buildIndexingRenderContinueTemplate,
|
|
15
|
+
buildIndexingWindowMessage,
|
|
16
|
+
RENDER_FROM_TOKEN,
|
|
12
17
|
type IndexingAttachmentInfo,
|
|
13
18
|
type BuildIndexingUserMessageOptions,
|
|
14
19
|
} from './indexing_user_message';
|
|
@@ -23,12 +23,14 @@ export function buildIndexingSystemPrompt(params: IndexingSystemPromptParams): s
|
|
|
23
23
|
`You are a background indexing agent for project ${service}.
|
|
24
24
|
- 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.
|
|
25
25
|
- 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.
|
|
26
|
-
- BIG
|
|
27
|
-
-
|
|
26
|
+
- 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.)
|
|
27
|
+
- 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.
|
|
28
|
+
- 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).
|
|
28
29
|
- 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.
|
|
29
30
|
- 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.
|
|
30
31
|
- 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.
|
|
31
|
-
- 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
|
|
32
|
+
- 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.
|
|
33
|
+
- 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.
|
|
32
34
|
- 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>.`;
|
|
33
35
|
|
|
34
36
|
if (serviceDescription) {
|
|
@@ -100,3 +100,162 @@ export function buildIndexingUserMessage(
|
|
|
100
100
|
|
|
101
101
|
return head + `- temporary URL (fetch this to read the file contents): ${attachment.url}`;
|
|
102
102
|
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Token the WORKER substitutes with the 1-based first page of the window it is about to
|
|
106
|
+
* render, when it builds the next pass of a document from `RENDER_CONTINUE_TEMPLATE`.
|
|
107
|
+
* Must match the worker's RENDER_FROM_TOKEN.
|
|
108
|
+
*/
|
|
109
|
+
export const RENDER_FROM_TOKEN = '{{RENDER_FROM}}';
|
|
110
|
+
const WINDOW_CURSOR_TOKEN = RENDER_FROM_TOKEN;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* User message for a VISION file (PDF): its pages are delivered as RENDERED PAGE IMAGES that
|
|
114
|
+
* the proxy worker injects into THIS message at the `placeholder` token (tool-result images
|
|
115
|
+
* render on neither provider, so the pages must be image blocks in the message itself). Each
|
|
116
|
+
* pass shows one WINDOW of pages starting at `renderFrom` (0-based).
|
|
117
|
+
*
|
|
118
|
+
* The WORKER advances the window: when its renderer reports pages remaining it enqueues the
|
|
119
|
+
* next pass itself, off the true page count, so a document indexes end-to-end with no browser
|
|
120
|
+
* involved. This message therefore only ever describes ONE window, and the model is never
|
|
121
|
+
* asked to decide whether the document is finished.
|
|
122
|
+
*
|
|
123
|
+
* renderFrom === 0 is the FIRST pass (leads with "A new file has just been uploaded." so the
|
|
124
|
+
* client builds the "Indexing: <name>" bubble); a continue pass (built by the worker from
|
|
125
|
+
* buildIndexingRenderContinueTemplate) leads with "CONTINUE indexing" so it is not a duplicate
|
|
126
|
+
* primary bubble.
|
|
127
|
+
*/
|
|
128
|
+
export function buildIndexingRenderMessage(
|
|
129
|
+
attachment: IndexingAttachmentInfo,
|
|
130
|
+
placeholder: string,
|
|
131
|
+
renderFrom: number,
|
|
132
|
+
): string {
|
|
133
|
+
const from = Math.max(0, renderFrom || 0);
|
|
134
|
+
if (from > 0) return buildIndexingRenderContinueTemplate(attachment, placeholder, String(from + 1));
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
`A new file has just been uploaded. Index it now.\n\n` +
|
|
138
|
+
buildRenderMeta(attachment) +
|
|
139
|
+
`\nThis is a PDF. Its pages are delivered to you as RENDERED PAGE IMAGES embedded directly in this ` +
|
|
140
|
+
`message (you do NOT need any tool, URL, or web_fetch to see them). You are shown a WINDOW of pages ` +
|
|
141
|
+
`at a time, starting at page ${from + 1}.\n` +
|
|
142
|
+
buildRenderDatafy(placeholder)
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* The CONTINUE pass, as a template the worker fills in. `pageLabel` defaults to the
|
|
148
|
+
* RENDER_FROM_TOKEN placeholder, which the worker replaces with the real 1-based start page
|
|
149
|
+
* of the window it is rendering; passing an explicit label produces a ready-to-send message.
|
|
150
|
+
*/
|
|
151
|
+
export function buildIndexingRenderContinueTemplate(
|
|
152
|
+
attachment: IndexingAttachmentInfo,
|
|
153
|
+
placeholder: string,
|
|
154
|
+
pageLabel: string = RENDER_FROM_TOKEN,
|
|
155
|
+
): string {
|
|
156
|
+
const src = `src::${attachment.storagePath}`;
|
|
157
|
+
return (
|
|
158
|
+
`CONTINUE indexing a PDF whose previous pass did not finish.\n\n` +
|
|
159
|
+
buildRenderMeta(attachment) +
|
|
160
|
+
`\nRecords for the earlier pages are ALREADY saved (they reference "${src}"). The NEXT window of ` +
|
|
161
|
+
`rendered page images (starting at page ${pageLabel}) is embedded in this message. Datafy each page as ` +
|
|
162
|
+
`before and do NOT re-save pages that are already saved.\n` +
|
|
163
|
+
buildRenderDatafy(placeholder)
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function buildRenderMeta(attachment: IndexingAttachmentInfo): string {
|
|
168
|
+
return (
|
|
169
|
+
`File metadata:\n` +
|
|
170
|
+
`- name: ${attachment.name}\n` +
|
|
171
|
+
`- storage path: ${attachment.storagePath}\n` +
|
|
172
|
+
(attachment.mime ? `- mime type: ${attachment.mime}\n` : '')
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Shared datafy guidance. The placeholder is where the worker splices the note + rendered
|
|
177
|
+
// page images; instructions reference "the page images in this message" so they read
|
|
178
|
+
// correctly whether the images land before or after this text.
|
|
179
|
+
//
|
|
180
|
+
// Deliberately says nothing about INDEXING_COMPLETE or about whether the document is
|
|
181
|
+
// finished: the worker decides that from the renderer's page count. Asking the model was
|
|
182
|
+
// what used to end an 88-page file at page 15.
|
|
183
|
+
function buildRenderDatafy(placeholder: string): string {
|
|
184
|
+
return (
|
|
185
|
+
`\n${placeholder}\n\n` +
|
|
186
|
+
`LOOK at each rendered page image in this message and DATAFY what it shows: for EVERY page ` +
|
|
187
|
+
`call postRecords and save records - one record per row / table entry / line item visible on the page ` +
|
|
188
|
+
`(or one record for the page if it is prose), capturing every value you can read (OCR the text, read tables ` +
|
|
189
|
+
`cell by cell, describe any photos/diagrams). Use the storage path above for the "src::" unique_id.\n\n` +
|
|
190
|
+
`Save records for THIS window of pages only, then stop and report what you saved. Do NOT try to read ` +
|
|
191
|
+
`the rest of the file and do NOT worry about the pages after this window: if any remain, the next window ` +
|
|
192
|
+
`is rendered and sent to you automatically. Report only the pages you were actually shown - never imply ` +
|
|
193
|
+
`you have seen the whole document.`
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* User message for a WINDOWED file: the worker splices ONE window of the file's rows or
|
|
199
|
+
* text into this message at `placeholder`, then continues from the reader's own cursor
|
|
200
|
+
* until the file is exhausted.
|
|
201
|
+
*
|
|
202
|
+
* The agent is deliberately NOT asked to page the file itself, and is NOT asked to judge
|
|
203
|
+
* whether it is finished. Both used to be its job, and both failed the same way: the
|
|
204
|
+
* traversal lived inside a single turn's budget, so a large file simply stopped partway
|
|
205
|
+
* with a confident summary of the part it had seen.
|
|
206
|
+
*/
|
|
207
|
+
export function buildIndexingWindowMessage(
|
|
208
|
+
attachment: IndexingAttachmentInfo,
|
|
209
|
+
placeholder: string,
|
|
210
|
+
isContinuation: boolean,
|
|
211
|
+
positionLabel?: string,
|
|
212
|
+
): string {
|
|
213
|
+
const src = `src::${attachment.storagePath}`;
|
|
214
|
+
const head = isContinuation
|
|
215
|
+
? `CONTINUE indexing a file whose previous pass did not finish.\n\n`
|
|
216
|
+
: `A new file has just been uploaded. Index it now.\n\n`;
|
|
217
|
+
const where = isContinuation
|
|
218
|
+
? `\nRecords for the earlier windows are ALREADY saved (they reference "${src}"). The NEXT window ` +
|
|
219
|
+
`(starting at ${positionLabel || WINDOW_CURSOR_TOKEN}) is embedded below. Do NOT re-save windows that are already saved.\n`
|
|
220
|
+
: `\nThis file is delivered to you ONE WINDOW at a time, embedded directly in this message. ` +
|
|
221
|
+
`You do NOT need any tool, URL, or web_fetch to read it.\n`;
|
|
222
|
+
|
|
223
|
+
return (
|
|
224
|
+
head +
|
|
225
|
+
buildRenderMeta(attachment) +
|
|
226
|
+
where +
|
|
227
|
+
`\n${placeholder}\n\n` +
|
|
228
|
+
`DATAFY this window: call postRecords and save records for everything in it - ONE RECORD PER ROW ` +
|
|
229
|
+
`for tabular data (keyed by the column headers), or one record per section for prose. Capture every ` +
|
|
230
|
+
`value you can read. Use the storage path above for the "src::" unique_id on the file-level record, ` +
|
|
231
|
+
`and link every row/section record to it by reference.\n\n` +
|
|
232
|
+
`Save records for THIS window only, then stop and report what you saved. Do NOT try to read the rest ` +
|
|
233
|
+
`of the file, and do NOT call readFileContent - if more remains, the next window is read and sent to ` +
|
|
234
|
+
`you automatically. Report only what you were actually shown, and never imply you have seen the whole ` +
|
|
235
|
+
`file when the note beside the window says more remains.`
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* User message for a RESUME pass: a previous indexing pass could not finish this large
|
|
241
|
+
* file, so continue it from where the already-saved records leave off (never restart).
|
|
242
|
+
*/
|
|
243
|
+
export function buildIndexingContinueMessage(attachment: IndexingAttachmentInfo): string {
|
|
244
|
+
const src = `src::${attachment.storagePath}`;
|
|
245
|
+
return (
|
|
246
|
+
`CONTINUE indexing a file whose previous pass did not finish.\n\n` +
|
|
247
|
+
`File metadata:\n` +
|
|
248
|
+
`- name: ${attachment.name}\n` +
|
|
249
|
+
`- storage path: ${attachment.storagePath}\n` +
|
|
250
|
+
(attachment.mime ? `- mime type: ${attachment.mime}\n` : '') +
|
|
251
|
+
`\nRecords for the earlier windows/pages of this file are ALREADY saved (they reference "${src}"). ` +
|
|
252
|
+
`First call getRecords with reference "${src}" to see how far the previous pass got (the furthest page/row/window already saved). ` +
|
|
253
|
+
`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:\n` +
|
|
254
|
+
` - 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.\n` +
|
|
255
|
+
` - 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".\n` +
|
|
256
|
+
` - Text: the cursor is the character offset already read.\n` +
|
|
257
|
+
`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. ` +
|
|
258
|
+
`Do NOT re-save windows that are already saved. ` +
|
|
259
|
+
`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.`
|
|
260
|
+
);
|
|
261
|
+
}
|