report file and trigger

This commit is contained in:
dal 2025-09-19 08:58:50 -06:00
parent 2922e1f1bb
commit f96cbf02e8
No known key found for this signature in database
GPG Key ID: 16F4B0E1E9F61122
5 changed files with 362 additions and 31 deletions

View File

@ -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"
}
}

View File

@ -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);

View File

@ -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', () => {

View File

@ -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