mirror of https://github.com/buster-so/buster.git
report file and trigger
This commit is contained in:
parent
2922e1f1bb
commit
f96cbf02e8
|
@ -34,7 +34,7 @@
|
|||
"@buster/web-tools": "workspace:*",
|
||||
"@duckdb/node-api": "1.3.2-alpha.26",
|
||||
"@duckdb/node-bindings": "1.3.2-alpha.26",
|
||||
"@trigger.dev/sdk": "4.0.2",
|
||||
"@trigger.dev/sdk": "4.0.4",
|
||||
"@types/js-yaml": "catalog:",
|
||||
"ai": "catalog:",
|
||||
"braintrust": "catalog:",
|
||||
|
@ -44,6 +44,6 @@
|
|||
"zod": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@trigger.dev/build": "4.0.2"
|
||||
"@trigger.dev/build": "4.0.4"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -68,11 +68,13 @@ export function createDoneToolStart(context: DoneToolContext, doneToolState: Don
|
|||
}
|
||||
}
|
||||
|
||||
// Update the chat with the most recent file - simply use the first file from extractedFiles
|
||||
// No filtering or sorting - just use whatever was selected for response messages
|
||||
if (context.chatId && extractedFiles.length > 0) {
|
||||
const mostRecentFile = extractedFiles[0];
|
||||
|
||||
// Update the chat with the most recent file using the full set that includes reports
|
||||
// Do not rely on the filtered response list since it excludes report files
|
||||
if (context.chatId && allFilesForChatUpdate.length > 0) {
|
||||
const mostRecentFile =
|
||||
allFilesForChatUpdate.find((f) => f.fileType === 'report_file') ||
|
||||
allFilesForChatUpdate[0];
|
||||
if (mostRecentFile) {
|
||||
console.info('[done-tool-start] Updating chat with most recent file', {
|
||||
chatId: context.chatId,
|
||||
fileId: mostRecentFile.id,
|
||||
|
@ -92,6 +94,7 @@ export function createDoneToolStart(context: DoneToolContext, doneToolState: Don
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const doneToolResponseEntry = createDoneToolResponseMessage(doneToolState, options.toolCallId);
|
||||
const doneToolMessage = createDoneToolRawLlmMessageEntry(doneToolState, options.toolCallId);
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
import type { ModelMessage, ToolCallOptions } from 'ai';
|
||||
import { describe, expect, test, vi } from 'vitest';
|
||||
import { CREATE_DASHBOARDS_TOOL_NAME } from '../../visualization-tools/dashboards/create-dashboards-tool/create-dashboards-tool';
|
||||
import { CREATE_METRICS_TOOL_NAME } from '../../visualization-tools/metrics/create-metrics-tool/create-metrics-tool';
|
||||
import { CREATE_REPORTS_TOOL_NAME } from '../../visualization-tools/reports/create-reports-tool/create-reports-tool';
|
||||
import type { DoneToolContext, DoneToolInput, DoneToolState } from './done-tool';
|
||||
import { createDoneToolDelta } from './done-tool-delta';
|
||||
import { createDoneToolFinish } from './done-tool-finish';
|
||||
|
@ -132,6 +135,256 @@ describe('Done Tool Streaming Tests', () => {
|
|||
await expect(startHandler(options)).resolves.not.toThrow();
|
||||
expect(state.toolCallId).toBe('tool-call-789');
|
||||
});
|
||||
|
||||
test('should prefer report_file for mostRecent and not create report file responses', async () => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
const state: DoneToolState = {
|
||||
toolCallId: undefined,
|
||||
args: undefined,
|
||||
finalResponse: undefined,
|
||||
};
|
||||
|
||||
const startHandler = createDoneToolStart(mockContext, state);
|
||||
|
||||
const reportId = 'report-1';
|
||||
const messages: ModelMessage[] = [
|
||||
{
|
||||
role: 'assistant',
|
||||
content: [
|
||||
{
|
||||
type: 'tool-call' as const,
|
||||
toolCallId: 'tc-report',
|
||||
toolName: CREATE_REPORTS_TOOL_NAME,
|
||||
input: { files: [{ content: 'report content' }] },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: 'tool',
|
||||
content: [
|
||||
{
|
||||
type: 'tool-result',
|
||||
toolCallId: 'tc-report',
|
||||
toolName: CREATE_REPORTS_TOOL_NAME,
|
||||
output: {
|
||||
type: 'json',
|
||||
value: JSON.stringify({
|
||||
files: [
|
||||
{
|
||||
id: reportId,
|
||||
name: 'Quarterly Report',
|
||||
version_number: 1,
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
await startHandler({ toolCallId: 'call-1', messages });
|
||||
|
||||
const queries = await import('@buster/database/queries');
|
||||
|
||||
// mostRecent should be set to the report
|
||||
expect(queries.updateChat).toHaveBeenCalled();
|
||||
const updateArgs = ((queries.updateChat as unknown as { mock: { calls: unknown[][] } }).mock
|
||||
.calls?.[0]?.[1] || {}) as Record<string, unknown>;
|
||||
expect(updateArgs).toMatchObject({
|
||||
mostRecentFileId: reportId,
|
||||
mostRecentFileType: 'report_file',
|
||||
mostRecentVersionNumber: 1,
|
||||
});
|
||||
|
||||
// No file response messages should be created for report-only case
|
||||
const fileResponseCallWithFiles = (
|
||||
queries.updateMessageEntries as unknown as { mock: { calls: [Record<string, any>][] } }
|
||||
).mock.calls.find((c) =>
|
||||
Array.isArray((c[0] as { responseMessages?: unknown[] }).responseMessages) &&
|
||||
((c[0] as { responseMessages?: { type?: string }[] }).responseMessages || []).some(
|
||||
(m) => m?.type === 'file'
|
||||
)
|
||||
);
|
||||
expect(fileResponseCallWithFiles).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should create non-report file responses but set mostRecent to report when both exist', async () => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
const state: DoneToolState = {
|
||||
toolCallId: undefined,
|
||||
args: undefined,
|
||||
finalResponse: undefined,
|
||||
};
|
||||
|
||||
const startHandler = createDoneToolStart(mockContext, state);
|
||||
|
||||
const reportId = 'report-2';
|
||||
const metricId = 'metric-1';
|
||||
|
||||
const messages: ModelMessage[] = [
|
||||
{
|
||||
role: 'assistant',
|
||||
content: [
|
||||
{
|
||||
type: 'tool-call' as const,
|
||||
toolCallId: 'tc-report',
|
||||
toolName: CREATE_REPORTS_TOOL_NAME,
|
||||
input: { files: [{ content: 'report content' }] },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: 'tool',
|
||||
content: [
|
||||
{
|
||||
type: 'tool-result',
|
||||
toolCallId: 'tc-report',
|
||||
toolName: CREATE_REPORTS_TOOL_NAME,
|
||||
output: {
|
||||
type: 'json',
|
||||
value: JSON.stringify({
|
||||
files: [
|
||||
{
|
||||
id: reportId,
|
||||
name: 'Key Metrics Report',
|
||||
version_number: 1,
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: 'tool',
|
||||
content: [
|
||||
{
|
||||
type: 'tool-result',
|
||||
toolCallId: 'tc-metric',
|
||||
toolName: CREATE_METRICS_TOOL_NAME,
|
||||
output: {
|
||||
type: 'json',
|
||||
value: JSON.stringify({
|
||||
files: [
|
||||
{
|
||||
id: metricId,
|
||||
name: 'Revenue',
|
||||
version_number: 1,
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
await startHandler({ toolCallId: 'call-2', messages });
|
||||
|
||||
const queries = await import('@buster/database/queries');
|
||||
|
||||
// mostRecent should prefer the report
|
||||
const updateArgs = ((queries.updateChat as unknown as { mock: { calls: unknown[][] } }).mock
|
||||
.calls?.[0]?.[1] || {}) as Record<string, unknown>;
|
||||
expect(updateArgs).toMatchObject({
|
||||
mostRecentFileId: reportId,
|
||||
mostRecentFileType: 'report_file',
|
||||
});
|
||||
|
||||
// Response messages should include the metric file
|
||||
const fileResponseCall = (
|
||||
queries.updateMessageEntries as unknown as { mock: { calls: [Record<string, any>][] } }
|
||||
).mock.calls.find((c) =>
|
||||
Array.isArray((c[0] as { responseMessages?: unknown[] }).responseMessages) &&
|
||||
((c[0] as { responseMessages?: { type?: string }[] }).responseMessages || []).some(
|
||||
(m) => m?.type === 'file'
|
||||
)
|
||||
);
|
||||
|
||||
expect(fileResponseCall).toBeDefined();
|
||||
const responseMessages = (fileResponseCall?.[0] as { responseMessages?: Record<string, any>[] })
|
||||
?.responseMessages as Record<string, any>[];
|
||||
const metricResponse = responseMessages?.find((m) => m.id === metricId);
|
||||
expect(metricResponse).toBeDefined();
|
||||
expect(metricResponse?.file_type).toBe('metric_file');
|
||||
});
|
||||
|
||||
test('should fall back to first non-report file when no report exists', async () => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
const state: DoneToolState = {
|
||||
toolCallId: undefined,
|
||||
args: undefined,
|
||||
finalResponse: undefined,
|
||||
};
|
||||
|
||||
const startHandler = createDoneToolStart(mockContext, state);
|
||||
|
||||
const dashboardId = 'dash-1';
|
||||
const metricId = 'metric-2';
|
||||
|
||||
const messages: ModelMessage[] = [
|
||||
{
|
||||
role: 'tool',
|
||||
content: [
|
||||
{
|
||||
type: 'tool-result',
|
||||
toolCallId: 'tc-dash',
|
||||
toolName: CREATE_DASHBOARDS_TOOL_NAME,
|
||||
output: {
|
||||
type: 'json',
|
||||
value: JSON.stringify({
|
||||
files: [
|
||||
{
|
||||
id: dashboardId,
|
||||
name: 'Sales Dashboard',
|
||||
version_number: 1,
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: 'tool',
|
||||
content: [
|
||||
{
|
||||
type: 'tool-result',
|
||||
toolCallId: 'tc-metric2',
|
||||
toolName: CREATE_METRICS_TOOL_NAME,
|
||||
output: {
|
||||
type: 'json',
|
||||
value: JSON.stringify({
|
||||
files: [
|
||||
{
|
||||
id: metricId,
|
||||
name: 'Margin',
|
||||
version_number: 1,
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
await startHandler({ toolCallId: 'call-3', messages });
|
||||
|
||||
const queries = await import('@buster/database/queries');
|
||||
const updateArgs = ((queries.updateChat as unknown as { mock: { calls: unknown[][] } }).mock
|
||||
.calls[0]?.[1] || {}) as Record<string, unknown>;
|
||||
|
||||
// Should fall back to the first available (dashboard here)
|
||||
expect(updateArgs).toMatchObject({
|
||||
mostRecentFileId: dashboardId,
|
||||
mostRecentFileType: 'dashboard_file',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('createDoneToolDelta', () => {
|
||||
|
|
|
@ -368,8 +368,8 @@ importers:
|
|||
specifier: 1.3.2-alpha.26
|
||||
version: 1.3.2-alpha.26
|
||||
'@trigger.dev/sdk':
|
||||
specifier: 4.0.2
|
||||
version: 4.0.2(ai@5.0.44(zod@3.25.76))(zod@3.25.76)
|
||||
specifier: 4.0.4
|
||||
version: 4.0.4(ai@5.0.44(zod@3.25.76))(zod@3.25.76)
|
||||
'@types/js-yaml':
|
||||
specifier: 'catalog:'
|
||||
version: 4.0.9
|
||||
|
@ -393,8 +393,8 @@ importers:
|
|||
version: 3.25.76
|
||||
devDependencies:
|
||||
'@trigger.dev/build':
|
||||
specifier: 4.0.2
|
||||
version: 4.0.2(typescript@5.9.2)
|
||||
specifier: 4.0.4
|
||||
version: 4.0.4(typescript@5.9.2)
|
||||
|
||||
apps/web:
|
||||
dependencies:
|
||||
|
@ -6245,14 +6245,18 @@ packages:
|
|||
resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==}
|
||||
engines: {node: '>= 10'}
|
||||
|
||||
'@trigger.dev/build@4.0.2':
|
||||
resolution: {integrity: sha512-GOqjGIUXWEEIfWqY2o+xr//4KTHYoRQIML8cCoP/L8x1wPb45qFJWTwNaECgYp9i+9vPMI4/G3Jm/jvWp9xznQ==}
|
||||
'@trigger.dev/build@4.0.4':
|
||||
resolution: {integrity: sha512-W3mP+RBkcYOrNYTTmQ/WdU6LB+2Tk1S6r3OjEWqXEPsXLEEw6BzHTHZBirHYX4lWRBL9jVkL+/H74ycyNfzRjg==}
|
||||
engines: {node: '>=18.20.0'}
|
||||
|
||||
'@trigger.dev/core@4.0.2':
|
||||
resolution: {integrity: sha512-hc/alfT7iVdJNZ5YSMbGR9FirLjURqdZ7tCBX4btKas0GDg6M5onwcQsJ3oom5TDp/Nrt+dHaviNMhFxhKCu3g==}
|
||||
engines: {node: '>=18.20.0'}
|
||||
|
||||
'@trigger.dev/core@4.0.4':
|
||||
resolution: {integrity: sha512-c5myttkNhqaqvLlEz3ttE1qEsULlD6ILBge5FAfEtMv9HVS/pNlgvMKrdFMefaGO/bE4HoxrNGdJsY683Kq32w==}
|
||||
engines: {node: '>=18.20.0'}
|
||||
|
||||
'@trigger.dev/sdk@4.0.2':
|
||||
resolution: {integrity: sha512-ulhWJRSHPXOHz0bMvkhAKThkW63x7lnjAb87LPi6dUps1YwwoOL8Nkr15xLXa73UrldPFT+9Y/GvQ9qpzU478w==}
|
||||
engines: {node: '>=18.20.0'}
|
||||
|
@ -6263,6 +6267,16 @@ packages:
|
|||
ai:
|
||||
optional: true
|
||||
|
||||
'@trigger.dev/sdk@4.0.4':
|
||||
resolution: {integrity: sha512-54krRw9SN1CGm5u17JBzu0hNzRf1u37jKbSFFngPJjUOltOgi/owey5+KNu1rGthabhOBK2VKzvKEd4sn08RCA==}
|
||||
engines: {node: '>=18.20.0'}
|
||||
peerDependencies:
|
||||
ai: ^4.2.0 || ^5.0.0
|
||||
zod: ^3.0.0 || ^4.0.0
|
||||
peerDependenciesMeta:
|
||||
ai:
|
||||
optional: true
|
||||
|
||||
'@turbopuffer/turbopuffer@1.0.0':
|
||||
resolution: {integrity: sha512-flPN00Zy9KgOHzPampOZPkNWNHLrkpcTiTel+VQ7sznOZO79LErGSDwzoyD7ykEj85scY7T8jVsD+qwQ9OPjyQ==}
|
||||
|
||||
|
@ -19263,9 +19277,9 @@ snapshots:
|
|||
|
||||
'@tootallnate/once@2.0.0': {}
|
||||
|
||||
'@trigger.dev/build@4.0.2(typescript@5.9.2)':
|
||||
'@trigger.dev/build@4.0.4(typescript@5.9.2)':
|
||||
dependencies:
|
||||
'@trigger.dev/core': 4.0.2
|
||||
'@trigger.dev/core': 4.0.4
|
||||
pkg-types: 1.3.1
|
||||
tinyglobby: 0.2.14
|
||||
tsconfck: 3.1.3(typescript@5.9.2)
|
||||
|
@ -19314,6 +19328,45 @@ snapshots:
|
|||
- supports-color
|
||||
- utf-8-validate
|
||||
|
||||
'@trigger.dev/core@4.0.4':
|
||||
dependencies:
|
||||
'@bugsnag/cuid': 3.2.1
|
||||
'@electric-sql/client': 1.0.0-beta.1
|
||||
'@google-cloud/precise-date': 4.0.0
|
||||
'@jsonhero/path': 1.0.21
|
||||
'@opentelemetry/api': 1.9.0
|
||||
'@opentelemetry/api-logs': 0.203.0
|
||||
'@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0)
|
||||
'@opentelemetry/exporter-logs-otlp-http': 0.203.0(@opentelemetry/api@1.9.0)
|
||||
'@opentelemetry/exporter-trace-otlp-http': 0.203.0(@opentelemetry/api@1.9.0)
|
||||
'@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0)
|
||||
'@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0)
|
||||
'@opentelemetry/sdk-logs': 0.203.0(@opentelemetry/api@1.9.0)
|
||||
'@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0)
|
||||
'@opentelemetry/sdk-trace-node': 2.0.1(@opentelemetry/api@1.9.0)
|
||||
'@opentelemetry/semantic-conventions': 1.36.0
|
||||
dequal: 2.0.3
|
||||
eventsource: 3.0.7
|
||||
eventsource-parser: 3.0.6
|
||||
execa: 8.0.1
|
||||
humanize-duration: 3.33.0
|
||||
jose: 5.10.0
|
||||
nanoid: 3.3.8
|
||||
prom-client: 15.1.3
|
||||
socket.io: 4.7.4
|
||||
socket.io-client: 4.7.5
|
||||
std-env: 3.9.0
|
||||
superjson: 2.2.2
|
||||
tinyexec: 0.3.2
|
||||
uncrypto: 0.1.3
|
||||
zod: 3.25.76
|
||||
zod-error: 1.5.0
|
||||
zod-validation-error: 1.5.0(zod@3.25.76)
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- supports-color
|
||||
- utf-8-validate
|
||||
|
||||
'@trigger.dev/sdk@4.0.2(ai@5.0.44(zod@3.25.76))(zod@3.25.76)':
|
||||
dependencies:
|
||||
'@opentelemetry/api': 1.9.0
|
||||
|
@ -19336,6 +19389,28 @@ snapshots:
|
|||
- supports-color
|
||||
- utf-8-validate
|
||||
|
||||
'@trigger.dev/sdk@4.0.4(ai@5.0.44(zod@3.25.76))(zod@3.25.76)':
|
||||
dependencies:
|
||||
'@opentelemetry/api': 1.9.0
|
||||
'@opentelemetry/semantic-conventions': 1.36.0
|
||||
'@trigger.dev/core': 4.0.4
|
||||
chalk: 5.6.0
|
||||
cronstrue: 2.59.0
|
||||
debug: 4.4.1
|
||||
evt: 2.5.9
|
||||
slug: 6.1.0
|
||||
ulid: 2.4.0
|
||||
uncrypto: 0.1.3
|
||||
uuid: 9.0.1
|
||||
ws: 8.18.3
|
||||
zod: 3.25.76
|
||||
optionalDependencies:
|
||||
ai: 5.0.44(zod@3.25.76)
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- supports-color
|
||||
- utf-8-validate
|
||||
|
||||
'@turbopuffer/turbopuffer@1.0.0':
|
||||
dependencies:
|
||||
pako: 2.1.0
|
||||
|
|
Loading…
Reference in New Issue