bunnyquery 1.13.0 → 1.15.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 +4 -2
- package/bunnyquery.css +31 -0
- package/bunnyquery.js +575 -146
- package/dist/engine.cjs +574 -156
- package/dist/engine.cjs.map +1 -1
- package/dist/engine.d.mts +297 -4
- package/dist/engine.d.ts +297 -4
- package/dist/engine.mjs +550 -157
- package/dist/engine.mjs.map +1 -1
- package/package.json +1 -1
- package/src/engine/budget.ts +49 -3
- package/src/engine/citations.ts +181 -0
- package/src/engine/config.ts +44 -0
- package/src/engine/history.ts +9 -0
- package/src/engine/host.ts +7 -0
- package/src/engine/index.ts +17 -0
- package/src/engine/project_settings.ts +40 -0
- package/src/engine/prompts/chat_system_prompt.ts +27 -1
- package/src/engine/requests.ts +69 -37
- package/src/engine/retrieval_memory.ts +348 -0
- package/src/engine/session.ts +33 -4
- package/styles/chat.css +31 -0
package/bunnyquery.js
CHANGED
|
@@ -40,6 +40,104 @@
|
|
|
40
40
|
return text;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
// src/engine/project_settings.ts
|
|
44
|
+
var UPLOAD_ACCESS_GROUPS = ["public", "authorized", "private"];
|
|
45
|
+
var DEFAULT_UPLOAD_ACCESS_GROUP = "authorized";
|
|
46
|
+
var PROJECT_SETTINGS_UNIQUE_ID = "bq::settings";
|
|
47
|
+
var UPLOAD_ACCESS_LABELS = {
|
|
48
|
+
public: "Public",
|
|
49
|
+
authorized: "Signed in users",
|
|
50
|
+
private: "Only me"
|
|
51
|
+
};
|
|
52
|
+
var UPLOAD_ACCESS_HINTS = {
|
|
53
|
+
public: "Anyone can ask about this file, including visitors who are not logged in.",
|
|
54
|
+
authorized: "Only users signed in to this project can ask about this file.",
|
|
55
|
+
private: "Only you can ask about this file."
|
|
56
|
+
};
|
|
57
|
+
var DEFAULT_WEB_ACCESS = false;
|
|
58
|
+
function normalizeWebAccess(value) {
|
|
59
|
+
return value === true;
|
|
60
|
+
}
|
|
61
|
+
function webAccessFrom(data) {
|
|
62
|
+
return data ? normalizeWebAccess(data.web_access) : DEFAULT_WEB_ACCESS;
|
|
63
|
+
}
|
|
64
|
+
var CHAT_GREETING_MAX_LENGTH = 400;
|
|
65
|
+
function normalizeChatGreeting(value) {
|
|
66
|
+
if (typeof value !== "string") return "";
|
|
67
|
+
const trimmed = value.replace(/\r\n?/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
68
|
+
return trimmed.slice(0, CHAT_GREETING_MAX_LENGTH);
|
|
69
|
+
}
|
|
70
|
+
function chatGreetingFrom(data) {
|
|
71
|
+
return normalizeChatGreeting(data?.chat_greeting);
|
|
72
|
+
}
|
|
73
|
+
function normalizeUploadAccessGroup(value) {
|
|
74
|
+
return UPLOAD_ACCESS_GROUPS.indexOf(value) === -1 ? DEFAULT_UPLOAD_ACCESS_GROUP : value;
|
|
75
|
+
}
|
|
76
|
+
function normalizeProjectAccessSetting(value) {
|
|
77
|
+
if (value === "ask") return "ask";
|
|
78
|
+
return UPLOAD_ACCESS_GROUPS.indexOf(value) === -1 ? null : value;
|
|
79
|
+
}
|
|
80
|
+
function accessSettingFrom(data) {
|
|
81
|
+
return normalizeProjectAccessSetting(data?.upload_access_group);
|
|
82
|
+
}
|
|
83
|
+
function uploadAccessGroupFrom(data) {
|
|
84
|
+
const v = accessSettingFrom(data);
|
|
85
|
+
return v && v !== "ask" ? v : DEFAULT_UPLOAD_ACCESS_GROUP;
|
|
86
|
+
}
|
|
87
|
+
function asksUploadAccessFrom(data) {
|
|
88
|
+
return accessSettingFrom(data) === "ask";
|
|
89
|
+
}
|
|
90
|
+
var reader = null;
|
|
91
|
+
var cache = /* @__PURE__ */ new Map();
|
|
92
|
+
function configureProjectSettings(fn) {
|
|
93
|
+
reader = fn;
|
|
94
|
+
}
|
|
95
|
+
function entry(service) {
|
|
96
|
+
let e = cache.get(service);
|
|
97
|
+
if (!e) {
|
|
98
|
+
e = { data: null, settled: false, inflight: null };
|
|
99
|
+
cache.set(service, e);
|
|
100
|
+
}
|
|
101
|
+
return e;
|
|
102
|
+
}
|
|
103
|
+
function loadProjectSettings(service) {
|
|
104
|
+
if (!service) return Promise.resolve(null);
|
|
105
|
+
const e = entry(service);
|
|
106
|
+
if (e.settled) return Promise.resolve(e.data);
|
|
107
|
+
if (e.inflight) return e.inflight;
|
|
108
|
+
if (!reader) return Promise.resolve(null);
|
|
109
|
+
const run = reader(service).then((data) => data && typeof data === "object" ? data : null).catch(() => null).then((data) => {
|
|
110
|
+
const cur = entry(service);
|
|
111
|
+
if (cur.inflight === run) {
|
|
112
|
+
cur.data = data;
|
|
113
|
+
cur.settled = true;
|
|
114
|
+
cur.inflight = null;
|
|
115
|
+
}
|
|
116
|
+
return data;
|
|
117
|
+
});
|
|
118
|
+
e.inflight = run;
|
|
119
|
+
return run;
|
|
120
|
+
}
|
|
121
|
+
function primeProjectSettings(service) {
|
|
122
|
+
void loadProjectSettings(service);
|
|
123
|
+
}
|
|
124
|
+
function readyProjectSettings(service) {
|
|
125
|
+
return loadProjectSettings(service);
|
|
126
|
+
}
|
|
127
|
+
function cachedProjectSettings(service) {
|
|
128
|
+
const e = cache.get(service);
|
|
129
|
+
return e && e.settled ? e.data : null;
|
|
130
|
+
}
|
|
131
|
+
function projectUploadAccessGroup(service) {
|
|
132
|
+
return uploadAccessGroupFrom(cachedProjectSettings(service));
|
|
133
|
+
}
|
|
134
|
+
function projectAsksUploadAccess(service) {
|
|
135
|
+
return asksUploadAccessFrom(cachedProjectSettings(service));
|
|
136
|
+
}
|
|
137
|
+
function projectChatGreeting(service) {
|
|
138
|
+
return chatGreetingFrom(cachedProjectSettings(service));
|
|
139
|
+
}
|
|
140
|
+
|
|
43
141
|
// src/engine/config.ts
|
|
44
142
|
var _config = null;
|
|
45
143
|
function configureChatEngine(config) {
|
|
@@ -105,6 +203,17 @@
|
|
|
105
203
|
function liveStreamingEnabled() {
|
|
106
204
|
return _config?.liveStreaming === true;
|
|
107
205
|
}
|
|
206
|
+
function webAccessEnabled(service) {
|
|
207
|
+
const opt = _config ? _config.webAccess : void 0;
|
|
208
|
+
if (typeof opt === "boolean") return opt;
|
|
209
|
+
return webAccessFrom(service ? cachedProjectSettings(service) : null);
|
|
210
|
+
}
|
|
211
|
+
function resolveWebAccess(service) {
|
|
212
|
+
const opt = _config ? _config.webAccess : void 0;
|
|
213
|
+
if (typeof opt === "boolean") return Promise.resolve(opt);
|
|
214
|
+
if (!service) return Promise.resolve(false);
|
|
215
|
+
return loadProjectSettings(service).then(webAccessFrom, () => false);
|
|
216
|
+
}
|
|
108
217
|
function streamRecoveryEnabled() {
|
|
109
218
|
if (!_config) return false;
|
|
110
219
|
if (_config.streamRecovery === false) return false;
|
|
@@ -350,12 +459,14 @@ Extracted content of attached office files (read inline below; do NOT fetch thei
|
|
|
350
459
|
// src/engine/prompts/chat_system_prompt.ts
|
|
351
460
|
function buildChatSystemPrompt(params) {
|
|
352
461
|
const { projectId, serviceName, serviceDescription, greeting, canUpload} = params;
|
|
462
|
+
const webAccess = params.webAccess === true;
|
|
353
463
|
const g = params.indexAccessGroup;
|
|
354
464
|
const indexGroupLiteral = typeof g === "number" ? String(g) : g === "public" || g === "private" || g === "authorized" || g === "admin" ? `"${g}"` : '"authorized"';
|
|
355
465
|
const groundingRules = `ANSWER ONLY FROM THE DATA. Every fact you state about this project's data - a name, a number, a date, a status, what a document says, whether something exists - must come from one of four places: a tool result returned in THIS conversation, a file or image attached to it (including one you read through its attachment link), the project description at the end of this prompt, or what the user told you themselves in this chat. Your general knowledge, the open internet and a plausible guess are NOT sources. Never use them to answer a question about the user's data, to fill a gap in it, or to complete a record that came back partial. A value that is not in what you retrieved is unknown: say it is not in the data instead of supplying a likely one.
|
|
356
466
|
WHEN THE DATA DOES NOT HAVE IT, SAY SO. Once you have searched the way these rules require and nothing answers the question, tell the user plainly, in their language, that this project's data does not contain it, and say what you searched (the tables, files or terms) so they can tell missing data from a missed search. Do not answer from memory instead, and do not turn a missing answer into a general explanation of the topic. Offer what would let you answer it: ${canUpload === false ? `another name or file to look under, or asking the project's owner to add it` : `the document to upload, or another name or file to look under`}.
|
|
357
467
|
KEEP WHAT THE DATA SAYS APART FROM WHAT YOU WORKED OUT. A total you computed, a comparison, a trend, an interpretation or a recommendation is yours, not the data's: label it as such ("calculated from...", "this suggests...") and name the rows or files it was made from. Never present an estimate, an assumption or an inference as something the data states.
|
|
358
468
|
These three rules govern every claim about the project's data. They do not limit the About BunnyQuery section (answer product questions from it), a greeting, or text the user explicitly asks you to write.`;
|
|
469
|
+
const documentRules = `DOCUMENTS (a contract, a report, a manual, a letter, any uploaded file the user asks about the content of): FIRST FIND THE FILE, with getFileOutline when you know its storage path, otherwise with getUniqueId using unique_id "src::" and condition "gte" (narrow it by the name the user used). THEN FIND THE PAGE: call searchText with the file's path and the words the user asked about; it returns the file, the page and the matching excerpt. QUOTE THE EXACT TEXT it returned and CITE THE FILE AND THE PAGE ("report.pdf, page 7"); never present a paraphrase as a quote. If searchText answers no_page_text, the file was indexed before page text existed: read it with readFileContent instead, paging with the cursor until END OF FILE, and cite the file. When searchText and the file's records come back empty over files that HAVE page text, say that the data does not have it and name the files and the terms you searched. Never fill the gap from your general knowledge, and never from the web${webAccess ? ` without saying so. WEB ACCESS IS ON for this project: when the user asks for something that is not in the project's data, you may search the web for it, but every such fact must be marked as coming from the web (name the site), kept apart from what the project's data says, and never used to fill in or complete a document's content. This is the one exception to the four sources named above.` : `: this chat has no web tool.`}`;
|
|
359
470
|
let systemPrompt = `
|
|
360
471
|
You are a dedicated assistant for the project ID: "${projectId}".
|
|
361
472
|
Scope: Only answer questions about this project and its data. Do not answer questions about other projects or topics unrelated to this project. When the user refers to "my database", "my data", or "my files", treat those as references to this project's database and file storage. The ONE exception is BunnyQuery itself - what this app is, what it can do, and how to use it - which is always in scope: answer it from the "About BunnyQuery" section at the end of this prompt.
|
|
@@ -364,13 +475,14 @@ ${groundingRules}
|
|
|
364
475
|
NUMBERS FROM A SPREADSHEET: use queryGrid, never mental arithmetic over records. A total, a count, an average, a "how many mention X", a "which one is biggest" - all of those are computed server-side over EVERY row of the file and come back with the sheet, the row count and the row numbers they were made from. Records are a SAMPLE, and a sample added up is a confident wrong number. Quote the row count and the sheet alongside the figure so the reader can check it.
|
|
365
476
|
CALL queryGrid describe FIRST, before any figure. Workbooks routinely state the same money more than once: a detail sheet, then per-song, per-album and per-artist sheets that each re-total it, plus a summary sheet whose bottom row is the file total. Those look like four different answers and are one. describe names which sheets restate which, and which rows are totals. Pick ONE sheet, say which you picked, and never add figures across a sheet and its summary. If the reply carries a warning about restatement, repeat it to the user.
|
|
366
477
|
A FILE TOTAL IS NOT A ROW'S TOTAL. The biggest number on a summary sheet is the whole file, not the thing that was asked about. Before quoting any figure, check it is scoped to what the question named: filter by the column that identifies it and report how many rows matched.
|
|
478
|
+
${documentRules}
|
|
367
479
|
Complete answers over stored data: The database holds one record per spreadsheet row, and each uploaded file becomes many records. ONE file is routinely SPLIT ACROSS SEVERAL TABLES - a summary row in one table, its page or row content in another, its extracted photos and other media in "__MEDIA__", and the indexer often invents a differently-named table on each pass. An index or tag filter matches inside ONE table only and requires table_name: on getRecords, an index or tag sent with table_name but no access_group is auto-filled with access_group "authorized", but THIS project indexes at access_group ${indexGroupLiteral}, so pass access_group ${indexGroupLiteral} EXPLICITLY on EVERY query that names a table_name here, index or tag or plain - the auto-fill would search a group this project's data is not in and come back empty, and leaving access_group off a plain table query does NOT mean "all groups": unless you are the project's owner the server reads a table with no group as access_group 0 (public only), so a table indexed at ${indexGroupLiteral} comes back empty with its records sitting right there. Files uploaded before the project's setting changed may sit at another group, so when a scoped query comes back empty, retry it across the other groups (0, 1, "private") before concluding there is nothing, while an index or tag WITHOUT table_name FAILS with an error instead of answering, so read the error rather than guessing. Reference is the exception: reference ALONE spans EVERY table and EVERY access group, so getRecords with reference "src::<the file's storage path>" is the one call that returns a whole file's records wherever the indexer put them. Adding table_name narrows it to that table; access_group WITHOUT table_name fails with '"table" is required'; table_name on its own returns that whole table across all access groups ONLY for the project's owner, and only its access_group 0 records for any other user, so name the group whenever you name a table. For anything NOT scoped to a single file, call getTables FIRST, run the query once per table that could hold the answer, and combine the results. For any request that counts, sums, totals, lists every match, compares across records, finds which one, or asks whether something is present or ABSENT (for example "how many", "total spent", "which card", "is there any", "\uC5C6\uC5B4?", "\uD558\uB098\uB3C4 \uC5C6\uB098?"), you MUST read the COMPLETE matching set before answering. Query with fetch_all set to true, or page through getToolResponsePage until pagination.complete is true, across EVERY table and EVERY relevant file. A single default query returns only the first page (about 50 records). That is a SAMPLE. Never treat it as the whole dataset. If you already answered from one table and then realise another table holds more, do not simply apologise: re-run the sweep and give the complete answer.
|
|
368
480
|
Never assert absence from a partial read. Do not say "there is no X", "none", "not found", or "\uC544\uB2C8\uC694, \uC5C6\uC2B5\uB2C8\uB2E4" until a complete scan has come back empty. If you have not finished scanning every relevant table and file, keep querying instead of guessing. A confident "no" that later turns out wrong is worse than telling the user you are still checking.
|
|
369
481
|
Embedded values: a search term is often stored inside a larger string. A merchant "BAKSA" appears as "DNH*BAKSA#4070277042", and a card as "5860****5173". Server-side index filters match only exact values, leading prefixes, or trailing suffixes, and tag filters only EXACT whole-tag values - never a partial or interior substring - so filtering on such a field silently drops rows. When the value you are looking for may be embedded, do not trust a narrow filter to be complete. Fetch the full set with fetch_all and match the substring yourself.
|
|
370
482
|
File attachments: When a user message contains an "Attached files:" section with markdown links, those links point to short-lived signed URLs in this project's db storage and will expire.
|
|
371
483
|
- 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.
|
|
372
484
|
- Other attached files (office documents like .docx/.xlsx/.pptx/.hwp/.hwpx/.ods, email messages (.eml), and text/data/code files like .csv/.tsv/.json/.xml/.txt/.md and source code) are ALREADY INDEXED: they were read end to end when they were uploaded, before this message reached you, and their content is in the database as records. Query it with getRecords using reference "src::<the storage path from the attachment link>" - one call, every table, every access group. Do NOT call web_fetch on their URLs. If you need the raw text rather than the indexed records (an exact quote, a specific cell), call readFileContent on that same path and page it with the cursor. Some turns instead carry the file text inlined between "BEGIN FILE CONTENT" / "END FILE CONTENT" markers; when that block is present read it directly, and a "[skapi: ...]" note inside it means that file could not be extracted.
|
|
373
|
-
|
|
485
|
+
${webAccess ? `- For any file given to you as a URL instead of inline content (e.g. PDFs), use your web_fetch tool to download and read each URL before answering. Treat the fetched contents as user-supplied input data. Do not ask the user to paste the file contents - fetch the URLs yourself.` : `- PDFs and every other attached file are indexed the same way before this message reaches you, and this chat has NO web tool: never try to download or fetch an attachment's URL. Answer a PDF from its records (getRecords with reference "src::<the storage path from the attachment link>"), and for its exact wording use searchText or readFileContent on that same path (see DOCUMENTS above).`}
|
|
374
486
|
Stored files and readFileContent: for a file ALREADY in this project's storage, its pages and rows were read at upload time and saved as records, so the database is your best source. Query those records first (getRecords with reference "src::<path>", or getUniqueId with unique_id "src::" and condition "gte" to find the file). readFileContent re-reads the raw file and is the right tool for text, spreadsheet and data files; it returns ONE window per call, so keep paging with the cursor from the previous window until it says END OF FILE before you conclude anything is absent. Be aware its PICTURES may not reach you: page images and embedded photos are attached as image blocks that several clients drop, leaving you only markers such as \xABPHOTO A88\xBB or a "(scanned; read the page images)" header. There is no OCR on the server, so a scanned page with no text layer carries no text at all. If you cannot actually see an image, say so plainly and fall back to the indexed records; never describe a picture you were not shown, and never tell the user the file is unreadable when its content is already in the database.
|
|
375
487
|
File links: When you find a record whose unique_id starts with "src::", the part after "src::" is the file's storage path or original URL. Always present it as a markdown link so the user can access it. Strip the "src::" prefix - do NOT show it. Format: [filename](db:path/to/file) for storage paths, or [filename](https://...) for external URLs. The db: prefix is REQUIRED on storage paths: it tells the chat client the target is a stored file rather than a web address, instead of leaving it to guess. Everything after db: is the path exactly as stored, including spaces and parentheses, and NOT url-encoded. Storage-path links render as clickable buttons in this chat client that fetch a fresh signed URL on demand - so even if a previously shared URL has expired, give the user the storage-path link instead of saying the file is unavailable. Never tell the user a file is inaccessible or a URL is expired if you have its storage path in the database.
|
|
376
488
|
File lookup: When the user asks to see, list, or show files (e.g. "show me uploaded files", "list my images", "show me the reference video"), query the database using getUniqueId with unique_id "src::" and condition "gte" (or getRecords by table) to find all indexed file records; every file extracted out of a document has one too, in table "__MEDIA__" (access_group "authorized"). Present each result as a markdown link as described above. Never say you cannot access file storage: the paths are indexed in the database.
|
|
@@ -1030,6 +1142,224 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1030
1142
|
return "\u2026" + label.slice(label.length - (LINK_LABEL_MAX_DISPLAY_CHARS - 1));
|
|
1031
1143
|
}
|
|
1032
1144
|
|
|
1145
|
+
// src/engine/retrieval_memory.ts
|
|
1146
|
+
var RETRIEVAL_NOTES_MAX = 6;
|
|
1147
|
+
var RETRIEVAL_NOTES_TOKEN_BUDGET = 1200;
|
|
1148
|
+
var RETRIEVAL_NOTE_MAX_ITEMS = 8;
|
|
1149
|
+
var RETRIEVAL_NOTE_MAX_CHARS = 600;
|
|
1150
|
+
var RETRIEVAL_NOTE_PREFIX = "[Retrieval note, not shown to the user";
|
|
1151
|
+
var FILE_KEYS = ["path", "storage_path", "storagePath", "file", "file_path", "filePath"];
|
|
1152
|
+
var TABLE_KEYS = ["table_name", "tableName"];
|
|
1153
|
+
var GROUP_KEYS = ["access_group", "accessGroup"];
|
|
1154
|
+
var SRC_PREFIX = "src::";
|
|
1155
|
+
var RESULT_WALK_MAX_NODES = 400;
|
|
1156
|
+
var RESULT_WALK_MAX_DEPTH = 5;
|
|
1157
|
+
function isWebToolName(name) {
|
|
1158
|
+
return typeof name === "string" && /^web_(search|fetch)/.test(name);
|
|
1159
|
+
}
|
|
1160
|
+
function pushUnique(list, value) {
|
|
1161
|
+
if (typeof value !== "string") return;
|
|
1162
|
+
var v = value.trim();
|
|
1163
|
+
if (!v || list.indexOf(v) !== -1 || list.length >= RETRIEVAL_NOTE_MAX_ITEMS) return;
|
|
1164
|
+
list.push(v);
|
|
1165
|
+
}
|
|
1166
|
+
function pushGroup(list, value) {
|
|
1167
|
+
if (typeof value === "number" && isFinite(value)) pushUnique(list, String(value));
|
|
1168
|
+
else pushUnique(list, value);
|
|
1169
|
+
}
|
|
1170
|
+
function pushHit(list, path, page) {
|
|
1171
|
+
if (typeof path !== "string" || !path.trim()) return;
|
|
1172
|
+
var n = typeof page === "number" ? page : Number(page);
|
|
1173
|
+
if (!isFinite(n) || n <= 0) return;
|
|
1174
|
+
n = Math.floor(n);
|
|
1175
|
+
for (var i = 0; i < list.length; i++) {
|
|
1176
|
+
if (list[i].path === path && list[i].page === n) return;
|
|
1177
|
+
}
|
|
1178
|
+
if (list.length >= RETRIEVAL_NOTE_MAX_ITEMS) return;
|
|
1179
|
+
list.push({ path: path.trim(), page: n });
|
|
1180
|
+
}
|
|
1181
|
+
function pathFromSrcId(value) {
|
|
1182
|
+
if (typeof value !== "string") return null;
|
|
1183
|
+
if (value.indexOf(SRC_PREFIX) !== 0) return null;
|
|
1184
|
+
var rest = value.slice(SRC_PREFIX.length).trim();
|
|
1185
|
+
return rest ? rest : null;
|
|
1186
|
+
}
|
|
1187
|
+
function parseJsonLoose(value) {
|
|
1188
|
+
if (typeof value !== "string") return value;
|
|
1189
|
+
var t = value.trim();
|
|
1190
|
+
if (!t || t[0] !== "{" && t[0] !== "[") return null;
|
|
1191
|
+
try {
|
|
1192
|
+
return JSON.parse(t);
|
|
1193
|
+
} catch (e) {
|
|
1194
|
+
return null;
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
function readArguments(note, args) {
|
|
1198
|
+
if (!args || typeof args !== "object") return;
|
|
1199
|
+
var i;
|
|
1200
|
+
for (i = 0; i < FILE_KEYS.length; i++) pushUnique(note.files, args[FILE_KEYS[i]]);
|
|
1201
|
+
for (i = 0; i < TABLE_KEYS.length; i++) pushUnique(note.tables, args[TABLE_KEYS[i]]);
|
|
1202
|
+
for (i = 0; i < GROUP_KEYS.length; i++) pushGroup(note.accessGroups, args[GROUP_KEYS[i]]);
|
|
1203
|
+
if (typeof args.table === "string") pushUnique(note.tables, args.table);
|
|
1204
|
+
else if (args.table && typeof args.table === "object") {
|
|
1205
|
+
pushUnique(note.tables, args.table.name);
|
|
1206
|
+
pushGroup(note.accessGroups, args.table.access_group);
|
|
1207
|
+
}
|
|
1208
|
+
var ref = pathFromSrcId(args.reference);
|
|
1209
|
+
if (ref) pushUnique(note.files, ref);
|
|
1210
|
+
var uid = pathFromSrcId(args.unique_id);
|
|
1211
|
+
if (uid) pushUnique(note.files, uid);
|
|
1212
|
+
}
|
|
1213
|
+
function readHits(note, result) {
|
|
1214
|
+
var parsed = parseJsonLoose(result);
|
|
1215
|
+
if (!parsed || typeof parsed !== "object") return;
|
|
1216
|
+
var visited = 0;
|
|
1217
|
+
var walk = function(node, depth) {
|
|
1218
|
+
if (!node || typeof node !== "object" || depth > RESULT_WALK_MAX_DEPTH) return;
|
|
1219
|
+
if (++visited > RESULT_WALK_MAX_NODES) return;
|
|
1220
|
+
if (Array.isArray(node)) {
|
|
1221
|
+
for (var i = 0; i < node.length && visited <= RESULT_WALK_MAX_NODES; i++) walk(node[i], depth + 1);
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
if (typeof node.path === "string" && node.page !== void 0) {
|
|
1225
|
+
pushHit(note.hits, node.path, node.page);
|
|
1226
|
+
}
|
|
1227
|
+
for (var k in node) {
|
|
1228
|
+
if (!Object.prototype.hasOwnProperty.call(node, k)) continue;
|
|
1229
|
+
var v = node[k];
|
|
1230
|
+
if (v && typeof v === "object") walk(v, depth + 1);
|
|
1231
|
+
}
|
|
1232
|
+
};
|
|
1233
|
+
walk(parsed, 0);
|
|
1234
|
+
}
|
|
1235
|
+
function claudeResultText(block) {
|
|
1236
|
+
var c = block && block.content;
|
|
1237
|
+
if (typeof c === "string") return c;
|
|
1238
|
+
if (Array.isArray(c)) {
|
|
1239
|
+
return c.map(function(part) {
|
|
1240
|
+
return part && typeof part.text === "string" ? part.text : "";
|
|
1241
|
+
}).join("\n");
|
|
1242
|
+
}
|
|
1243
|
+
return "";
|
|
1244
|
+
}
|
|
1245
|
+
function emptyNote(source) {
|
|
1246
|
+
return { files: [], tables: [], accessGroups: [], hits: [], tools: [], web: false, source };
|
|
1247
|
+
}
|
|
1248
|
+
function noteHasContent(note) {
|
|
1249
|
+
return note.files.length > 0 || note.tables.length > 0 || note.accessGroups.length > 0 || note.hits.length > 0 || note.tools.length > 0 || note.web;
|
|
1250
|
+
}
|
|
1251
|
+
function noteFromToolBlocks(body) {
|
|
1252
|
+
if (!body || typeof body !== "object") return null;
|
|
1253
|
+
var note = emptyNote("tools");
|
|
1254
|
+
var sawTool = false;
|
|
1255
|
+
var i;
|
|
1256
|
+
var content = body.content;
|
|
1257
|
+
if (Array.isArray(content)) {
|
|
1258
|
+
var callNames = {};
|
|
1259
|
+
for (i = 0; i < content.length; i++) {
|
|
1260
|
+
var block = content[i];
|
|
1261
|
+
if (!block || typeof block !== "object") continue;
|
|
1262
|
+
var type = block.type;
|
|
1263
|
+
if (type === "mcp_tool_use" || type === "tool_use") {
|
|
1264
|
+
sawTool = true;
|
|
1265
|
+
pushUnique(note.tools, block.name);
|
|
1266
|
+
if (typeof block.id === "string" && typeof block.name === "string") callNames[block.id] = block.name;
|
|
1267
|
+
readArguments(note, block.input);
|
|
1268
|
+
} else if (type === "server_tool_use" || type === "web_search_tool_use") {
|
|
1269
|
+
sawTool = true;
|
|
1270
|
+
if (isWebToolName(block.name) || type === "web_search_tool_use") note.web = true;
|
|
1271
|
+
pushUnique(note.tools, block.name || type);
|
|
1272
|
+
} else if (type === "mcp_tool_result" || type === "tool_result") {
|
|
1273
|
+
sawTool = true;
|
|
1274
|
+
var name = typeof block.tool_use_id === "string" ? callNames[block.tool_use_id] : void 0;
|
|
1275
|
+
if (!block.is_error && (name === void 0 || name === "searchText")) readHits(note, claudeResultText(block));
|
|
1276
|
+
} else if (type === "web_search_tool_result" || type === "web_fetch_tool_result") {
|
|
1277
|
+
sawTool = true;
|
|
1278
|
+
note.web = true;
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
var output = body.output;
|
|
1283
|
+
if (Array.isArray(output)) {
|
|
1284
|
+
for (i = 0; i < output.length; i++) {
|
|
1285
|
+
var item = output[i];
|
|
1286
|
+
if (!item || typeof item !== "object") continue;
|
|
1287
|
+
var kind = typeof item.type === "string" ? item.type : "";
|
|
1288
|
+
if (kind === "mcp_call" || kind === "function_call") {
|
|
1289
|
+
sawTool = true;
|
|
1290
|
+
pushUnique(note.tools, item.name);
|
|
1291
|
+
readArguments(note, parseJsonLoose(item.arguments) || item.arguments);
|
|
1292
|
+
if (!item.error && item.name === "searchText") readHits(note, item.output);
|
|
1293
|
+
} else if (kind.indexOf("web_search") === 0 || kind.indexOf("web_fetch") === 0) {
|
|
1294
|
+
sawTool = true;
|
|
1295
|
+
note.web = true;
|
|
1296
|
+
pushUnique(note.tools, kind);
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
return sawTool ? note : null;
|
|
1301
|
+
}
|
|
1302
|
+
function retrievalNoteFromText(text) {
|
|
1303
|
+
if (typeof text !== "string" || !text) return null;
|
|
1304
|
+
var note = emptyNote("answer");
|
|
1305
|
+
var link = /\]\(db:([^)\s"]+)/g;
|
|
1306
|
+
var m;
|
|
1307
|
+
while ((m = link.exec(text)) !== null) pushUnique(note.files, m[1]);
|
|
1308
|
+
var src = /src::([^\s"'`<>)\]]+)/g;
|
|
1309
|
+
while ((m = src.exec(text)) !== null) {
|
|
1310
|
+
pushUnique(note.files, m[1].replace(/[.,;:!?]+$/, ""));
|
|
1311
|
+
}
|
|
1312
|
+
return noteHasContent(note) ? note : null;
|
|
1313
|
+
}
|
|
1314
|
+
function extractRetrievalNote(body, answerText) {
|
|
1315
|
+
var fromTools = noteFromToolBlocks(body);
|
|
1316
|
+
var fromText = retrievalNoteFromText(answerText);
|
|
1317
|
+
if (!fromTools) {
|
|
1318
|
+
if (fromText) return fromText;
|
|
1319
|
+
return providerBodyWasRead(body) ? emptyNote("body") : null;
|
|
1320
|
+
}
|
|
1321
|
+
if (fromText) {
|
|
1322
|
+
for (var i = 0; i < fromText.files.length; i++) pushUnique(fromTools.files, fromText.files[i]);
|
|
1323
|
+
}
|
|
1324
|
+
return noteHasContent(fromTools) ? fromTools : emptyNote("body");
|
|
1325
|
+
}
|
|
1326
|
+
function providerBodyWasRead(body) {
|
|
1327
|
+
if (!body || typeof body !== "object") return false;
|
|
1328
|
+
return Array.isArray(body.content) || Array.isArray(body.output);
|
|
1329
|
+
}
|
|
1330
|
+
function isRetrievalNote(value) {
|
|
1331
|
+
return !!value && typeof value === "object" && Array.isArray(value.files) && Array.isArray(value.tables) && Array.isArray(value.accessGroups) && Array.isArray(value.hits) && Array.isArray(value.tools);
|
|
1332
|
+
}
|
|
1333
|
+
function formatRetrievalNote(note) {
|
|
1334
|
+
if (!note || !noteHasContent(note)) return "";
|
|
1335
|
+
var parts = [];
|
|
1336
|
+
if (note.files.length) parts.push("files " + note.files.join(", "));
|
|
1337
|
+
if (note.tables.length) parts.push("tables " + note.tables.join(", "));
|
|
1338
|
+
if (note.accessGroups.length) parts.push("access_group " + note.accessGroups.join(", "));
|
|
1339
|
+
if (note.hits.length) {
|
|
1340
|
+
var byFile = {};
|
|
1341
|
+
var order = [];
|
|
1342
|
+
for (var i = 0; i < note.hits.length; i++) {
|
|
1343
|
+
var h = note.hits[i];
|
|
1344
|
+
if (!byFile[h.path]) {
|
|
1345
|
+
byFile[h.path] = [];
|
|
1346
|
+
order.push(h.path);
|
|
1347
|
+
}
|
|
1348
|
+
byFile[h.path].push(h.page);
|
|
1349
|
+
}
|
|
1350
|
+
parts.push("searchText hits " + order.map(function(p) {
|
|
1351
|
+
return p + " p" + byFile[p].join(", p");
|
|
1352
|
+
}).join("; "));
|
|
1353
|
+
}
|
|
1354
|
+
if (note.tools.length) parts.push("tools " + note.tools.join(", "));
|
|
1355
|
+
if (note.web) parts.push("web tool used");
|
|
1356
|
+
var line = RETRIEVAL_NOTE_PREFIX + ": this turn used " + parts.join(" | ") + "]";
|
|
1357
|
+
if (line.length > RETRIEVAL_NOTE_MAX_CHARS) {
|
|
1358
|
+
line = line.slice(0, RETRIEVAL_NOTE_MAX_CHARS - 4) + "...]";
|
|
1359
|
+
}
|
|
1360
|
+
return line;
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1033
1363
|
// src/engine/budget.ts
|
|
1034
1364
|
var CONTEXT_WINDOW_DEFAULT = { claude: 2e5, openai: 128e3 };
|
|
1035
1365
|
var CONTEXT_WINDOW_BY_MODEL = {
|
|
@@ -1050,6 +1380,9 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1050
1380
|
"claude-haiku-4-5": 2e5,
|
|
1051
1381
|
"claude-3-5-sonnet": 2e5,
|
|
1052
1382
|
// openai, exact ids
|
|
1383
|
+
// gpt-6.1: OpenAI's listing carries no window; taken as the gpt-6 family's until a
|
|
1384
|
+
// published figure says otherwise (2026-10-12, the default model).
|
|
1385
|
+
"gpt-6.1-sol": 105e4,
|
|
1053
1386
|
"gpt-6-astra": 105e4,
|
|
1054
1387
|
"gpt-6-sol": 105e4,
|
|
1055
1388
|
"gpt-6-luna": 105e4,
|
|
@@ -1069,6 +1402,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1069
1402
|
"claude-opus": 1e6,
|
|
1070
1403
|
"claude-sonnet": 1e6,
|
|
1071
1404
|
"claude-haiku": 2e5,
|
|
1405
|
+
"gpt-6.1": 105e4,
|
|
1072
1406
|
"gpt-6": 105e4,
|
|
1073
1407
|
"gpt-5.6": 105e4,
|
|
1074
1408
|
"gpt-5": 128e3
|
|
@@ -1086,6 +1420,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1086
1420
|
"claude-haiku-4-5": 64e3,
|
|
1087
1421
|
"claude-3-5-sonnet": 8e3,
|
|
1088
1422
|
// openai
|
|
1423
|
+
"gpt-6.1-sol": 128e3,
|
|
1089
1424
|
"gpt-6-astra": 128e3,
|
|
1090
1425
|
"gpt-6-sol": 128e3,
|
|
1091
1426
|
"gpt-6-luna": 128e3,
|
|
@@ -1105,6 +1440,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1105
1440
|
"claude-opus": 128e3,
|
|
1106
1441
|
"claude-sonnet": 64e3,
|
|
1107
1442
|
"claude-haiku": 64e3,
|
|
1443
|
+
"gpt-6.1": 128e3,
|
|
1108
1444
|
"gpt-6": 128e3,
|
|
1109
1445
|
"gpt-5.6": 128e3,
|
|
1110
1446
|
"gpt-5": 128e3
|
|
@@ -1182,6 +1518,23 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1182
1518
|
if (!content) return content;
|
|
1183
1519
|
return content.replace(/```([^\n`]+?\.[^\s.`]+)\n[\s\S]*?```/g, "[file previously attached: $1]");
|
|
1184
1520
|
}
|
|
1521
|
+
function applyRetrievalNotes(messages, latestIndex) {
|
|
1522
|
+
var out = messages.slice();
|
|
1523
|
+
var used = 0, tokens = 0;
|
|
1524
|
+
for (var i = messages.length - 1; i >= 0; i--) {
|
|
1525
|
+
if (used >= RETRIEVAL_NOTES_MAX) break;
|
|
1526
|
+
var m = messages[i];
|
|
1527
|
+
if (i === latestIndex || !m || m.role !== "assistant" || !isRetrievalNote(m._retrieval)) continue;
|
|
1528
|
+
var line = formatRetrievalNote(m._retrieval);
|
|
1529
|
+
if (!line) continue;
|
|
1530
|
+
var cost = estimateTextTokens(line) + 2;
|
|
1531
|
+
if (tokens + cost > RETRIEVAL_NOTES_TOKEN_BUDGET) break;
|
|
1532
|
+
tokens += cost;
|
|
1533
|
+
used++;
|
|
1534
|
+
out[i] = Object.assign({}, m, { content: (m.content || "") + "\n\n" + line });
|
|
1535
|
+
}
|
|
1536
|
+
return { messages: out, notesUsed: used, noteTokens: tokens };
|
|
1537
|
+
}
|
|
1185
1538
|
function buildBoundedChatMessages(options) {
|
|
1186
1539
|
var contextBasedBudget = contextBasedBudgetFor(options.platform, options.model, options.projectId);
|
|
1187
1540
|
var availableInputBudget = getInputTokenBudget(options.platform, options.model, options.projectId);
|
|
@@ -1197,6 +1550,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1197
1550
|
);
|
|
1198
1551
|
var windowed = options.history.slice(-maxHistoryMessages);
|
|
1199
1552
|
var latestIndex = windowed.length - 1;
|
|
1553
|
+
if (options.retrievalNotes !== false) windowed = applyRetrievalNotes(windowed, latestIndex).messages;
|
|
1200
1554
|
var trimmed = windowed.map(function(m, i2) {
|
|
1201
1555
|
if (i2 === latestIndex) return m;
|
|
1202
1556
|
var stripped = stripFileBlocksFromHistory(m.content);
|
|
@@ -1220,6 +1574,105 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
1220
1574
|
};
|
|
1221
1575
|
}
|
|
1222
1576
|
|
|
1577
|
+
// src/engine/citations.ts
|
|
1578
|
+
var DATA_TOOL_NAMES = [
|
|
1579
|
+
"getRecords",
|
|
1580
|
+
"queryGrid",
|
|
1581
|
+
"readFileContent",
|
|
1582
|
+
"searchText",
|
|
1583
|
+
"getFileOutline",
|
|
1584
|
+
"getUniqueId",
|
|
1585
|
+
"getTables",
|
|
1586
|
+
"getTags",
|
|
1587
|
+
"getIndexes"
|
|
1588
|
+
];
|
|
1589
|
+
var NO_DATA_TOOL_MARK = "No project data was read for this answer";
|
|
1590
|
+
function citationFileName(path) {
|
|
1591
|
+
var segs = String(path || "").split("/");
|
|
1592
|
+
for (var i = segs.length - 1; i >= 0; i--) {
|
|
1593
|
+
if (segs[i]) return segs[i];
|
|
1594
|
+
}
|
|
1595
|
+
return String(path || "");
|
|
1596
|
+
}
|
|
1597
|
+
function citationLabel(path, page) {
|
|
1598
|
+
return citationFileName(path) + (typeof page === "number" && page > 0 ? " p." + page : "");
|
|
1599
|
+
}
|
|
1600
|
+
function isMintablePath(path) {
|
|
1601
|
+
return !!path && !isHttpUrlLike(path);
|
|
1602
|
+
}
|
|
1603
|
+
function citationsFor(note) {
|
|
1604
|
+
if (!isRetrievalNote(note)) return [];
|
|
1605
|
+
var out = [];
|
|
1606
|
+
var hitPaths = [];
|
|
1607
|
+
var byPath = {};
|
|
1608
|
+
var i;
|
|
1609
|
+
for (i = 0; i < note.hits.length; i++) {
|
|
1610
|
+
var h = note.hits[i];
|
|
1611
|
+
if (!h || typeof h.path !== "string" || !isMintablePath(h.path)) continue;
|
|
1612
|
+
var page = typeof h.page === "number" ? h.page : Number(h.page);
|
|
1613
|
+
if (!isFinite(page) || page <= 0) continue;
|
|
1614
|
+
page = Math.floor(page);
|
|
1615
|
+
if (!byPath[h.path]) {
|
|
1616
|
+
byPath[h.path] = [];
|
|
1617
|
+
hitPaths.push(h.path);
|
|
1618
|
+
}
|
|
1619
|
+
if (byPath[h.path].indexOf(page) === -1) byPath[h.path].push(page);
|
|
1620
|
+
}
|
|
1621
|
+
for (i = 0; i < hitPaths.length; i++) {
|
|
1622
|
+
var p = hitPaths[i];
|
|
1623
|
+
var pages = byPath[p].slice().sort(function(a, b) {
|
|
1624
|
+
return a - b;
|
|
1625
|
+
});
|
|
1626
|
+
for (var j = 0; j < pages.length; j++) {
|
|
1627
|
+
out.push({ path: p, page: pages[j], label: citationLabel(p, pages[j]) });
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
for (i = 0; i < note.files.length; i++) {
|
|
1631
|
+
var f = note.files[i];
|
|
1632
|
+
if (typeof f !== "string" || !isMintablePath(f)) continue;
|
|
1633
|
+
if (byPath[f]) continue;
|
|
1634
|
+
var dup = false;
|
|
1635
|
+
for (var k = 0; k < out.length; k++) {
|
|
1636
|
+
if (out[k].path === f && out[k].page === void 0) {
|
|
1637
|
+
dup = true;
|
|
1638
|
+
break;
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
if (dup) continue;
|
|
1642
|
+
out.push({ path: f, label: citationLabel(f) });
|
|
1643
|
+
}
|
|
1644
|
+
return out;
|
|
1645
|
+
}
|
|
1646
|
+
function isDataToolName(name) {
|
|
1647
|
+
return DATA_TOOL_NAMES.indexOf(name) !== -1;
|
|
1648
|
+
}
|
|
1649
|
+
function answerUsedNoDataTool(note) {
|
|
1650
|
+
if (!isRetrievalNote(note)) return false;
|
|
1651
|
+
if (note.web) return false;
|
|
1652
|
+
for (var i = 0; i < note.tools.length; i++) {
|
|
1653
|
+
if (isDataToolName(note.tools[i])) return false;
|
|
1654
|
+
}
|
|
1655
|
+
return true;
|
|
1656
|
+
}
|
|
1657
|
+
function retrievalNoteOfAnswer(msg) {
|
|
1658
|
+
if (!msg || msg.role !== "assistant") return null;
|
|
1659
|
+
if (msg.isError || msg.isPending || msg.isBackgroundTask || msg._streamPending) return null;
|
|
1660
|
+
return isRetrievalNote(msg._retrieval) ? msg._retrieval : null;
|
|
1661
|
+
}
|
|
1662
|
+
function citationLinkPart(citation, resolveFreshHref) {
|
|
1663
|
+
var expiredHref = buildDisplayExpiredAttachmentHref(citation.path, citation.label);
|
|
1664
|
+
var cached = resolveFreshHref ? resolveFreshHref(expiredHref) : void 0;
|
|
1665
|
+
return {
|
|
1666
|
+
type: "link",
|
|
1667
|
+
label: truncateLabelForDisplay(citation.label),
|
|
1668
|
+
fullLabel: citation.label,
|
|
1669
|
+
href: cached || expiredHref,
|
|
1670
|
+
expired: !cached,
|
|
1671
|
+
expiredHref,
|
|
1672
|
+
remotePath: citation.path
|
|
1673
|
+
};
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1223
1676
|
// src/engine/download_encoding.ts
|
|
1224
1677
|
var BOM = "\uFEFF";
|
|
1225
1678
|
var BOM_EXTS = /* @__PURE__ */ new Set(["csv", "tsv", "tab", "txt", "text", "log"]);
|
|
@@ -2135,12 +2588,28 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2135
2588
|
var ANTHROPIC_BETA_HEADER = `${ANTHROPIC_MCP_BETA},${ANTHROPIC_WEB_FETCH_BETA},${ANTHROPIC_PROMPT_CACHING_BETA}`;
|
|
2136
2589
|
var WEB_FETCH_MAX_USES = 40;
|
|
2137
2590
|
var WEB_FETCH_MAX_CONTENT_TOKENS = 2e5;
|
|
2591
|
+
var claudeWebFetchTool = () => ({
|
|
2592
|
+
type: "web_fetch_20250910",
|
|
2593
|
+
name: "web_fetch",
|
|
2594
|
+
max_uses: WEB_FETCH_MAX_USES,
|
|
2595
|
+
citations: { enabled: true },
|
|
2596
|
+
max_content_tokens: WEB_FETCH_MAX_CONTENT_TOKENS
|
|
2597
|
+
});
|
|
2138
2598
|
var OPENAI_RESPONSES_API_URL = "https://api.openai.com/v1/responses";
|
|
2139
2599
|
var DEFAULT_OPENAI_IMAGE_DETAIL = "auto";
|
|
2140
2600
|
var OPENAI_WEB_SEARCH_EXTERNAL_WEB_ACCESS = true;
|
|
2601
|
+
var openaiWebSearchTool = () => ({
|
|
2602
|
+
type: "web_search",
|
|
2603
|
+
external_web_access: OPENAI_WEB_SEARCH_EXTERNAL_WEB_ACCESS
|
|
2604
|
+
});
|
|
2605
|
+
var webToolsForChat = (platform, webAccess) => {
|
|
2606
|
+
if (webAccess !== true) return [];
|
|
2607
|
+
if (platform === "claude") return [claudeWebFetchTool()];
|
|
2608
|
+
return [openaiWebSearchTool()] ;
|
|
2609
|
+
};
|
|
2141
2610
|
var MCP_NAME = "BunnyQuery";
|
|
2142
|
-
var DEFAULT_CLAUDE_MODEL = "claude-sonnet-5";
|
|
2143
|
-
var DEFAULT_OPENAI_MODEL = "gpt-
|
|
2611
|
+
var DEFAULT_CLAUDE_MODEL = "claude-sonnet-5-5";
|
|
2612
|
+
var DEFAULT_OPENAI_MODEL = "gpt-6.1-sol";
|
|
2144
2613
|
var mcpUrl = () => chatEngineConfig().mcpBaseUrl;
|
|
2145
2614
|
function withMcpParams(base, params) {
|
|
2146
2615
|
if (!base) return base;
|
|
@@ -2153,6 +2622,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2153
2622
|
}
|
|
2154
2623
|
var mcpContextParam = (platform, model) => getModelContextWindow(platform, model);
|
|
2155
2624
|
var mcpIndexingUrl = (platform = "openai", model) => withMcpParams(mcpUrl(), { profile: "index", ctx: mcpContextParam(platform, model) });
|
|
2625
|
+
var mcpChatUrl = (endpointUrl, platform, model) => withMcpParams(endpointUrl, { profile: "chat", ctx: mcpContextParam(platform, model) });
|
|
2156
2626
|
function mcpEndpointFor(anonymous, publicProjectId, service) {
|
|
2157
2627
|
if (!anonymous) return { url: mcpUrl(), token: "$ACCESS_TOKEN" };
|
|
2158
2628
|
const project = publicProjectId || service;
|
|
@@ -2338,8 +2808,10 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2338
2808
|
system,
|
|
2339
2809
|
mcpServer,
|
|
2340
2810
|
extractContent,
|
|
2341
|
-
fileUrls
|
|
2811
|
+
fileUrls,
|
|
2812
|
+
webAccess
|
|
2342
2813
|
}) {
|
|
2814
|
+
const web = typeof webAccess === "boolean" ? webAccess : await resolveWebAccess(service);
|
|
2343
2815
|
const mcpServerDefinition = {
|
|
2344
2816
|
type: "url",
|
|
2345
2817
|
name: mcpServer.name,
|
|
@@ -2398,13 +2870,10 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2398
2870
|
...mcpServer.defaultConfig ? { default_config: mcpServer.defaultConfig } : {},
|
|
2399
2871
|
...mcpServer.configs ? { configs: mcpServer.configs } : {}
|
|
2400
2872
|
},
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
citations: { enabled: true },
|
|
2406
|
-
max_content_tokens: WEB_FETCH_MAX_CONTENT_TOKENS
|
|
2407
|
-
}
|
|
2873
|
+
// `web_fetch` rides here only when the project's web switch is on. The
|
|
2874
|
+
// beta header above is left as it is: it only makes the tool TYPE
|
|
2875
|
+
// available, and a request that does not list the tool cannot use it.
|
|
2876
|
+
...webToolsForChat("claude", web)
|
|
2408
2877
|
]
|
|
2409
2878
|
}
|
|
2410
2879
|
});
|
|
@@ -2424,9 +2893,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2424
2893
|
fileUrls,
|
|
2425
2894
|
mcpServer: {
|
|
2426
2895
|
name: MCP_NAME,
|
|
2427
|
-
url:
|
|
2428
|
-
ctx: mcpContextParam("claude", model || DEFAULT_CLAUDE_MODEL)
|
|
2429
|
-
}),
|
|
2896
|
+
url: mcpChatUrl(endpoint.url, "claude", model || DEFAULT_CLAUDE_MODEL),
|
|
2430
2897
|
// Omitted entirely for an anonymous turn; the `if (mcpServer.authorizationToken)`
|
|
2431
2898
|
// guard below drops the key rather than sending an empty one.
|
|
2432
2899
|
authorizationToken: endpoint.token
|
|
@@ -2436,6 +2903,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2436
2903
|
const endpoint = mcpEndpointFor(mcpScope?.anonymous, mcpScope?.publicProjectId, service);
|
|
2437
2904
|
const resolvedModel = model || DEFAULT_OPENAI_MODEL;
|
|
2438
2905
|
const imageDetail = getOpenAIImageDetail(resolvedModel);
|
|
2906
|
+
const web = await resolveWebAccess(service);
|
|
2439
2907
|
const messageList = messages && messages.length ? prepareOpenAIMessages(messages, imageDetail) : [
|
|
2440
2908
|
{
|
|
2441
2909
|
role: "user",
|
|
@@ -2481,19 +2949,15 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2481
2949
|
{
|
|
2482
2950
|
type: "mcp",
|
|
2483
2951
|
server_label: MCP_NAME,
|
|
2484
|
-
server_url:
|
|
2952
|
+
server_url: mcpChatUrl(endpoint.url, "openai", resolvedModel),
|
|
2485
2953
|
require_approval: "never",
|
|
2486
2954
|
// No `headers` at all for an anonymous turn: `Bearer ` with an
|
|
2487
2955
|
// empty token is a credential the MCP server rejects, and the
|
|
2488
2956
|
// project-scoped endpoint needs none.
|
|
2489
2957
|
...endpoint.token ? { headers: { Authorization: "Bearer " + endpoint.token } } : {}
|
|
2490
2958
|
},
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
type: "web_search",
|
|
2494
|
-
external_web_access: OPENAI_WEB_SEARCH_EXTERNAL_WEB_ACCESS
|
|
2495
|
-
}
|
|
2496
|
-
]
|
|
2959
|
+
// `web_search` rides here only when the project's web switch is on.
|
|
2960
|
+
...webToolsForChat("openai", web)
|
|
2497
2961
|
]
|
|
2498
2962
|
}
|
|
2499
2963
|
});
|
|
@@ -2641,12 +3105,9 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2641
3105
|
require_approval: "never",
|
|
2642
3106
|
headers: { Authorization: "Bearer $ACCESS_TOKEN" }
|
|
2643
3107
|
},
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
external_web_access: OPENAI_WEB_SEARCH_EXTERNAL_WEB_ACCESS
|
|
2648
|
-
}
|
|
2649
|
-
]
|
|
3108
|
+
// UNCONDITIONAL, unlike a chat turn: the project's web switch
|
|
3109
|
+
// governs answers, not indexing. Kept exactly as it was.
|
|
3110
|
+
...[openaiWebSearchTool()]
|
|
2650
3111
|
]
|
|
2651
3112
|
}
|
|
2652
3113
|
}));
|
|
@@ -2699,13 +3160,9 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
2699
3160
|
type: "mcp_toolset",
|
|
2700
3161
|
mcp_server_name: MCP_NAME
|
|
2701
3162
|
},
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
max_uses: WEB_FETCH_MAX_USES,
|
|
2706
|
-
citations: { enabled: true },
|
|
2707
|
-
max_content_tokens: WEB_FETCH_MAX_CONTENT_TOKENS
|
|
2708
|
-
}
|
|
3163
|
+
// UNCONDITIONAL, unlike a chat turn: the project's web switch governs
|
|
3164
|
+
// answers, not indexing. Kept exactly as it was.
|
|
3165
|
+
claudeWebFetchTool()
|
|
2709
3166
|
]
|
|
2710
3167
|
}
|
|
2711
3168
|
}));
|
|
@@ -3185,6 +3642,10 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
3185
3642
|
if (item._fromBgChain) okm._fromBgChain = true;
|
|
3186
3643
|
if (item._isBgTask) okm.isBackgroundTask = true;
|
|
3187
3644
|
if (isCompact) okm._compact = true;
|
|
3645
|
+
if (!item._isBgTask && !isCompact) {
|
|
3646
|
+
var note = extractRetrievalNote(response, assistantText);
|
|
3647
|
+
if (note) okm._retrieval = note;
|
|
3648
|
+
}
|
|
3188
3649
|
if (serverItemId !== void 0) okm._serverItemId = serverItemId;
|
|
3189
3650
|
if (replyTs !== void 0) okm._ts = replyTs;
|
|
3190
3651
|
if (indexFile && isFinite(executedTs) && executedTs > 0) okm._tsStart = executedTs;
|
|
@@ -4880,6 +5341,10 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
4880
5341
|
return;
|
|
4881
5342
|
}
|
|
4882
5343
|
var reply = isErr ? { role: "assistant", content: getErrorMessage(body), isError: true, _serverItemId: itemId } : { role: "assistant", content: text, _serverItemId: itemId };
|
|
5344
|
+
if (!isErr) {
|
|
5345
|
+
var rNote = extractRetrievalNote(body, text);
|
|
5346
|
+
if (rNote) reply._retrieval = rNote;
|
|
5347
|
+
}
|
|
4883
5348
|
if (ownerKey && this.getHistoryCacheKey() !== ownerKey) {
|
|
4884
5349
|
this._applyReplyToCache(ownerKey, reply, itemId);
|
|
4885
5350
|
} else {
|
|
@@ -5288,7 +5753,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
5288
5753
|
if (isErrorResponseBody(response)) return { content: getErrorMessage(response), isError: true };
|
|
5289
5754
|
var answer = params.aiPlatform === "openai" ? extractOpenAIText(response) : extractClaudeText(response);
|
|
5290
5755
|
answer = (answer || "").trim();
|
|
5291
|
-
return { content: answer || "No text response received from AI provider.", isError: false };
|
|
5756
|
+
return { content: answer || "No text response received from AI provider.", isError: false, retrieval: extractRetrievalNote(response, answer) };
|
|
5292
5757
|
}).catch(function(err) {
|
|
5293
5758
|
return { content: getErrorMessage(err), isError: true };
|
|
5294
5759
|
}).then(function(result) {
|
|
@@ -5300,6 +5765,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
5300
5765
|
}
|
|
5301
5766
|
var reply = { role: "assistant", content: result.content, isError: result.isError };
|
|
5302
5767
|
if (dispatchItemId) reply._serverItemId = dispatchItemId;
|
|
5768
|
+
if (result.retrieval) reply._retrieval = result.retrieval;
|
|
5303
5769
|
self._applyReplyToCache(params.key, reply, dispatchItemId);
|
|
5304
5770
|
return result;
|
|
5305
5771
|
});
|
|
@@ -5963,6 +6429,10 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
5963
6429
|
}
|
|
5964
6430
|
if (ownerKey && this.getHistoryCacheKey() !== ownerKey) {
|
|
5965
6431
|
var offReply = isErrorResponseBody(response) ? { role: "assistant", content: getErrorMessage(response), isError: true } : { role: "assistant", content: ((platform === "openai" ? extractOpenAIText(response) : extractClaudeText(response)) || "").trim() || "No text response received from AI provider." };
|
|
6432
|
+
if (!offReply.isError) {
|
|
6433
|
+
var offNote = extractRetrievalNote(response, offReply.content);
|
|
6434
|
+
if (offNote) offReply._retrieval = offNote;
|
|
6435
|
+
}
|
|
5966
6436
|
this._applyReplyToCache(ownerKey, offReply, serverId);
|
|
5967
6437
|
if (serverId) this.cancelledServerIds.delete(serverId);
|
|
5968
6438
|
return;
|
|
@@ -5984,6 +6454,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
5984
6454
|
} else {
|
|
5985
6455
|
var answer = platform === "openai" ? extractOpenAIText(response) : extractClaudeText(response);
|
|
5986
6456
|
answer = (answer || "").trim() || "No text response received from AI provider.";
|
|
6457
|
+
var qNote = extractRetrievalNote(response, answer);
|
|
5987
6458
|
var lid = this._newLocalId();
|
|
5988
6459
|
if (targetIdx >= 0 && this.state.messages[targetIdx] && this.state.messages[targetIdx].isPending) {
|
|
5989
6460
|
var qPainted = this._paintedTextAt(targetIdx);
|
|
@@ -5991,16 +6462,21 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
5991
6462
|
var qSettled = { role: "assistant", content: qPainted, _localId: lid };
|
|
5992
6463
|
if (prevQ._serverItemId) qSettled._serverItemId = prevQ._serverItemId;
|
|
5993
6464
|
if (prevQ._ownerKey) qSettled._ownerKey = prevQ._ownerKey;
|
|
6465
|
+
if (qNote) qSettled._retrieval = qNote;
|
|
5994
6466
|
this.state.messages[targetIdx] = qSettled;
|
|
5995
6467
|
this.host.notify();
|
|
5996
6468
|
this.enqueueTypewrite(targetIdx, answer, lid, qPainted);
|
|
5997
6469
|
} else if (targetIdx >= 0) {
|
|
5998
|
-
|
|
6470
|
+
var qInsert = { role: "assistant", content: "", _localId: lid };
|
|
6471
|
+
if (qNote) qInsert._retrieval = qNote;
|
|
6472
|
+
this.state.messages.splice(targetIdx, 0, qInsert);
|
|
5999
6473
|
this.host.notify();
|
|
6000
6474
|
this.enqueueTypewrite(targetIdx, answer, lid);
|
|
6001
6475
|
} else {
|
|
6002
6476
|
var aiIdx = this.state.messages.length;
|
|
6003
|
-
|
|
6477
|
+
var qAppend = { role: "assistant", content: "", _localId: lid };
|
|
6478
|
+
if (qNote) qAppend._retrieval = qNote;
|
|
6479
|
+
this.state.messages.push(qAppend);
|
|
6004
6480
|
this.host.notify();
|
|
6005
6481
|
this.enqueueTypewrite(aiIdx, answer, lid);
|
|
6006
6482
|
}
|
|
@@ -6590,6 +7066,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
6590
7066
|
var settled = { role: "assistant", content: painted, isPending: false, _localId: lid };
|
|
6591
7067
|
if (prevSettled._serverItemId) settled._serverItemId = prevSettled._serverItemId;
|
|
6592
7068
|
if (prevSettled._ownerKey) settled._ownerKey = prevSettled._ownerKey;
|
|
7069
|
+
if (latest._retrieval) settled._retrieval = latest._retrieval;
|
|
6593
7070
|
this.state.messages[pendingIdx] = settled;
|
|
6594
7071
|
this._removeStrayPendingAssistants();
|
|
6595
7072
|
this.host.notify();
|
|
@@ -6810,6 +7287,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
6810
7287
|
var isErr = isErrorResponseBody(response);
|
|
6811
7288
|
var answer = isErr ? getErrorMessage(response) : ((platform === "openai" ? extractOpenAIText(response) : extractClaudeText(response)) || "").trim();
|
|
6812
7289
|
var reportedComplete = !isErr && !!answer && answer.indexOf(INDEXING_COMPLETE_MARKER) !== -1;
|
|
7290
|
+
var hNote = isErr ? null : extractRetrievalNote(response, answer);
|
|
6813
7291
|
var stripMarker = function(t) {
|
|
6814
7292
|
return reportedComplete ? t.split(INDEXING_COMPLETE_MARKER).join("").trim() : t;
|
|
6815
7293
|
};
|
|
@@ -6835,7 +7313,9 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
6835
7313
|
}
|
|
6836
7314
|
var hPainted = this._paintedTextAt(idx);
|
|
6837
7315
|
var lid = this._newLocalId();
|
|
6838
|
-
|
|
7316
|
+
var hReply = { role: "assistant", content: hPainted, _localId: lid, _serverItemId: itemId };
|
|
7317
|
+
if (hNote) hReply._retrieval = hNote;
|
|
7318
|
+
this.state.messages[idx] = hReply;
|
|
6839
7319
|
this.host.notify();
|
|
6840
7320
|
this.enqueueTypewrite(idx, text, lid, hPainted);
|
|
6841
7321
|
this.updateHistoryCache();
|
|
@@ -6869,6 +7349,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
6869
7349
|
}
|
|
6870
7350
|
var lid2 = this._newLocalId();
|
|
6871
7351
|
var reply = { role: "assistant", content: "", _localId: lid2, _serverItemId: itemId };
|
|
7352
|
+
if (hNote) reply._retrieval = hNote;
|
|
6872
7353
|
this.state.messages.splice(userIdx + 1, 0, reply);
|
|
6873
7354
|
this.host.notify();
|
|
6874
7355
|
this.enqueueTypewrite(userIdx + 1, text2, lid2);
|
|
@@ -8533,102 +9014,11 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
8533
9014
|
return out;
|
|
8534
9015
|
}
|
|
8535
9016
|
|
|
8536
|
-
// src/engine/project_settings.ts
|
|
8537
|
-
var UPLOAD_ACCESS_GROUPS = ["public", "authorized", "private"];
|
|
8538
|
-
var DEFAULT_UPLOAD_ACCESS_GROUP = "authorized";
|
|
8539
|
-
var PROJECT_SETTINGS_UNIQUE_ID = "bq::settings";
|
|
8540
|
-
var UPLOAD_ACCESS_LABELS = {
|
|
8541
|
-
public: "Public",
|
|
8542
|
-
authorized: "Signed in users",
|
|
8543
|
-
private: "Only me"
|
|
8544
|
-
};
|
|
8545
|
-
var UPLOAD_ACCESS_HINTS = {
|
|
8546
|
-
public: "Anyone can ask about this file, including visitors who are not logged in.",
|
|
8547
|
-
authorized: "Only users signed in to this project can ask about this file.",
|
|
8548
|
-
private: "Only you can ask about this file."
|
|
8549
|
-
};
|
|
8550
|
-
var CHAT_GREETING_MAX_LENGTH = 400;
|
|
8551
|
-
function normalizeChatGreeting(value) {
|
|
8552
|
-
if (typeof value !== "string") return "";
|
|
8553
|
-
const trimmed = value.replace(/\r\n?/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
8554
|
-
return trimmed.slice(0, CHAT_GREETING_MAX_LENGTH);
|
|
8555
|
-
}
|
|
8556
|
-
function chatGreetingFrom(data) {
|
|
8557
|
-
return normalizeChatGreeting(data?.chat_greeting);
|
|
8558
|
-
}
|
|
8559
|
-
function normalizeUploadAccessGroup(value) {
|
|
8560
|
-
return UPLOAD_ACCESS_GROUPS.indexOf(value) === -1 ? DEFAULT_UPLOAD_ACCESS_GROUP : value;
|
|
8561
|
-
}
|
|
8562
|
-
function normalizeProjectAccessSetting(value) {
|
|
8563
|
-
if (value === "ask") return "ask";
|
|
8564
|
-
return UPLOAD_ACCESS_GROUPS.indexOf(value) === -1 ? null : value;
|
|
8565
|
-
}
|
|
8566
|
-
function accessSettingFrom(data) {
|
|
8567
|
-
return normalizeProjectAccessSetting(data?.upload_access_group);
|
|
8568
|
-
}
|
|
8569
|
-
function uploadAccessGroupFrom(data) {
|
|
8570
|
-
const v = accessSettingFrom(data);
|
|
8571
|
-
return v && v !== "ask" ? v : DEFAULT_UPLOAD_ACCESS_GROUP;
|
|
8572
|
-
}
|
|
8573
|
-
function asksUploadAccessFrom(data) {
|
|
8574
|
-
return accessSettingFrom(data) === "ask";
|
|
8575
|
-
}
|
|
8576
|
-
var reader = null;
|
|
8577
|
-
var cache = /* @__PURE__ */ new Map();
|
|
8578
|
-
function configureProjectSettings(fn) {
|
|
8579
|
-
reader = fn;
|
|
8580
|
-
}
|
|
8581
|
-
function entry(service) {
|
|
8582
|
-
let e = cache.get(service);
|
|
8583
|
-
if (!e) {
|
|
8584
|
-
e = { data: null, settled: false, inflight: null };
|
|
8585
|
-
cache.set(service, e);
|
|
8586
|
-
}
|
|
8587
|
-
return e;
|
|
8588
|
-
}
|
|
8589
|
-
function loadProjectSettings(service) {
|
|
8590
|
-
if (!service) return Promise.resolve(null);
|
|
8591
|
-
const e = entry(service);
|
|
8592
|
-
if (e.settled) return Promise.resolve(e.data);
|
|
8593
|
-
if (e.inflight) return e.inflight;
|
|
8594
|
-
if (!reader) return Promise.resolve(null);
|
|
8595
|
-
const run = reader(service).then((data) => data && typeof data === "object" ? data : null).catch(() => null).then((data) => {
|
|
8596
|
-
const cur = entry(service);
|
|
8597
|
-
if (cur.inflight === run) {
|
|
8598
|
-
cur.data = data;
|
|
8599
|
-
cur.settled = true;
|
|
8600
|
-
cur.inflight = null;
|
|
8601
|
-
}
|
|
8602
|
-
return data;
|
|
8603
|
-
});
|
|
8604
|
-
e.inflight = run;
|
|
8605
|
-
return run;
|
|
8606
|
-
}
|
|
8607
|
-
function primeProjectSettings(service) {
|
|
8608
|
-
void loadProjectSettings(service);
|
|
8609
|
-
}
|
|
8610
|
-
function readyProjectSettings(service) {
|
|
8611
|
-
return loadProjectSettings(service);
|
|
8612
|
-
}
|
|
8613
|
-
function cachedProjectSettings(service) {
|
|
8614
|
-
const e = cache.get(service);
|
|
8615
|
-
return e && e.settled ? e.data : null;
|
|
8616
|
-
}
|
|
8617
|
-
function projectUploadAccessGroup(service) {
|
|
8618
|
-
return uploadAccessGroupFrom(cachedProjectSettings(service));
|
|
8619
|
-
}
|
|
8620
|
-
function projectAsksUploadAccess(service) {
|
|
8621
|
-
return asksUploadAccessFrom(cachedProjectSettings(service));
|
|
8622
|
-
}
|
|
8623
|
-
function projectChatGreeting(service) {
|
|
8624
|
-
return chatGreetingFrom(cachedProjectSettings(service));
|
|
8625
|
-
}
|
|
8626
|
-
|
|
8627
9017
|
// src/index.js
|
|
8628
9018
|
(function() {
|
|
8629
9019
|
var MCP_PROD = "https://mcp.broadwayinc.computer";
|
|
8630
9020
|
var MCP_DEV = "https://mcp-dev.broadwayinc.computer";
|
|
8631
|
-
var BQ_VERSION = "1.
|
|
9021
|
+
var BQ_VERSION = "1.15.0" ;
|
|
8632
9022
|
var ATTACHMENT_URL_EXPIRES_SECONDS = 600;
|
|
8633
9023
|
var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
8634
9024
|
var GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
@@ -10600,7 +10990,12 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
10600
10990
|
// reaches it has settled the fetch first: sendMessage awaits
|
|
10601
10991
|
// readyProjectSettings on the text-only branch, and the attachment
|
|
10602
10992
|
// branch resolves the upload group before it dispatches.
|
|
10603
|
-
indexAccessGroup: projectUploadAccessGroup(S.projectId)
|
|
10993
|
+
indexAccessGroup: projectUploadAccessGroup(S.projectId),
|
|
10994
|
+
// Whether this chat may reach the open web. The request builders
|
|
10995
|
+
// attach web_fetch / web_search on the same switch, so the prompt
|
|
10996
|
+
// follows the tool list. Same sync cache read as the group above,
|
|
10997
|
+
// settled by the same readyProjectSettings before a send.
|
|
10998
|
+
webAccess: webAccessEnabled(S.projectId)});
|
|
10604
10999
|
}
|
|
10605
11000
|
function refreshSkapiSession() {
|
|
10606
11001
|
return S.skapi.getProfile({ refreshToken: true }).then(function() {
|
|
@@ -10783,6 +11178,17 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
10783
11178
|
unavailable: isLinkUnavailable(link, unavailableLinkMap)
|
|
10784
11179
|
});
|
|
10785
11180
|
}
|
|
11181
|
+
function citationChipsHtml(note) {
|
|
11182
|
+
var cites = citationsFor(note);
|
|
11183
|
+
var html = "";
|
|
11184
|
+
for (var i = 0; i < cites.length; i++) {
|
|
11185
|
+
var part = citationLinkPart(cites[i], function(expiredHref) {
|
|
11186
|
+
return refreshedExpiredLinkMap[expiredHref];
|
|
11187
|
+
});
|
|
11188
|
+
html += linkToAnchorHtml(part, false);
|
|
11189
|
+
}
|
|
11190
|
+
return html;
|
|
11191
|
+
}
|
|
10786
11192
|
function buildLinkPartFromGroups(full, g1, g2, g3, g4, g5, g6) {
|
|
10787
11193
|
return classifyInlineLink(full, [g1, g2, g3, g4, g5, g6], {
|
|
10788
11194
|
projectId: S.projectId,
|
|
@@ -11963,6 +12369,16 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
11963
12369
|
var md = h("div", { class: "bq-md", translate: "no", html: parseMsgPartsHtml(msg.content) });
|
|
11964
12370
|
md.addEventListener("click", onBubbleLinkClick);
|
|
11965
12371
|
bubble.appendChild(md);
|
|
12372
|
+
var citeNote = retrievalNoteOfAnswer(msg);
|
|
12373
|
+
if (citeNote) {
|
|
12374
|
+
var citeHtml = citationChipsHtml(citeNote);
|
|
12375
|
+
if (citeHtml) {
|
|
12376
|
+
var citeRow = h("div", { class: "bq-citations", translate: "no", html: citeHtml });
|
|
12377
|
+
citeRow.addEventListener("click", onBubbleLinkClick);
|
|
12378
|
+
bubble.appendChild(citeRow);
|
|
12379
|
+
}
|
|
12380
|
+
if (answerUsedNoDataTool(citeNote)) bubble.appendChild(h("span", { class: "bq-no-source-note", text: NO_DATA_TOOL_MARK }));
|
|
12381
|
+
}
|
|
11966
12382
|
if (msg.isUploadingAttachments) bubble.appendChild(h("span", { class: "bq-pending-note", text: "(Uploading files...)" }));
|
|
11967
12383
|
else if (msg.isAwaitingIndexing) bubble.appendChild(h("span", { class: "bq-pending-note", text: "(Indexing files...)" }));
|
|
11968
12384
|
else if (msg.isPendingQueued) bubble.appendChild(h("span", { class: "bq-pending-note", text: "(In queue)" }));
|
|
@@ -13240,16 +13656,24 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
13240
13656
|
// null → follow the project's own "Allow anonymous users" setting
|
|
13241
13657
|
// (getConnectionInfo().conf.require_login); true/false pins it.
|
|
13242
13658
|
allowAnonymous: null,
|
|
13659
|
+
// Whether the chat's answers may reach the open web (Claude web_fetch,
|
|
13660
|
+
// OpenAI web_search). null = follow the project's own "Web access for
|
|
13661
|
+
// answers" setting on its Project Settings page (`web_access` in the
|
|
13662
|
+
// bq::settings record), which is off unless the owner turned it on;
|
|
13663
|
+
// true/false pins it for this page, both ways. Read at
|
|
13664
|
+
// configureChatEngine time and when each chat request is built.
|
|
13665
|
+
webAccess: null,
|
|
13243
13666
|
// Server-driven windowed indexing; read at configureChatEngine time.
|
|
13244
13667
|
// Listed here so the defaults object is the full opt surface.
|
|
13245
13668
|
windowedIndexing: true,
|
|
13246
13669
|
// Live streaming of chat turns; read at configureChatEngine time.
|
|
13247
|
-
//
|
|
13248
|
-
//
|
|
13249
|
-
//
|
|
13670
|
+
// ON by default: every region's polling worker relays the response
|
|
13671
|
+
// bytes now, so the answer paints as it arrives instead of up to one
|
|
13672
|
+
// poll tick after it finished. Pass false for buffered replies.
|
|
13673
|
+
// Still a REQUEST, not a switch: it is refused (with a warning, falling
|
|
13250
13674
|
// back to buffered) when the embedder's own skapi-js is too old to
|
|
13251
13675
|
// carry skapi's half of the stream flag. See skapiSupportsStreaming.
|
|
13252
|
-
liveStreaming:
|
|
13676
|
+
liveStreaming: true,
|
|
13253
13677
|
// Socket delivery for the streamed reply. OFF unless the embedder asks,
|
|
13254
13678
|
// and separately from liveStreaming, because this widget runs on someone
|
|
13255
13679
|
// else's page with someone else's skapi instance: skapi's joinRealtime
|
|
@@ -13273,7 +13697,7 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
13273
13697
|
S.booted = true;
|
|
13274
13698
|
console.log("[bunnyquery] v" + BQ_VERSION);
|
|
13275
13699
|
var canStream = skapiSupportsStreaming(S.skapi);
|
|
13276
|
-
var liveStreaming = S.opts.liveStreaming === true;
|
|
13700
|
+
var liveStreaming = S.opts.liveStreaming == null ? true : S.opts.liveStreaming === true;
|
|
13277
13701
|
if (liveStreaming && !canStream) {
|
|
13278
13702
|
liveStreaming = false;
|
|
13279
13703
|
console.warn(
|
|
@@ -13314,17 +13738,22 @@ Index the REMAINING windows - one record per row/item, looking at any page image
|
|
|
13314
13738
|
windowedIndexing: S.opts.windowedIndexing !== false,
|
|
13315
13739
|
// Client-side attachment parsers (e.g. an .hwp parser) passed via init opts.
|
|
13316
13740
|
attachmentParsers: S.opts.attachmentParsers || void 0,
|
|
13741
|
+
// Web access for chat answers. Handed over ONLY when the embedder
|
|
13742
|
+
// pinned it: an undefined here means "ask the project", and the engine
|
|
13743
|
+
// then reads the project's `web_access` setting when it builds each
|
|
13744
|
+
// chat request (off unless the owner turned it on). Passing the null
|
|
13745
|
+
// default through would read as a decision.
|
|
13746
|
+
webAccess: typeof S.opts.webAccess === "boolean" ? S.opts.webAccess : void 0,
|
|
13317
13747
|
// ---- live streaming (mirrored in agent.vue's ai_agent.ts) --------
|
|
13318
|
-
//
|
|
13319
|
-
//
|
|
13320
|
-
//
|
|
13321
|
-
//
|
|
13322
|
-
//
|
|
13323
|
-
//
|
|
13324
|
-
//
|
|
13325
|
-
//
|
|
13326
|
-
//
|
|
13327
|
-
// region, where agent.vue flips one module constant.
|
|
13748
|
+
// On by default since every region's polling worker relays the
|
|
13749
|
+
// response bytes. It used to be off for the shipping-order reason
|
|
13750
|
+
// windowedIndexing had: against a worker that does not relay, the
|
|
13751
|
+
// request has its `since` cursor rejected or the row keeps an SSE
|
|
13752
|
+
// transcript where the readers expect a parsed document, and the turn
|
|
13753
|
+
// reads back as an empty answer (a streamed row settles with a STATUS
|
|
13754
|
+
// AND NO BODY on purpose). An embed against such a worker passes
|
|
13755
|
+
// liveStreaming: false. The widget takes it as an init opt because an
|
|
13756
|
+
// embed picks its own region, where agent.vue flips one module constant.
|
|
13328
13757
|
// It also needs a skapi-js that supports `stream`/`onStream`, and the
|
|
13329
13758
|
// page's pin is the EMBEDDER's, so the request is granted above by
|
|
13330
13759
|
// skapiSupportsStreaming rather than taken on trust here.
|