bunnyquery 1.9.7 → 1.10.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/README.md +60 -22
- package/bunnyquery.css +53 -2
- package/bunnyquery.js +1880 -197
- package/dist/engine.cjs +1806 -133
- package/dist/engine.cjs.map +1 -1
- package/dist/engine.d.mts +1335 -11
- package/dist/engine.d.ts +1335 -11
- package/dist/engine.mjs +1765 -134
- package/dist/engine.mjs.map +1 -1
- package/package.json +1 -1
- package/src/engine/budget.ts +46 -2
- package/src/engine/config.ts +243 -0
- package/src/engine/errors.ts +87 -1
- package/src/engine/history.ts +113 -2
- package/src/engine/host.ts +85 -0
- package/src/engine/index.ts +111 -2
- package/src/engine/office.ts +16 -4
- package/src/engine/project_settings.ts +303 -0
- package/src/engine/prompts/chat_system_prompt.ts +21 -6
- package/src/engine/prompts/indexing_system_prompt.ts +8 -4
- package/src/engine/prompts/indexing_user_message.ts +39 -3
- package/src/engine/requests.ts +203 -8
- package/src/engine/session.ts +1553 -29
- package/src/engine/sse.ts +1054 -0
- package/src/widget.css +21 -2
- package/styles/chat.css +32 -0
package/README.md
CHANGED
|
@@ -27,7 +27,8 @@ you can build your own chat UI on top of it. See
|
|
|
27
27
|
prompt when an upload hits a file that already exists (skip / reindex only /
|
|
28
28
|
overwrite, with "apply to all remaining"). Images are read with vision/OCR,
|
|
29
29
|
large documents and spreadsheets are read window by window, PDFs are rendered
|
|
30
|
-
to page images,
|
|
30
|
+
to page images, emails are read as their headers, body and attachment text,
|
|
31
|
+
and everything else extractable is inlined as text. See
|
|
31
32
|
[Supported file types](#supported-file-types).
|
|
32
33
|
- **Background indexing**: an uploaded file is indexed in the background,
|
|
33
34
|
across as many passes as it takes. A file's passes collapse into a single
|
|
@@ -124,6 +125,9 @@ Mounts the widget. Returns the `BunnyQuery` object.
|
|
|
124
125
|
| `hostDomain` | `string` | `null` | db-CDN host for temporary file URLs. Defaults to `skapi.app` (dev) / `skapi.com` (prod). |
|
|
125
126
|
| `attachmentParsers` | `array` | `null` | Client-side attachment parsers. See [Attachment parser plugins](#attachment-parser-plugins). |
|
|
126
127
|
| `windowedIndexing` | `boolean` | `true` | Server-driven windowed indexing for text and grid files (see [file types](#supported-file-types)). Pass `false` to fall back to agent-driven paging, which keeps the traversal inside the model's turn budget and the tab open. |
|
|
128
|
+
| `allowAnonymous` | `boolean` | `null` | Open the chat with no login for visitors without an account. `null` follows the project's own "Allow anonymous users" setting (`getConnectionInfo().conf.require_login`); `true`/`false` pins it. |
|
|
129
|
+
| `liveStreaming` | `boolean` | `false` | Paint a chat answer into its bubble as it arrives, instead of at the end. A **request**, not a switch: the widget honours it only when your page's `skapi-js` actually carries skapi's half of the stream flag (it checks for `clientSecretRequestStream` and `clientSecretRequestFinalize`), and otherwise warns once and falls back to buffered replies. An older SDK silently drops the flag, which would leave the destination streaming SSE into a buffered row that reads back empty. It still also needs a polling worker that relays the response bytes, which the widget cannot check, so leave it off until the region you talk to is deployed. |
|
|
130
|
+
| `liveStreamingRealtime` | `boolean` | `false` | Deliver streamed chunks over skapi's websocket instead of waiting for the next poll tick. Requires `liveStreaming`. Off unless you ask for it: skapi's `joinRealtime` **replaces** the connection's group, so for the length of a turn it takes the room out from under whatever else your app uses realtime for. Purely an accelerator; with it off the reply still streams, on the poll's cadence. |
|
|
127
131
|
|
|
128
132
|
### Methods
|
|
129
133
|
|
|
@@ -158,9 +162,11 @@ configure.
|
|
|
158
162
|
An attachment is used in two places, and they take different routes:
|
|
159
163
|
|
|
160
164
|
- **In the chat message.** Extractable files are inlined as text; anything else
|
|
161
|
-
(PDFs, images) is handed over as a temporary link
|
|
162
|
-
|
|
163
|
-
|
|
165
|
+
(PDFs, images) is handed over as a temporary link. Server-side re-minting of
|
|
166
|
+
chat links is deliberately off (an S3 presign is signed for GET only and 403s
|
|
167
|
+
the HEAD probe OpenAI sends before downloading), so the CDN link is left in
|
|
168
|
+
place; the turn is instead dispatched only once the indexing queue has drained,
|
|
169
|
+
which is what keeps the link fresh.
|
|
164
170
|
- **In background indexing**, where the file is read in full and saved into the
|
|
165
171
|
project's knowledge. This is the path with the window and page loops below.
|
|
166
172
|
|
|
@@ -182,10 +188,12 @@ may have expired).
|
|
|
182
188
|
`.pdf`
|
|
183
189
|
|
|
184
190
|
PDF text layers are often absent or unreliable, so a PDF is indexed **visually**:
|
|
185
|
-
the proxy worker renders a window of pages
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
191
|
+
the proxy worker renders a window of pages to images and injects them as image
|
|
192
|
+
blocks in the indexing message. The window is five pages on Claude and on the
|
|
193
|
+
OpenAI models that accept full-resolution images, and two on OpenAI's
|
|
194
|
+
downsampled and nano tiers. Tool-result images render on neither provider, which
|
|
195
|
+
is why the pages have to be in the message itself. That makes scanned PDFs work
|
|
196
|
+
as well as digital ones.
|
|
189
197
|
|
|
190
198
|
The worker advances the window itself, off its renderer's true page count, and
|
|
191
199
|
enqueues the next pass. Indexing a long document therefore does not depend on
|
|
@@ -195,9 +203,15 @@ declaring itself finished.
|
|
|
195
203
|
### 3. Large documents, spreadsheets & data: read window by window
|
|
196
204
|
|
|
197
205
|
```
|
|
198
|
-
.xls .xlsx .xlsm
|
|
206
|
+
.xls .xlsx .xlsm grids: sheet-by-sheet row windows, plus embedded photos
|
|
207
|
+
.ods OpenDocument sheets: character windows, plus photos
|
|
199
208
|
.csv .tsv .tab row-bounded windows with absolute row numbers
|
|
200
|
-
.docx .
|
|
209
|
+
.doc .docx .docm word processor documents
|
|
210
|
+
.ppt .pptx .pptm slide decks
|
|
211
|
+
.hwp .hwpx Hancom word processor
|
|
212
|
+
.odt .odp OpenDocument text and slides
|
|
213
|
+
.epub .rtf .html .htm other long-form documents
|
|
214
|
+
.eml email: headers, body, attachment text
|
|
201
215
|
.txt .md .markdown .log plain text
|
|
202
216
|
.json .jsonl .ndjson .xml .yaml .yml
|
|
203
217
|
```
|
|
@@ -224,13 +238,19 @@ The skapi proxy downloads the file, extracts its text **server-side**, and
|
|
|
224
238
|
inlines that text into the request, so the model reads it directly with no
|
|
225
239
|
fetching. This keeps indexing consistent across model providers.
|
|
226
240
|
|
|
227
|
-
**Office
|
|
228
|
-
and the macro-enabled `.docm`/`.xlsm`/`.pptm`):
|
|
241
|
+
**Office, e-book & email** (binary/zip/MIME, parsed; includes legacy binary
|
|
242
|
+
`.doc`/`.xls`/`.ppt` and the macro-enabled `.docm`/`.xlsm`/`.pptm`):
|
|
229
243
|
`.doc` · `.docx` · `.docm` · `.xls` · `.xlsx` · `.xlsm` · `.ppt` · `.pptx` · `.pptm`
|
|
230
|
-
· `.hwp` · `.hwpx` · `.ods` · `.odt` · `.odp` · `.epub`
|
|
244
|
+
· `.hwp` · `.hwpx` · `.ods` · `.odt` · `.odp` · `.epub` · `.eml`
|
|
245
|
+
|
|
246
|
+
An `.eml` email yields its header block, its body and the text of every attached
|
|
247
|
+
document (spreadsheet, document, csv, calendar, the text layer of a PDF) inline;
|
|
248
|
+
pictures attached to or embedded in it are extracted into `__MEDIA__` like the
|
|
249
|
+
pictures in any other document, and every other attachment is listed by name
|
|
250
|
+
only, never saved as a separate file.
|
|
231
251
|
|
|
232
252
|
**Text, data, markup & source code** (decoded as text; `.html`/`.htm` have their
|
|
233
|
-
tags stripped):
|
|
253
|
+
tags stripped and `.rtf` is parsed, control words and non-text groups discarded):
|
|
234
254
|
|
|
235
255
|
```
|
|
236
256
|
.csv .tsv .tab .txt .text .log .md .markdown .rst .json .ndjson .jsonl .geojson
|
|
@@ -243,10 +263,18 @@ Plus a **MIME fallback**: any file whose content type is text-like (`text/*`,
|
|
|
243
263
|
`application/json`, `application/xml`, `*+json`, `*+xml`, `*+yaml`, …) is decoded
|
|
244
264
|
even when its extension isn't in the list above.
|
|
245
265
|
|
|
246
|
-
Encoding is auto-detected: UTF-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
266
|
+
Encoding is auto-detected: a UTF-32 or UTF-16 BOM is taken as definitive,
|
|
267
|
+
otherwise UTF-8 (BOM-aware), CP949/EUC-KR (Korean) and Latin-1 are all decoded
|
|
268
|
+
and scored, and the one producing the least mojibake wins. It is a scoring pass,
|
|
269
|
+
not a first-that-succeeds ladder, so one stray byte in a clean Korean file no
|
|
270
|
+
longer dumps the whole file into Latin-1. Extracted text is capped at **200,000
|
|
271
|
+
characters**; longer files are truncated with a `...[truncated for length;
|
|
272
|
+
showing the first 200000 of N characters. To read and index the WHOLE file, call
|
|
273
|
+
the readFileContent tool with this file's storage path; it returns the file
|
|
274
|
+
window by window (with images for scanned/photo content).]` marker. (The separate
|
|
275
|
+
client-side parser-plugin cap uses the shorter `...[truncated for length;
|
|
276
|
+
original N characters]` marker.) The formats listed in section 3 are windowed
|
|
277
|
+
precisely so they never hit that cap.
|
|
250
278
|
|
|
251
279
|
Note the overlap between sections 3 and 4 is deliberate: a `.docx` or a `.csv`
|
|
252
280
|
is windowed when it is indexed, and extracted whole when it rides along in a
|
|
@@ -288,7 +316,7 @@ display.
|
|
|
288
316
|
|
|
289
317
|
By default the chat agent reads images with vision/OCR, renders PDF pages to
|
|
290
318
|
images, reads large documents and spreadsheets window by window, and extracts
|
|
291
|
-
Office/OpenDocument/EPUB and text/data/code files on the server. See
|
|
319
|
+
Office/OpenDocument/EPUB/email and text/data/code files on the server. See
|
|
292
320
|
[Supported file types](#supported-file-types). For any format read by **none**
|
|
293
321
|
of these (e.g. a proprietary binary format), register a **parser plugin**: it
|
|
294
322
|
runs in the browser, turns the uploaded file into text (or an HTML string), and
|
|
@@ -428,6 +456,15 @@ helpers. See the `.d.ts` shipped with `bunnyquery/engine`.
|
|
|
428
456
|
| `poll` | `number?` | Value attached as `poll` on every request. Omit it if your `clientSecretRequest` already resolves with the final body; pass `0` for the deployed `skapi-js@latest` (needed for the early ack + a manual `.poll()` handle that powers queued-send cancel, the widget's case). |
|
|
429
457
|
| `attachmentParsers` | `array?` | Client-side attachment parsers, registered at configure time. More can be added later with `registerAttachmentParser()`. See [Attachment parser plugins](#attachment-parser-plugins). |
|
|
430
458
|
| `windowedIndexing` | `boolean?` | Opt in to **server-driven** windowed indexing for text and grid files (see [file types](#supported-file-types)). Off by default in the engine; the widget passes it as `true`. The deployed skapi workers support it; only leave it off against a self-hosted worker that does not yet strip the `_skapi_window` directive, where it would reach the provider as an unknown body field and fail the call terminally with no retry. |
|
|
459
|
+
| `liveStreaming` | `boolean?` | Opt in to **live streaming** of chat turns. Off by default, and the backend ships first: a streamed row settles with a status and NO body (the answer was the stream), so against a worker that does not relay, the turn reads back empty. Pair it with `clientSecretRequestFinalize` and `clientSecretRequestStream`, and gate it on `skapiSupportsStreaming(skapi)`. |
|
|
460
|
+
| `clientSecretRequestFinalize` | `function?` | `skapi.clientSecretRequestFinalize`, bound to your Skapi instance. Stores the version of a streamed turn that history keeps (the engine sends the assembled provider body, so it reads back exactly like a buffered turn) and releases that request's chunks. Without it a streamed turn is never finalized and its row stays empty. |
|
|
461
|
+
| `clientSecretRequestStream` | `function?` | `skapi.clientSecretRequestStream`, bound to your Skapi instance. The **second half of the durability guarantee**: a row that settles while no poll is attached (closed tab, discarded background tab, slept device) is never finalized, so its answer stays in the chunk store and its history row is terminal and empty. Given the request id this drains that turn's chunks in one pass; the engine parses them exactly as it parses a live stream and finalizes what it read, so each row is recovered at most once. Without it the engine mints no recovery marker at all and behaves as it did before streaming. |
|
|
462
|
+
| `onLiveStreamUpdate` | `function?` | Observation hook for a streaming turn (`{ serverItemId, ownerKey, phase, text, thinkingText, toolNames, complete, errored }`). The engine already paints the answer text itself, so this is only for affordances it does not decide the presentation of. Never throw from it. |
|
|
463
|
+
| `liveStreamingRealtime` | `boolean?` | Push relayed chunks over skapi's websocket as well. Requires `liveStreaming`. Off by default: `joinRealtime` replaces the connection's group for the length of a turn, so only a host that owns its skapi instance should opt in. |
|
|
464
|
+
| `streamRecovery` | `boolean?` | Set `false` to force the read-back of already-streamed turns **off**, even though the chunk reader is injected. There is no need to set it to turn recovery on: injecting `clientSecretRequestStream` is what arms it. |
|
|
465
|
+
| `mintIndexDoneMarker` | `function?` | Write the durable "indexing finished" marker (`done::<path>`, reference `src::<path>`, table `__INDEXING__`) for the runs this client knows are complete. Best-effort, must never throw. Without it the engine falls back to inference. |
|
|
466
|
+
| `upsertIndexRunRecord` | `function?` | Create-or-update the per-file run record (`run::<path>`, reference `src::<path>`, table `__INDEXING__`), which is what lets chat rows and files-page badges paint without scanning background history. You implement the upsert (the records API has none) and the status precedence: `'working'` must never overwrite a terminal status. Without it the engine uses the legacy scan/probe path. |
|
|
467
|
+
| `csrHistoryItemLookup` | `function?` | Single-item `csr-poll` point lookup, used by `ChatSession.hydrateCompactItems` to fetch a compact history stub's real body when an indexing row is expanded. Without it stubs keep their server-extracted heads. |
|
|
431
468
|
|
|
432
469
|
### Display and paging helpers
|
|
433
470
|
|
|
@@ -480,9 +517,10 @@ boot-time fallback for when the silent path cannot refresh.
|
|
|
480
517
|
`height: 100dvh`) or it will collapse.
|
|
481
518
|
- File and folder uploads are stored in your Skapi project's database storage and
|
|
482
519
|
served from a temporary db-CDN URL (`hostDomain`); links in chat refresh on expiry.
|
|
483
|
-
Links a
|
|
484
|
-
upstream call, so a
|
|
485
|
-
URL.
|
|
520
|
+
Links a background **indexing** pass carries are re-minted server-side
|
|
521
|
+
immediately before the upstream call (`_skapi_file_urls`), so a pass that waits
|
|
522
|
+
days behind a bulk upload never hands the model a dead URL. Chat-message links
|
|
523
|
+
are not re-minted; a chat turn waits for the indexing queue to drain instead.
|
|
486
524
|
- The number of files attachable to a single message is capped, and beyond a
|
|
487
525
|
point the chips collapse into a "...(n) more" pill rather than being rendered.
|
|
488
526
|
Very large batches belong on a dedicated upload page, not the chat composer.
|
package/bunnyquery.css
CHANGED
|
@@ -664,23 +664,42 @@
|
|
|
664
664
|
background: var(--bq-paper);
|
|
665
665
|
}
|
|
666
666
|
|
|
667
|
+
/* ZERO HEIGHT IN FLOW, DELIBERATELY. Mirrors agent.vue, where the reasoning is
|
|
668
|
+
written out in full: this bar mounts when a history fetch starts and unmounts
|
|
669
|
+
when the page lands, both while the reader sits at the top of the list, and as
|
|
670
|
+
an in-flow sticky element it moved every row by its own height each time
|
|
671
|
+
(measured in Chrome with overflow-anchor:none). The outer box now contributes
|
|
672
|
+
nothing to layout and the inner strip overflows it, so the bar still sits
|
|
673
|
+
pinned at the top of the viewport while mounting it moves nothing. */
|
|
667
674
|
.bq-history-loading {
|
|
668
675
|
position: sticky;
|
|
669
676
|
top: 0;
|
|
670
677
|
z-index: 1;
|
|
678
|
+
height: 0;
|
|
679
|
+
padding: 0;
|
|
680
|
+
overflow: visible;
|
|
681
|
+
pointer-events: none;
|
|
682
|
+
}
|
|
683
|
+
.bq-history-loading-inner {
|
|
671
684
|
display: flex;
|
|
685
|
+
align-items: center;
|
|
672
686
|
justify-content: center;
|
|
673
687
|
gap: 0.1em;
|
|
674
688
|
padding: 0.5rem 0 1rem;
|
|
675
689
|
font-size: 0.8rem;
|
|
676
690
|
color: var(--bq-muted);
|
|
677
|
-
background: var(--bq-paper);
|
|
691
|
+
background: linear-gradient(to bottom, var(--bq-paper) 70%, transparent);
|
|
678
692
|
}
|
|
679
|
-
/* initial first-page load: center it in the (empty) messages area
|
|
693
|
+
/* initial first-page load: center it in the (empty) messages area. A full-box
|
|
694
|
+
overlay rather than a top strip, so it takes its height back - and it replaces
|
|
695
|
+
an EMPTY messages area, so it has nothing to move. */
|
|
680
696
|
.bq-history-loading.is-initial {
|
|
681
697
|
position: absolute;
|
|
682
698
|
inset: 0;
|
|
699
|
+
height: auto;
|
|
700
|
+
display: flex;
|
|
683
701
|
align-items: center;
|
|
702
|
+
justify-content: center;
|
|
684
703
|
padding: 0;
|
|
685
704
|
}
|
|
686
705
|
|
|
@@ -1236,6 +1255,38 @@
|
|
|
1236
1255
|
.bq-cancel-queue-btn:hover:not(.is-disabled) { background: var(--bq-warning-bg); color: var(--bq-warning); }
|
|
1237
1256
|
.bq-cancel-queue-btn.is-disabled { opacity: 0.3; cursor: not-allowed; pointer-events: none; }
|
|
1238
1257
|
|
|
1258
|
+
/* ---- unfinalized streamed answer (ask for it) ----------------------------
|
|
1259
|
+
A turn whose answer was streamed and never finalized keeps its bytes in the
|
|
1260
|
+
chunk store, and this bubble is what the reader gets when nothing is currently
|
|
1261
|
+
fetching them: the per-load recovery cap left this one behind, or a read failed.
|
|
1262
|
+
It is deliberately NOT the danger red of an error bubble - nothing is broken and
|
|
1263
|
+
nothing is lost, the answer is simply not here yet - and deliberately not a
|
|
1264
|
+
spinner, which would promise an arrival nobody has scheduled. Shared by both
|
|
1265
|
+
chatboxes (this file is bunnyquery/styles/chat.css, which agent.vue imports). */
|
|
1266
|
+
.bq-stream-recover-note {
|
|
1267
|
+
display: block;
|
|
1268
|
+
font-size: 0.72rem;
|
|
1269
|
+
color: var(--bq-muted);
|
|
1270
|
+
font-style: italic;
|
|
1271
|
+
}
|
|
1272
|
+
.bq-bubble.is-stream-failed .bq-stream-recover-note { color: var(--bq-warning); }
|
|
1273
|
+
.bq-stream-recover-btn {
|
|
1274
|
+
display: inline-block;
|
|
1275
|
+
margin-top: 0.4rem;
|
|
1276
|
+
padding: 0.18rem 0.55rem;
|
|
1277
|
+
min-height: 0;
|
|
1278
|
+
border: 1px solid var(--bq-line);
|
|
1279
|
+
background: transparent;
|
|
1280
|
+
color: inherit;
|
|
1281
|
+
font-size: 0.72rem;
|
|
1282
|
+
line-height: 1.3;
|
|
1283
|
+
cursor: pointer;
|
|
1284
|
+
box-shadow: none;
|
|
1285
|
+
border-radius: 0;
|
|
1286
|
+
}
|
|
1287
|
+
.bq-stream-recover-btn:hover { background: rgba(127, 127, 127, 0.12); }
|
|
1288
|
+
.bq-bubble.is-stream-failed .bq-stream-recover-btn { border-color: var(--bq-warning-border); color: var(--bq-warning); }
|
|
1289
|
+
|
|
1239
1290
|
/* ---- collapsed background-indexing group ---------------------------------*/
|
|
1240
1291
|
/* One file's many indexing passes (first pass + every CONTINUE pass, each with
|
|
1241
1292
|
a request AND a response bubble) render as a single status row instead of
|