bunnyquery 1.6.0 → 1.7.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 +157 -29
- package/bunnyquery.css +122 -0
- package/bunnyquery.js +1400 -195
- package/dist/engine.cjs +1119 -117
- package/dist/engine.cjs.map +1 -1
- package/dist/engine.d.mts +517 -12
- package/dist/engine.d.ts +517 -12
- package/dist/engine.mjs +1103 -118
- package/dist/engine.mjs.map +1 -1
- package/package.json +1 -1
- package/src/engine/config.ts +19 -0
- package/src/engine/history.ts +20 -2
- package/src/engine/host.ts +47 -1
- package/src/engine/index.ts +25 -1
- package/src/engine/indexing_groups.ts +350 -0
- package/src/engine/links.ts +309 -9
- package/src/engine/office.ts +53 -5
- package/src/engine/prompts/chat_system_prompt.ts +1 -1
- package/src/engine/prompts/index.ts +3 -0
- package/src/engine/prompts/indexing_user_message.ts +110 -29
- package/src/engine/requests.ts +107 -20
- package/src/engine/session.ts +741 -82
- package/src/engine/viewport_fill.ts +186 -0
- package/styles/chat.css +122 -0
package/src/engine/requests.ts
CHANGED
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
* state that stays in the consumer, not here;
|
|
11
11
|
* only the BgTaskEntry TYPE lives here)
|
|
12
12
|
*/
|
|
13
|
-
import { buildIndexingSystemPrompt, buildIndexingUserMessage, buildIndexingContinueMessage, buildIndexingRenderMessage } from './prompts';
|
|
14
|
-
import { isServerExtractable, isPagedReadFile, isImageVisionFile, makeExtractPlaceholder, makeRenderPlaceholder, RENDER_PAGES_PER_WINDOW, type ExtractDirective, type FileUrlDirective } from './office';
|
|
15
|
-
import { chatEngineConfig, pollOpt } from './config';
|
|
13
|
+
import { buildIndexingSystemPrompt, buildIndexingUserMessage, buildIndexingContinueMessage, buildIndexingRenderMessage, buildIndexingRenderContinueTemplate, buildIndexingWindowMessage } from './prompts';
|
|
14
|
+
import { isServerExtractable, isPagedReadFile, isImageVisionFile, isWindowedReadFile, makeExtractPlaceholder, makeRenderPlaceholder, makeWindowPlaceholder, RENDER_PAGES_PER_WINDOW, type ExtractDirective, type FileUrlDirective } from './office';
|
|
15
|
+
import { chatEngineConfig, pollOpt, windowedIndexingEnabled } from './config';
|
|
16
16
|
|
|
17
17
|
export const ANTHROPIC_MESSAGES_API_URL = 'https://api.anthropic.com/v1/messages';
|
|
18
18
|
const ANTHROPIC_MODELS_API_URL = 'https://api.anthropic.com/v1/models';
|
|
@@ -33,30 +33,52 @@ const OPENAI_WEB_SEARCH_EXTERNAL_WEB_ACCESS = true;
|
|
|
33
33
|
export const MCP_NAME = 'BunnyQuery';
|
|
34
34
|
|
|
35
35
|
export const DEFAULT_CLAUDE_MODEL = 'claude-sonnet-4-6';
|
|
36
|
-
export const DEFAULT_OPENAI_MODEL = 'gpt-5.
|
|
36
|
+
export const DEFAULT_OPENAI_MODEL = 'gpt-5.6-luna';
|
|
37
37
|
|
|
38
38
|
const mcpUrl = () => chatEngineConfig().mcpBaseUrl;
|
|
39
39
|
const clientSecretRequest = (opts: any) => chatEngineConfig().clientSecretRequest(opts);
|
|
40
40
|
|
|
41
|
+
// Resolve the per-image `detail` for OpenAI. The version match tolerates a
|
|
42
|
+
// trailing variant/date suffix (`gpt-5.4-nano`, `-mini`, `-2026-01-01`, …):
|
|
43
|
+
// previously the pattern was anchored with no suffix allowed, so EVERY suffixed
|
|
44
|
+
// model silently fell through to 'auto' — i.e. the cheap tiers that most need
|
|
45
|
+
// resolution were the ones getting downsampled images.
|
|
46
|
+
//
|
|
47
|
+
// Base models keep their exact previous behavior ('original'). A suffixed
|
|
48
|
+
// variant resolves to 'high' rather than 'original': 'high' is the universally
|
|
49
|
+
// supported value, and we have no way to confirm a given variant accepts
|
|
50
|
+
// 'original' — sending an unsupported value would fail the whole request, which
|
|
51
|
+
// is far worse than a slightly less detailed image.
|
|
41
52
|
const getOpenAIImageDetail = (model?: string) => {
|
|
42
53
|
const normalized = (model || DEFAULT_OPENAI_MODEL).trim().toLowerCase();
|
|
43
|
-
const match = normalized.match(/^gpt-(\d+)(?:\.(\d+))?$/);
|
|
54
|
+
const match = normalized.match(/^gpt-(\d+)(?:\.(\d+))?(-[a-z0-9.\-]+)?$/);
|
|
44
55
|
if (!match) {
|
|
45
56
|
return DEFAULT_OPENAI_IMAGE_DETAIL;
|
|
46
57
|
}
|
|
47
58
|
|
|
48
59
|
const major = Number(match[1]);
|
|
49
60
|
const minor = match[2] === undefined ? null : Number(match[2]);
|
|
61
|
+
const isVariant = !!match[3];
|
|
50
62
|
|
|
51
|
-
|
|
52
|
-
|
|
63
|
+
const supportsOriginal = major > 5 || (major === 5 && minor !== null && minor >= 4);
|
|
64
|
+
if (!supportsOriginal) {
|
|
65
|
+
return DEFAULT_OPENAI_IMAGE_DETAIL;
|
|
53
66
|
}
|
|
54
67
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
68
|
+
return isVariant ? 'high' : 'original';
|
|
69
|
+
};
|
|
58
70
|
|
|
59
|
-
|
|
71
|
+
// Per-image `detail` for WORKER-RENDERED document pages (the `_skapi_render`
|
|
72
|
+
// path). Same resolution as above with one difference: these are dense scans
|
|
73
|
+
// whose entire purpose is to be read, so 'auto' is never acceptable — it lets
|
|
74
|
+
// the API downsample exactly the pixels the model needs to OCR. Floor it at
|
|
75
|
+
// 'high'; models that support full-resolution 'original' still get it.
|
|
76
|
+
//
|
|
77
|
+
// Without this the worker falls back to its own model-blind default ('high'),
|
|
78
|
+
// which silently denies the strongest models the 'original' detail they support.
|
|
79
|
+
const getRenderImageDetail = (model?: string) => {
|
|
80
|
+
const detail = getOpenAIImageDetail(model);
|
|
81
|
+
return detail === DEFAULT_OPENAI_IMAGE_DETAIL ? 'high' : detail;
|
|
60
82
|
};
|
|
61
83
|
|
|
62
84
|
export type ClaudeRole = 'user' | 'assistant';
|
|
@@ -478,20 +500,66 @@ export async function notifyAgentSaveAttachment(info: AttachmentSaveInfo) {
|
|
|
478
500
|
const visionFile = !parsedContent && isImageVisionFile(attachment.name, attachment.mime);
|
|
479
501
|
const renderFrom = Math.max(0, info.renderFrom || 0);
|
|
480
502
|
const renderPlaceholder = visionFile ? makeRenderPlaceholder(attachment.storagePath) : undefined;
|
|
503
|
+
// Tell the worker what image `detail` to stamp on the injected page blocks.
|
|
504
|
+
// The worker is model-blind, so without this it applies a one-size default and
|
|
505
|
+
// the strongest models never get the 'original' detail they support. Only
|
|
506
|
+
// meaningful for OpenAI — Claude has no per-image detail knob and the worker
|
|
507
|
+
// ignores the field for it.
|
|
508
|
+
const renderDetail = platform === 'openai'
|
|
509
|
+
? getRenderImageDetail(info.model || DEFAULT_OPENAI_MODEL)
|
|
510
|
+
: undefined;
|
|
511
|
+
// `auto_continue` + `continue_text` hand the page loop to the WORKER: when its renderer
|
|
512
|
+
// reports pages left after this window, it builds the next pass from this template
|
|
513
|
+
// (substituting the window's 1-based start page for RENDER_FROM_TOKEN) and enqueues it
|
|
514
|
+
// itself. That is what makes a 500-page document index end-to-end — the loop no longer
|
|
515
|
+
// depends on the tab staying open, nor on the model correctly declaring itself unfinished.
|
|
481
516
|
const skapiRender = visionFile && renderPlaceholder
|
|
482
517
|
? {
|
|
483
518
|
_skapi_render: [
|
|
484
|
-
{
|
|
519
|
+
{
|
|
520
|
+
path: attachment.storagePath, from: renderFrom, count: RENDER_PAGES_PER_WINDOW,
|
|
521
|
+
placeholder: renderPlaceholder, name: attachment.name, mime: attachment.mime, detail: renderDetail,
|
|
522
|
+
auto_continue: true,
|
|
523
|
+
continue_text: buildIndexingRenderContinueTemplate(attachment, renderPlaceholder),
|
|
524
|
+
},
|
|
525
|
+
],
|
|
526
|
+
}
|
|
527
|
+
: {};
|
|
528
|
+
|
|
529
|
+
// SERVER-DRIVEN windowed read. When enabled, the worker reads one window of the file
|
|
530
|
+
// per request and continues from the reader's own cursor until the file is exhausted,
|
|
531
|
+
// so the traversal no longer lives inside the model's turn budget.
|
|
532
|
+
//
|
|
533
|
+
// Flag-gated because the BACKEND MUST SHIP FIRST: against a worker that does not strip
|
|
534
|
+
// `_skapi_window`, the directive reaches the provider as an unknown body field and the
|
|
535
|
+
// call fails terminally with no retry.
|
|
536
|
+
const windowedRead =
|
|
537
|
+
!visionFile && !parsedContent && windowedIndexingEnabled() &&
|
|
538
|
+
isWindowedReadFile(attachment.name, attachment.mime);
|
|
539
|
+
const windowPlaceholder = windowedRead ? makeWindowPlaceholder(attachment.storagePath) : undefined;
|
|
540
|
+
const skapiWindow = windowedRead && windowPlaceholder
|
|
541
|
+
? {
|
|
542
|
+
_skapi_window: [
|
|
543
|
+
{
|
|
544
|
+
path: attachment.storagePath,
|
|
545
|
+
cursor: null,
|
|
546
|
+
placeholder: windowPlaceholder,
|
|
547
|
+
name: attachment.name,
|
|
548
|
+
mime: attachment.mime,
|
|
549
|
+
kind: 'window',
|
|
550
|
+
auto_continue: true,
|
|
551
|
+
continue_text: buildIndexingWindowMessage(attachment, windowPlaceholder, true),
|
|
552
|
+
},
|
|
485
553
|
],
|
|
486
554
|
}
|
|
487
555
|
: {};
|
|
488
556
|
|
|
489
557
|
// Spreadsheets are read by PAGING through readFileContent (grid rows), NOT inlined - so
|
|
490
558
|
// they skip the inline server-extract and the agent is told to page the whole file.
|
|
491
|
-
const pagedRead = !visionFile && (continuing || (!parsedContent && isPagedReadFile(attachment.name, attachment.mime)));
|
|
559
|
+
const pagedRead = !visionFile && !windowedRead && (continuing || (!parsedContent && isPagedReadFile(attachment.name, attachment.mime)));
|
|
492
560
|
|
|
493
561
|
// Client-parsed content wins over server-side extraction.
|
|
494
|
-
const serverExtract = !visionFile && !continuing && !parsedContent && !pagedRead && isServerExtractable(attachment.name, attachment.mime);
|
|
562
|
+
const serverExtract = !visionFile && !windowedRead && !continuing && !parsedContent && !pagedRead && isServerExtractable(attachment.name, attachment.mime);
|
|
495
563
|
const placeholder = serverExtract ? makeExtractPlaceholder(attachment.storagePath) : undefined;
|
|
496
564
|
const extractContent: ExtractDirective[] | undefined =
|
|
497
565
|
serverExtract && placeholder
|
|
@@ -502,6 +570,8 @@ export async function notifyAgentSaveAttachment(info: AttachmentSaveInfo) {
|
|
|
502
570
|
|
|
503
571
|
const userMessage = (visionFile && renderPlaceholder)
|
|
504
572
|
? buildIndexingRenderMessage(attachment, renderPlaceholder, renderFrom)
|
|
573
|
+
: (windowedRead && windowPlaceholder)
|
|
574
|
+
? buildIndexingWindowMessage(attachment, windowPlaceholder, false)
|
|
505
575
|
: continuing
|
|
506
576
|
? buildIndexingContinueMessage(attachment)
|
|
507
577
|
: buildIndexingUserMessage(
|
|
@@ -541,6 +611,7 @@ export async function notifyAgentSaveAttachment(info: AttachmentSaveInfo) {
|
|
|
541
611
|
max_output_tokens: MAX_TOKENS,
|
|
542
612
|
...skapiExtract,
|
|
543
613
|
...skapiRender,
|
|
614
|
+
...skapiWindow,
|
|
544
615
|
input: [
|
|
545
616
|
{ role: 'system', content: systemPrompt },
|
|
546
617
|
{
|
|
@@ -589,6 +660,7 @@ export async function notifyAgentSaveAttachment(info: AttachmentSaveInfo) {
|
|
|
589
660
|
max_tokens: MAX_TOKENS,
|
|
590
661
|
...skapiExtract,
|
|
591
662
|
...skapiRender,
|
|
663
|
+
...skapiWindow,
|
|
592
664
|
system: [
|
|
593
665
|
{
|
|
594
666
|
type: 'text',
|
|
@@ -713,6 +785,23 @@ export async function listOpenAIModels(service: string, owner: string) {
|
|
|
713
785
|
// so the chat-history BETWEEN query never includes bg-queue items. '-' (45) works.
|
|
714
786
|
export const BG_INDEXING_QUEUE_SUFFIX = '-bg';
|
|
715
787
|
|
|
788
|
+
/**
|
|
789
|
+
* True when a request belongs to the background-indexing queue.
|
|
790
|
+
*
|
|
791
|
+
* Accepts BOTH shapes this value arrives in: the bare queue name the client sends
|
|
792
|
+
* ("<userId>-bg"), and the server qid that comes back on history/poll responses
|
|
793
|
+
* ("<service>:<queue>|<seq>"). Testing the tail of the raw value only works for the
|
|
794
|
+
* first: a qid ends in "|<seq>", so `endsWith('-bg')` is always false for it — which
|
|
795
|
+
* silently meant history items were NEVER recognised as background tasks.
|
|
796
|
+
*/
|
|
797
|
+
export function isBgIndexingQueue(queueName?: string): boolean {
|
|
798
|
+
if (typeof queueName !== 'string' || !queueName) return false;
|
|
799
|
+
const prefix = queueName.split('|')[0];
|
|
800
|
+
const idx = prefix.lastIndexOf(':');
|
|
801
|
+
const name = idx === -1 ? prefix : prefix.slice(idx + 1);
|
|
802
|
+
return name.slice(-BG_INDEXING_QUEUE_SUFFIX.length) === BG_INDEXING_QUEUE_SUFFIX;
|
|
803
|
+
}
|
|
804
|
+
|
|
716
805
|
// Pending background-indexing task descriptor. NOTE: the live mutable queue
|
|
717
806
|
// (a Vue `reactive([])` in agent.vue, a plain array in bunnyquery) is app-level
|
|
718
807
|
// state owned by the consumer — only the TYPE lives in the engine.
|
|
@@ -733,18 +822,16 @@ export type BgTaskEntry = {
|
|
|
733
822
|
|
|
734
823
|
// Token the indexing agent appends to its final message ONLY when it has fully read and
|
|
735
824
|
// saved the whole file. Its ABSENCE is what tells the client to run another CONTINUE pass.
|
|
825
|
+
//
|
|
826
|
+
// Applies to the TEXT/GRID paging path only. The vision path (rendered PDF pages) no longer
|
|
827
|
+
// asks the model whether it is finished — the worker advances that loop off the renderer's
|
|
828
|
+
// page count — so this marker has no say in whether a PDF keeps going.
|
|
736
829
|
export const INDEXING_COMPLETE_MARKER = 'INDEXING_COMPLETE';
|
|
737
830
|
// Cap on CONTINUE passes per file, so a file the agent can never mark complete (or a
|
|
738
831
|
// pathological loop) stops instead of re-dispatching forever. The text/grid paging path
|
|
739
832
|
// reads MANY windows within a single pass (the agent loops readFileContent in one turn), so
|
|
740
833
|
// a small cap suffices.
|
|
741
834
|
export const MAX_INDEXING_RESUME_PASSES = 6;
|
|
742
|
-
// The VISION path (rendered PDF pages) can only advance ONE page-window per pass, because the
|
|
743
|
-
// worker injects a single window of images into each request. A big scanned PDF therefore
|
|
744
|
-
// needs one pass PER window (pages / RENDER_PAGES_PER_WINDOW), so it gets a much higher cap.
|
|
745
|
-
// The loop still terminates naturally when the agent reaches the last window (it appends
|
|
746
|
-
// INDEXING_COMPLETE); this cap is only the backstop for a doc larger than it covers.
|
|
747
|
-
export const MAX_VISION_RESUME_PASSES = 40;
|
|
748
835
|
|
|
749
836
|
export async function getChatHistory(
|
|
750
837
|
params: { service?: string; owner?: string; platform: 'claude' | 'openai'; queue?: string },
|