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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bunnyquery",
3
- "version": "1.12.1",
3
+ "version": "1.13.0",
4
4
  "description": "Embeddable BunnyQuery AI chat widget + its framework-agnostic chat engine",
5
5
  "main": "bunnyquery.js",
6
6
  "exports": {
@@ -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(updatedTs) && updatedTs > 0 ? updatedTs : (isFinite(createdTs) && createdTs > 0 ? createdTs : undefined);
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.
@@ -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 = wallClockNow();
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 `updated`.
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 = wallClockNow();
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