bunnyquery 1.12.1 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bunnyquery.js +87 -4
- package/dist/engine.cjs +34 -3
- package/dist/engine.cjs.map +1 -1
- package/dist/engine.d.mts +14 -0
- package/dist/engine.d.ts +14 -0
- package/dist/engine.mjs +34 -3
- package/dist/engine.mjs.map +1 -1
- package/package.json +1 -1
- package/src/engine/history.ts +10 -1
- package/src/engine/prompts/chat_system_prompt.ts +23 -0
- package/src/engine/session.ts +38 -3
package/package.json
CHANGED
package/src/engine/history.ts
CHANGED
|
@@ -798,8 +798,17 @@ export function mapHistoryListToMessages(list: any[], platform: 'claude' | 'open
|
|
|
798
798
|
// cases the pass simply shows no duration, which is right: the honest answer
|
|
799
799
|
// to "how long did it take" is nothing, not the queue wait dressed up as it.
|
|
800
800
|
var executedTs = Number(item && item.executed);
|
|
801
|
+
// When skapi recorded the destination's response as having finished arriving.
|
|
802
|
+
// It is the answer's time whenever the row carries it, because `updated` is
|
|
803
|
+
// not that for a STREAMED turn: finalizing the turn stores its answer a few
|
|
804
|
+
// seconds later and moves `updated` to that moment. Absent on rows settled
|
|
805
|
+
// before the worker recorded it, on a turn that failed with no response, and
|
|
806
|
+
// under an SDK that does not project it. All three fall back to `updated`,
|
|
807
|
+
// which is exactly what this showed before.
|
|
808
|
+
var respondedTs = Number(item && item.responded);
|
|
801
809
|
var userTs = isFinite(createdTs) && createdTs > 0 ? createdTs : (isFinite(updatedTs) && updatedTs > 0 ? updatedTs : undefined);
|
|
802
|
-
var replyTs = isFinite(
|
|
810
|
+
var replyTs = isFinite(respondedTs) && respondedTs > 0 ? respondedTs
|
|
811
|
+
: (isFinite(updatedTs) && updatedTs > 0 ? updatedTs : (isFinite(createdTs) && createdTs > 0 ? createdTs : undefined));
|
|
803
812
|
|
|
804
813
|
if (userText) {
|
|
805
814
|
var displayContent;
|
|
@@ -72,10 +72,33 @@ export function buildChatSystemPrompt(params: ChatSystemPromptParams): string {
|
|
|
72
72
|
: (g === 'public' || g === 'private' || g === 'authorized' || g === 'admin') ? `"${g}"`
|
|
73
73
|
: '"authorized"';
|
|
74
74
|
|
|
75
|
+
// ---- Grounding ---------------------------------------------------------
|
|
76
|
+
// The rules around this block were all written against the OPPOSITE mistake: a
|
|
77
|
+
// model that says "not found" before it has really looked. Read alone they push
|
|
78
|
+
// one way only (keep searching, never say it is absent), and nothing said the
|
|
79
|
+
// other half: state only what the data returned. Customers reported answers
|
|
80
|
+
// that were not in their data at all, which is the one thing this product must
|
|
81
|
+
// never do, so the other half is spelled out here.
|
|
82
|
+
//
|
|
83
|
+
// Three rules, each closing one way an invented answer gets out: a fact with no
|
|
84
|
+
// source, a "not found" replaced by general knowledge, and an inference worded
|
|
85
|
+
// as if the data said it. The last line keeps them from swallowing what is NOT
|
|
86
|
+
// a claim about the project's data (product help, a greeting, authored text).
|
|
87
|
+
// The upload suggestion follows canUpload for the same reason the About section
|
|
88
|
+
// does: a visitor who cannot upload must never be told to.
|
|
89
|
+
const groundingRules =
|
|
90
|
+
`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.
|
|
91
|
+
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
|
|
92
|
+
? `another name or file to look under, or asking the project's owner to add it`
|
|
93
|
+
: `the document to upload, or another name or file to look under`}.
|
|
94
|
+
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.
|
|
95
|
+
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.`;
|
|
96
|
+
|
|
75
97
|
let systemPrompt = `
|
|
76
98
|
You are a dedicated assistant for the project ID: "${projectId}".
|
|
77
99
|
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.
|
|
78
100
|
Knowledge lookup: Before saying you don't know or that something isn't in the chat history, ALWAYS query this project's database through the available MCP tools to look for the answer. The user's data is the source of truth - the chat transcript is not. Only respond with "I don't know" or "I couldn't find that" after you have actually searched the project's data and come back empty.
|
|
101
|
+
${groundingRules}
|
|
79
102
|
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.
|
|
80
103
|
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.
|
|
81
104
|
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.
|
package/src/engine/session.ts
CHANGED
|
@@ -1220,6 +1220,35 @@ export class ChatSession {
|
|
|
1220
1220
|
/** Live streams by server item id. One per in-flight streamed turn. */
|
|
1221
1221
|
private liveStreams: { [itemId: string]: LiveStreamState } = {};
|
|
1222
1222
|
|
|
1223
|
+
/** When skapi recorded a turn's response as having finished arriving, by server
|
|
1224
|
+
* item id. Read off the terminal envelope of a STREAMED turn (the only settle
|
|
1225
|
+
* that carries it, see _settleLiveStream) and used as the answer bubble's time
|
|
1226
|
+
* in place of this browser's clock. Entries are never removed: a settle reaches
|
|
1227
|
+
* here twice (the poll's onResponse, then the promise), and the value for an id
|
|
1228
|
+
* cannot change. */
|
|
1229
|
+
private _respondedAt: { [itemId: string]: number } = {};
|
|
1230
|
+
|
|
1231
|
+
private _noteRespondedAt(itemId: string, response: any): void {
|
|
1232
|
+
if (!itemId || !isCsrStatusEnvelope(response)) return;
|
|
1233
|
+
var at = Number(response.responded);
|
|
1234
|
+
if (!isFinite(at) || at <= 0) return;
|
|
1235
|
+
// Self-healing for a session built without the constructor (the engine tests
|
|
1236
|
+
// drive Object.create(ChatSession.prototype)), like typewriterQueue below.
|
|
1237
|
+
if (!this._respondedAt) this._respondedAt = {};
|
|
1238
|
+
this._respondedAt[itemId] = at;
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
/** The time a live answer bubble is stamped with: when skapi received the
|
|
1242
|
+
* response, if the settle told us, else now. "Now" is what every bubble got
|
|
1243
|
+
* before, and it is still right for a buffered turn, whose settle hands back the
|
|
1244
|
+
* destination's body and nothing of skapi's; a history load then replaces it
|
|
1245
|
+
* with the server's value. */
|
|
1246
|
+
private _replyStampFor(msg: ChatMessage | undefined): number {
|
|
1247
|
+
var id = msg ? msg._serverItemId : undefined;
|
|
1248
|
+
var at = (typeof id === 'string' && id && this._respondedAt) ? this._respondedAt[id] : undefined;
|
|
1249
|
+
return typeof at === 'number' ? at : wallClockNow();
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1223
1252
|
/**
|
|
1224
1253
|
* Open (or re-open) the live stream for `itemId`, or null when this poll must
|
|
1225
1254
|
* not carry one.
|
|
@@ -1472,6 +1501,10 @@ export class ChatSession {
|
|
|
1472
1501
|
private _settleLiveStream(st: LiveStreamState, response: any): any {
|
|
1473
1502
|
this._closeLiveStream(st, true);
|
|
1474
1503
|
if (!isCsrStatusEnvelope(response)) return response;
|
|
1504
|
+
// Before any return below, and for every terminal status: the bubble that
|
|
1505
|
+
// settles this turn is stamped from it whether it ends as an answer, an error
|
|
1506
|
+
// or a truncation.
|
|
1507
|
+
this._noteRespondedAt(st.id, response);
|
|
1475
1508
|
// RESOLVED only, and this is not caution, it is correctness. 'cancelled' is the
|
|
1476
1509
|
// envelope the user's own Stop produces, and _isCancelledPollResult downstream
|
|
1477
1510
|
// is what settles the turn as stopped: hand it an assembled body instead and a
|
|
@@ -3377,7 +3410,7 @@ export class ChatSession {
|
|
|
3377
3410
|
insertAtTarget(msg: ChatMessage, targetIdx: number): void {
|
|
3378
3411
|
// Error/direct replies land here rather than through the typewriter, so this
|
|
3379
3412
|
// is where they pick up their display time.
|
|
3380
|
-
if (msg && msg.role === 'assistant' && msg._ts === undefined) msg._ts =
|
|
3413
|
+
if (msg && msg.role === 'assistant' && msg._ts === undefined) msg._ts = this._replyStampFor(msg);
|
|
3381
3414
|
// Overwrite only a placeholder that genuinely belongs to the turn above it.
|
|
3382
3415
|
// Two things now sit at this index that must never be replaced by a chat
|
|
3383
3416
|
// answer: a background "Indexing:" placeholder (a file's rows are inserted
|
|
@@ -4069,9 +4102,11 @@ export class ChatSession {
|
|
|
4069
4102
|
// Stamp the reply bubble's display time as it starts revealing. This is the
|
|
4070
4103
|
// single chokepoint every typed reply (immediate, queued, and bg-resolution)
|
|
4071
4104
|
// passes through, so it is where a live reply gets its "when it arrived"
|
|
4072
|
-
// timestamp; a history reload later replaces it with the server
|
|
4105
|
+
// timestamp; a history reload later replaces it with the server's own.
|
|
4106
|
+
// _replyStampFor prefers the time skapi received the response when the settle
|
|
4107
|
+
// reported one (a streamed turn), over this browser's clock.
|
|
4073
4108
|
var target = this.state.messages[idx];
|
|
4074
|
-
if (target && target._ts === undefined) target._ts =
|
|
4109
|
+
if (target && target._ts === undefined) target._ts = this._replyStampFor(target);
|
|
4075
4110
|
// SELF-HEALING, because this is now reached from the live paint path too. The
|
|
4076
4111
|
// queue is a class field, so it is only initialised by the constructor; any
|
|
4077
4112
|
// caller holding a ChatSession built another way (the engine tests drive
|