bunnyquery 1.8.11 → 1.8.13
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.css +16 -3
- package/bunnyquery.js +41 -20
- package/dist/engine.cjs +23 -3
- package/dist/engine.cjs.map +1 -1
- package/dist/engine.d.mts +56 -1
- package/dist/engine.d.ts +56 -1
- package/dist/engine.mjs +23 -4
- package/dist/engine.mjs.map +1 -1
- package/package.json +1 -1
- package/src/engine/greeting.ts +49 -0
- package/src/engine/index.ts +4 -0
- package/src/engine/prompts/chat_system_prompt.ts +48 -3
- package/src/widget.css +9 -0
- package/styles/chat.css +7 -3
package/package.json
CHANGED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The chat's opening line, in ONE place.
|
|
3
|
+
*
|
|
4
|
+
* Both clients paint this bubble themselves (the widget builds DOM nodes,
|
|
5
|
+
* agent.vue renders template markup) because the project name sits inside its
|
|
6
|
+
* own translate="no" element. Forking the sentence between them is how the two
|
|
7
|
+
* copies drifted before, so the sentence lives here and each client only
|
|
8
|
+
* decides how to draw the three pieces.
|
|
9
|
+
*
|
|
10
|
+
* It is ALSO what the assistant is told it opened with (buildChatSystemPrompt's
|
|
11
|
+
* `greeting`): the bubble is pure client-side chrome and never enters the
|
|
12
|
+
* message history, so without this the model cannot answer "what do you mean,
|
|
13
|
+
* indexed?" or "which files should I upload?" about its own first line.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export type ChatGreetingParams = {
|
|
17
|
+
/** Project display name. Rendered by the client inside translate="no". */
|
|
18
|
+
projectName?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Whether this session can attach files at all. False for an anonymous
|
|
21
|
+
* widget visitor and for a frozen database seen by a non-admin: telling
|
|
22
|
+
* those users to upload is a dead end, so they get the ask-first line.
|
|
23
|
+
*/
|
|
24
|
+
canUpload?: boolean;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type ChatGreetingParts = {
|
|
28
|
+
/** Text before the project name. */
|
|
29
|
+
lead: string;
|
|
30
|
+
/** The quoted project name, or "" when the project has no name. */
|
|
31
|
+
name: string;
|
|
32
|
+
/** Text after the project name. */
|
|
33
|
+
tail: string;
|
|
34
|
+
/** The whole line as plain text: what the assistant is told it said. */
|
|
35
|
+
text: string;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export function buildChatGreeting(params: ChatGreetingParams): ChatGreetingParts {
|
|
39
|
+
const name = params.projectName ? '"' + params.projectName + '"' : '';
|
|
40
|
+
// canUpload is opt-out: a caller that does not know defaults to the
|
|
41
|
+
// upload-first line, which is the right lead for every ordinary session.
|
|
42
|
+
const lead = params.canUpload === false
|
|
43
|
+
? 'Hi! Ask me anything about the data in your project'
|
|
44
|
+
: 'Hi! Start by attaching the files related to your project';
|
|
45
|
+
const tail = params.canUpload === false
|
|
46
|
+
? '.'
|
|
47
|
+
: ', or pasting plain text into the chat. Once they are indexed, ask me anything about that data.';
|
|
48
|
+
return { lead, name, tail, text: lead + (name ? ' ' + name : '') + tail };
|
|
49
|
+
}
|
package/src/engine/index.ts
CHANGED
|
@@ -41,6 +41,10 @@ export {
|
|
|
41
41
|
|
|
42
42
|
export * from './prompts';
|
|
43
43
|
|
|
44
|
+
// The opening bubble's sentence, shared so the two clients cannot fork it and
|
|
45
|
+
// so the assistant can be told what it opened with (see greeting.ts).
|
|
46
|
+
export { buildChatGreeting, type ChatGreetingParams, type ChatGreetingParts } from './greeting';
|
|
47
|
+
|
|
44
48
|
// Pure helpers (Tier-1.5): error detection, token budgeting, link/path
|
|
45
49
|
// normalization, and history mapping — shared so both consumers stay identical.
|
|
46
50
|
export { getErrorMessage, isErrorResponseBody, isAuthExpiredError, isNonRetryableRequestError, isProviderApiKeyError } from './errors';
|
|
@@ -17,18 +17,37 @@ export type ChatSystemPromptParams = {
|
|
|
17
17
|
serviceName?: string;
|
|
18
18
|
/** Project description. When present, name + description are appended. */
|
|
19
19
|
serviceDescription?: string;
|
|
20
|
+
/**
|
|
21
|
+
* The opening bubble's text (buildChatGreeting().text). That bubble is
|
|
22
|
+
* client-side chrome and never enters the message history, so the model is
|
|
23
|
+
* told about it here. Without it, "what do you mean?" about its own first
|
|
24
|
+
* line is unanswerable.
|
|
25
|
+
*/
|
|
26
|
+
greeting?: string;
|
|
27
|
+
/**
|
|
28
|
+
* Whether this user can attach files. False for an anonymous widget visitor
|
|
29
|
+
* and for a frozen database seen by a non-admin, where the upload
|
|
30
|
+
* instructions below would send them at an affordance they do not have.
|
|
31
|
+
*/
|
|
32
|
+
canUpload?: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Which UI the user is in. The console has pages (Files, Settings) the
|
|
35
|
+
* embedded widget does not, so the "where do I do that" directions are only
|
|
36
|
+
* given when the caller says which one this is.
|
|
37
|
+
*/
|
|
38
|
+
client?: 'console' | 'widget';
|
|
20
39
|
};
|
|
21
40
|
|
|
22
41
|
export function buildChatSystemPrompt(params: ChatSystemPromptParams): string {
|
|
23
|
-
const { projectId, serviceName, serviceDescription } = params;
|
|
42
|
+
const { projectId, serviceName, serviceDescription, greeting, canUpload, client } = params;
|
|
24
43
|
|
|
25
44
|
let systemPrompt = `
|
|
26
45
|
You are a dedicated assistant for the project ID: "${projectId}".
|
|
27
|
-
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.
|
|
46
|
+
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.
|
|
28
47
|
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.
|
|
29
48
|
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" (where the indexer writes; pass access_group explicitly, including 0, to search another group), 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. 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", "없어?", "하나도 없나?"), 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.
|
|
30
49
|
Never assert absence from a partial read. Do not say "there is no X", "none", "not found", or "아니요, 없습니다" 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.
|
|
31
|
-
Embedded values: a search term is often stored inside a larger string. A merchant "
|
|
50
|
+
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.
|
|
32
51
|
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.
|
|
33
52
|
- 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.
|
|
34
53
|
- Other attached files (office documents like .docx/.xlsx/.pptx/.hwp/.hwpx/.ods, 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.
|
|
@@ -55,6 +74,32 @@ Zucchini,29,$43.50
|
|
|
55
74
|
\`\`\`
|
|
56
75
|
The same pattern applies to any format - name the block after the file you intend: \`\`\`my-data.json, \`\`\`index.html, \`\`\`sample.txt, and so on.`;
|
|
57
76
|
|
|
77
|
+
// ---- About BunnyQuery -------------------------------------------------
|
|
78
|
+
// The product self-knowledge. Without it the Scope line above turns every
|
|
79
|
+
// "what is this?" / "how do I upload?" / "why don't you know anything?" into
|
|
80
|
+
// a refusal, which is exactly the moment a new user asks them: the opening
|
|
81
|
+
// bubble invites an upload, and the reply to "which files?" has to land.
|
|
82
|
+
// Keep every claim here TRUE and checkable in the product; the closing rule
|
|
83
|
+
// tells the model to admit ignorance rather than invent the rest.
|
|
84
|
+
systemPrompt += `
|
|
85
|
+
About BunnyQuery (this app - questions about it are in scope):
|
|
86
|
+
You are the assistant inside BunnyQuery, an AI assistant for the user's own business data. Instead of digging through folders, dashboards and files, the user uploads their documents, spreadsheets, images, notes and records, BunnyQuery indexes them into this project's database, and you answer questions, write reports and summarize from THAT data rather than from the open internet. Each project has its own data, its own AI platform (ChatGPT or Claude, powered by the project owner's own API key) and its own base prompt. BunnyQuery is built on Skapi (www.skapi.com), so the same project database is also reachable over MCP from any MCP-compatible AI client (mcp.broadwayinc.computer), and this chat can be embedded in a website as a widget with one script tag. Answer product questions from the facts in this section. If you are asked something about BunnyQuery that is NOT stated here - pricing, plan limits, a roadmap, a feature you cannot see - say you are not certain and point the user at the project owner or the BunnyQuery site, rather than inventing it.
|
|
87
|
+
How data gets in: ${canUpload === false
|
|
88
|
+
? `this user CANNOT upload in this session (they are not signed in, or the project's database is frozen for non-admins), and the attach affordances are hidden from them. Never instruct them to attach, drag in or upload a file, and never blame a missing answer on them not having uploaded it. Answer from what is already indexed, and when something genuinely is not in the project, say so and suggest asking the project's owner to add it.`
|
|
89
|
+
: `the user attaches files to a chat message with the paperclip button in the composer, or drags and drops them onto the chat (whole folders work; up to 20 files per message). Uploaded files land in this project's file storage and are indexed automatically: read end to end and turned into database records. "Indexed" means exactly that, and it is why you can only answer from a file once its indexing has finished. While a file indexes, the chat shows a status row for it: yellow while it is working, green when it is indexed, red if it failed. A large file is indexed in windows over several passes, which takes longer; indexing runs on the server, so it keeps going if the user closes the page and the row is still there when they come back. The user can also paste plain text straight into the chat and ask you to save it - store it with the postRecords tool. BunnyQuery reads over 50 formats: office documents (.docx, .xlsx, .pptx, .hwp, .hwpx, .odt, .ods, .odp, .epub), PDFs, images, .csv/.tsv, .json, .xml, .html, .txt/.md and source code. Images and scanned PDFs are read with vision at index time.`}
|
|
90
|
+
Getting answers out: the user asks in plain language, in any language, and you answer from this project's data. You can also produce reports and downloadable files (CSV and the rest) as described in the File generation rules above, and any stored file can be handed back as a link, with images rendering inline in the chat.${client === 'console'
|
|
91
|
+
? `
|
|
92
|
+
Where things are in the BunnyQuery console (this user is in it, at bunnyquery.com): the left nav has "Query" (this chat), "Files" (browse this project's stored files, upload more, and see which are indexed), "Collaborators" (invite teammates or clients so they can ask questions themselves) and "Settings" (the AI platform, model and API key, the project's description / base prompt, which is added to your instructions, and the Freeze Database switch that blocks writes). Plans and billing live on the project's Subscription page - send the user there rather than quoting prices, which you do not know.`
|
|
93
|
+
: ''}${client === 'widget'
|
|
94
|
+
? `
|
|
95
|
+
This chat is the BunnyQuery widget embedded in a website, so the user may have no access to the project console: keep any instructions to what can be done here in the chat.`
|
|
96
|
+
: ''}`;
|
|
97
|
+
|
|
98
|
+
if (greeting) {
|
|
99
|
+
systemPrompt += `
|
|
100
|
+
Your opening message: this chat always opens with a fixed line from you, reading """${greeting}""". It is rendered by the client and is NOT part of the message history you receive, so the user can reply to it ("which files?", "what do you mean by indexed?", "what can you do?") with nothing in the transcript to refer back to. Treat that line as something you said, and answer the follow-up from this section.`;
|
|
101
|
+
}
|
|
102
|
+
|
|
58
103
|
if (serviceDescription) {
|
|
59
104
|
systemPrompt += `
|
|
60
105
|
Project name: "${serviceName ?? ''}"
|
package/src/widget.css
CHANGED
|
@@ -1013,6 +1013,15 @@
|
|
|
1013
1013
|
.bq-input-row { flex-direction: column; padding: 0.65rem 0.75rem 0.75rem; }
|
|
1014
1014
|
.bq-input-wrap { width: 100%; flex: 0 0 auto; }
|
|
1015
1015
|
.bq-input-row .btn { width: 100%; }
|
|
1016
|
+
/* `flex-basis: 100%` on these two means FULL WIDTH only while the row is a
|
|
1017
|
+
row. The line above flips it to a column, where the main axis is vertical
|
|
1018
|
+
and the same declaration claims the container's full HEIGHT: the chips
|
|
1019
|
+
then fill the column, and since the row still wraps, the input and Send
|
|
1020
|
+
wrap into a SECOND COLUMN, off to the right of the screen. That is the
|
|
1021
|
+
attachment bug, and it is why it never appeared on the dashboard, whose
|
|
1022
|
+
own mobile block already carries these two lines (agent.vue). */
|
|
1023
|
+
.bq-attachments { flex-basis: auto; width: 100%; }
|
|
1024
|
+
.bq-attachment-warning { flex-basis: auto; width: 100%; }
|
|
1016
1025
|
.bq-bubble { max-width: 100%; }
|
|
1017
1026
|
.bq-page { padding: 2rem 1.25rem; }
|
|
1018
1027
|
.bq-section-title { padding: 0.75rem; }
|
package/styles/chat.css
CHANGED
|
@@ -56,7 +56,11 @@
|
|
|
56
56
|
cancelled bubbles, indexing passes (the collapsed rows' members), and the
|
|
57
57
|
empty-chat greeting. Inlined data URI so both consumers get it without
|
|
58
58
|
markup or asset-hosting changes. */
|
|
59
|
-
|
|
59
|
+
/* The greeting is NOT excluded here any more. It used to be
|
|
60
|
+
(:not(.bq-empty-greeting)), because it was a one-off notice shown only to an
|
|
61
|
+
empty chat. It is now a permanent first message in every conversation, so it
|
|
62
|
+
is an assistant bubble like any other and wears the same face. */
|
|
63
|
+
.bq-message.is-assistant:not(.is-error):not(.is-cancelled):not(.bq-index-pass) .bq-bubble::before {
|
|
60
64
|
content: "";
|
|
61
65
|
/* block, not float: the bunny takes the first line by itself and the
|
|
62
66
|
message starts on the next line below it */
|
|
@@ -71,12 +75,12 @@
|
|
|
71
75
|
/* While the reply is still "Thinking..." the bubble holds only the dot-trail
|
|
72
76
|
loader: put the bunny INLINE with the dots instead of stacking above them.
|
|
73
77
|
Settled replies keep the block bunny with the message on the next line. */
|
|
74
|
-
.bq-message.is-assistant:not(.is-error):not(.is-cancelled):not(.bq-index-pass)
|
|
78
|
+
.bq-message.is-assistant:not(.is-error):not(.is-cancelled):not(.bq-index-pass) .bq-bubble:has(> .bq-loader)::before {
|
|
75
79
|
display: inline-block;
|
|
76
80
|
vertical-align: middle;
|
|
77
81
|
margin-right: 0.4rem;
|
|
78
82
|
}
|
|
79
|
-
.bq-message.is-assistant:not(.is-error):not(.is-cancelled):not(.bq-index-pass)
|
|
83
|
+
.bq-message.is-assistant:not(.is-error):not(.is-cancelled):not(.bq-index-pass) .bq-bubble:has(> .bq-loader) > .bq-loader {
|
|
80
84
|
vertical-align: middle;
|
|
81
85
|
}
|
|
82
86
|
|