mirror of https://github.com/buster-so/buster.git
ok need to debug dash and metrics
This commit is contained in:
parent
c476aebd47
commit
3f3b9233f3
|
@ -1,49 +1,40 @@
|
|||
import { randomUUID } from 'node:crypto';
|
||||
import type { ModelMessage } from 'ai';
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import { CREATE_DASHBOARDS_TOOL_NAME } from '../../../visualization-tools/dashboards/create-dashboards-tool/create-dashboards-tool';
|
||||
import { MODIFY_DASHBOARDS_TOOL_NAME } from '../../../visualization-tools/dashboards/modify-dashboards-tool/modify-dashboards-tool';
|
||||
import { CREATE_METRICS_TOOL_NAME } from '../../../visualization-tools/metrics/create-metrics-tool/create-metrics-tool';
|
||||
import { MODIFY_METRICS_TOOL_NAME } from '../../../visualization-tools/metrics/modify-metrics-tool/modify-metrics-tool';
|
||||
import { CREATE_REPORTS_TOOL_NAME } from '../../../visualization-tools/reports/create-reports-tool/create-reports-tool';
|
||||
import { MODIFY_REPORTS_TOOL_NAME } from '../../../visualization-tools/reports/modify-reports-tool/modify-reports-tool';
|
||||
import { extractFilesFromToolCalls } from './done-tool-file-selection';
|
||||
|
||||
describe('done-tool-file-selection', () => {
|
||||
describe('extractFilesFromToolCalls', () => {
|
||||
test('should handle file extraction from tool calls', () => {
|
||||
test('should extract metrics from create metrics tool result', () => {
|
||||
const fileId = randomUUID();
|
||||
const mockMessages: ModelMessage[] = [
|
||||
{
|
||||
role: 'assistant',
|
||||
content: [
|
||||
{
|
||||
type: 'tool-call' as const,
|
||||
toolCallId: 'file-tool-123',
|
||||
toolName: 'create-metrics-file',
|
||||
input: {
|
||||
files: [
|
||||
{
|
||||
name: 'Revenue Analysis',
|
||||
yml_content: 'name: Revenue\nsql: SELECT * FROM sales',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: 'tool',
|
||||
content: [
|
||||
{
|
||||
type: 'tool-result',
|
||||
output: {
|
||||
files: [
|
||||
{
|
||||
id: randomUUID(),
|
||||
name: 'Revenue Analysis',
|
||||
file_type: 'metric',
|
||||
yml_content: 'name: Revenue\\nsql: SELECT * FROM sales',
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
version_number: 1,
|
||||
},
|
||||
],
|
||||
message: 'created',
|
||||
type: 'json',
|
||||
value: JSON.stringify({
|
||||
files: [
|
||||
{
|
||||
id: fileId,
|
||||
name: 'Revenue Analysis',
|
||||
version_number: 1,
|
||||
},
|
||||
],
|
||||
message: 'Metrics created successfully',
|
||||
}),
|
||||
},
|
||||
} as any,
|
||||
toolName: CREATE_METRICS_TOOL_NAME,
|
||||
toolCallId: 'tool-123',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
@ -52,6 +43,7 @@ describe('done-tool-file-selection', () => {
|
|||
|
||||
expect(extractedFiles).toHaveLength(1);
|
||||
expect(extractedFiles[0]).toMatchObject({
|
||||
id: fileId,
|
||||
fileType: 'metric',
|
||||
fileName: 'Revenue Analysis',
|
||||
status: 'completed',
|
||||
|
@ -60,22 +52,31 @@ describe('done-tool-file-selection', () => {
|
|||
});
|
||||
});
|
||||
|
||||
test('should extract dashboard files from tool calls', () => {
|
||||
test('should extract dashboards from create dashboards tool result', () => {
|
||||
const fileId = randomUUID();
|
||||
const mockMessages: ModelMessage[] = [
|
||||
{
|
||||
role: 'tool',
|
||||
content: {
|
||||
files: [
|
||||
{
|
||||
id: randomUUID(),
|
||||
name: 'Sales Dashboard',
|
||||
file_type: 'dashboard',
|
||||
yml_content: 'title: Sales Dashboard',
|
||||
version_number: 1,
|
||||
content: [
|
||||
{
|
||||
type: 'tool-result',
|
||||
output: {
|
||||
type: 'json',
|
||||
value: JSON.stringify({
|
||||
files: [
|
||||
{
|
||||
id: fileId,
|
||||
name: 'Sales Dashboard',
|
||||
version_number: 1,
|
||||
},
|
||||
],
|
||||
message: 'Dashboard created successfully',
|
||||
}),
|
||||
},
|
||||
],
|
||||
message: 'Dashboard created successfully',
|
||||
} as any,
|
||||
toolName: CREATE_DASHBOARDS_TOOL_NAME,
|
||||
toolCallId: 'tool-456',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -83,35 +84,178 @@ describe('done-tool-file-selection', () => {
|
|||
|
||||
expect(extractedFiles).toHaveLength(1);
|
||||
expect(extractedFiles[0]).toMatchObject({
|
||||
id: fileId,
|
||||
fileType: 'dashboard',
|
||||
fileName: 'Sales Dashboard',
|
||||
status: 'completed',
|
||||
operation: 'created',
|
||||
versionNumber: 1,
|
||||
});
|
||||
});
|
||||
|
||||
test('should detect modified operation from message', () => {
|
||||
test('should extract reports from create reports tool result', () => {
|
||||
const fileId = randomUUID();
|
||||
const mockMessages: ModelMessage[] = [
|
||||
{
|
||||
role: 'tool',
|
||||
content: {
|
||||
files: [
|
||||
{
|
||||
id: randomUUID(),
|
||||
name: 'Updated Metric',
|
||||
file_type: 'metric',
|
||||
version_number: 2,
|
||||
content: [
|
||||
{
|
||||
type: 'tool-result',
|
||||
output: {
|
||||
type: 'json',
|
||||
value: JSON.stringify({
|
||||
files: [
|
||||
{
|
||||
id: fileId,
|
||||
name: 'Q4 Analysis Report',
|
||||
version_number: 1,
|
||||
},
|
||||
],
|
||||
message: 'Report created successfully',
|
||||
}),
|
||||
},
|
||||
],
|
||||
message: 'Metric modified successfully',
|
||||
} as any,
|
||||
toolName: CREATE_REPORTS_TOOL_NAME,
|
||||
toolCallId: 'tool-789',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const extractedFiles = extractFilesFromToolCalls(mockMessages);
|
||||
|
||||
expect(extractedFiles).toHaveLength(1);
|
||||
expect(extractedFiles[0]?.operation).toBe('modified');
|
||||
expect(extractedFiles[0]).toMatchObject({
|
||||
id: fileId,
|
||||
fileType: 'report',
|
||||
fileName: 'Q4 Analysis Report',
|
||||
status: 'completed',
|
||||
operation: 'created',
|
||||
versionNumber: 1,
|
||||
});
|
||||
});
|
||||
|
||||
test('should handle modify metrics tool result', () => {
|
||||
const fileId = randomUUID();
|
||||
const mockMessages: ModelMessage[] = [
|
||||
{
|
||||
role: 'tool',
|
||||
content: [
|
||||
{
|
||||
type: 'tool-result',
|
||||
output: {
|
||||
type: 'json',
|
||||
value: JSON.stringify({
|
||||
files: [
|
||||
{
|
||||
id: fileId,
|
||||
name: 'Updated Metric',
|
||||
version_number: 2,
|
||||
},
|
||||
],
|
||||
message: 'Metric modified successfully',
|
||||
}),
|
||||
},
|
||||
toolName: MODIFY_METRICS_TOOL_NAME,
|
||||
toolCallId: 'tool-abc',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const extractedFiles = extractFilesFromToolCalls(mockMessages);
|
||||
|
||||
expect(extractedFiles).toHaveLength(1);
|
||||
expect(extractedFiles[0]).toMatchObject({
|
||||
id: fileId,
|
||||
fileType: 'metric',
|
||||
fileName: 'Updated Metric',
|
||||
status: 'completed',
|
||||
operation: 'modified',
|
||||
versionNumber: 2,
|
||||
});
|
||||
});
|
||||
|
||||
test('should handle modify dashboards tool result', () => {
|
||||
const fileId = randomUUID();
|
||||
const mockMessages: ModelMessage[] = [
|
||||
{
|
||||
role: 'tool',
|
||||
content: [
|
||||
{
|
||||
type: 'tool-result',
|
||||
output: {
|
||||
type: 'json',
|
||||
value: JSON.stringify({
|
||||
files: [
|
||||
{
|
||||
id: fileId,
|
||||
name: 'Updated Dashboard',
|
||||
version_number: 2,
|
||||
},
|
||||
],
|
||||
message: 'Dashboard modified successfully',
|
||||
}),
|
||||
},
|
||||
toolName: MODIFY_DASHBOARDS_TOOL_NAME,
|
||||
toolCallId: 'tool-def',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const extractedFiles = extractFilesFromToolCalls(mockMessages);
|
||||
|
||||
expect(extractedFiles).toHaveLength(1);
|
||||
expect(extractedFiles[0]).toMatchObject({
|
||||
id: fileId,
|
||||
fileType: 'dashboard',
|
||||
fileName: 'Updated Dashboard',
|
||||
status: 'completed',
|
||||
operation: 'modified',
|
||||
versionNumber: 2,
|
||||
});
|
||||
});
|
||||
|
||||
test('should handle modify reports tool result with different structure', () => {
|
||||
const fileId = randomUUID();
|
||||
const mockMessages: ModelMessage[] = [
|
||||
{
|
||||
role: 'tool',
|
||||
content: [
|
||||
{
|
||||
type: 'tool-result',
|
||||
output: {
|
||||
type: 'json',
|
||||
value: JSON.stringify({
|
||||
success: true,
|
||||
message: 'Report modified successfully',
|
||||
file: {
|
||||
id: fileId,
|
||||
name: 'Updated Report',
|
||||
content: 'Report content',
|
||||
version_number: 2,
|
||||
updated_at: new Date().toISOString(),
|
||||
},
|
||||
}),
|
||||
},
|
||||
toolName: MODIFY_REPORTS_TOOL_NAME,
|
||||
toolCallId: 'tool-ghi',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const extractedFiles = extractFilesFromToolCalls(mockMessages);
|
||||
|
||||
expect(extractedFiles).toHaveLength(1);
|
||||
expect(extractedFiles[0]).toMatchObject({
|
||||
id: fileId,
|
||||
fileType: 'report',
|
||||
fileName: 'Updated Report',
|
||||
status: 'completed',
|
||||
operation: 'modified',
|
||||
versionNumber: 2,
|
||||
});
|
||||
});
|
||||
|
||||
test('should deduplicate files by version number', () => {
|
||||
|
@ -119,31 +263,49 @@ describe('done-tool-file-selection', () => {
|
|||
const mockMessages: ModelMessage[] = [
|
||||
{
|
||||
role: 'tool',
|
||||
content: {
|
||||
files: [
|
||||
{
|
||||
id: fileId,
|
||||
name: 'Test Metric',
|
||||
file_type: 'metric',
|
||||
version_number: 1,
|
||||
content: [
|
||||
{
|
||||
type: 'tool-result',
|
||||
output: {
|
||||
type: 'json',
|
||||
value: JSON.stringify({
|
||||
files: [
|
||||
{
|
||||
id: fileId,
|
||||
name: 'Test Metric',
|
||||
version_number: 1,
|
||||
},
|
||||
],
|
||||
message: 'Created',
|
||||
}),
|
||||
},
|
||||
],
|
||||
message: 'created',
|
||||
} as any,
|
||||
toolName: CREATE_METRICS_TOOL_NAME,
|
||||
toolCallId: 'tool-1',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: 'tool',
|
||||
content: {
|
||||
files: [
|
||||
{
|
||||
id: fileId,
|
||||
name: 'Test Metric Updated',
|
||||
file_type: 'metric',
|
||||
version_number: 2,
|
||||
content: [
|
||||
{
|
||||
type: 'tool-result',
|
||||
output: {
|
||||
type: 'json',
|
||||
value: JSON.stringify({
|
||||
files: [
|
||||
{
|
||||
id: fileId,
|
||||
name: 'Test Metric Updated',
|
||||
version_number: 2,
|
||||
},
|
||||
],
|
||||
message: 'Modified',
|
||||
}),
|
||||
},
|
||||
],
|
||||
message: 'modified',
|
||||
} as any,
|
||||
toolName: MODIFY_METRICS_TOOL_NAME,
|
||||
toolCallId: 'tool-2',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -152,6 +314,48 @@ describe('done-tool-file-selection', () => {
|
|||
expect(extractedFiles).toHaveLength(1);
|
||||
expect(extractedFiles[0]?.versionNumber).toBe(2);
|
||||
expect(extractedFiles[0]?.fileName).toBe('Test Metric Updated');
|
||||
expect(extractedFiles[0]?.operation).toBe('modified');
|
||||
});
|
||||
|
||||
test('should handle multiple files in a single tool result', () => {
|
||||
const fileId1 = randomUUID();
|
||||
const fileId2 = randomUUID();
|
||||
const mockMessages: ModelMessage[] = [
|
||||
{
|
||||
role: 'tool',
|
||||
content: [
|
||||
{
|
||||
type: 'tool-result',
|
||||
output: {
|
||||
type: 'json',
|
||||
value: JSON.stringify({
|
||||
files: [
|
||||
{
|
||||
id: fileId1,
|
||||
name: 'Metric 1',
|
||||
version_number: 1,
|
||||
},
|
||||
{
|
||||
id: fileId2,
|
||||
name: 'Metric 2',
|
||||
version_number: 1,
|
||||
},
|
||||
],
|
||||
message: 'Metrics created successfully',
|
||||
}),
|
||||
},
|
||||
toolName: CREATE_METRICS_TOOL_NAME,
|
||||
toolCallId: 'tool-multi',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const extractedFiles = extractFilesFromToolCalls(mockMessages);
|
||||
|
||||
expect(extractedFiles).toHaveLength(2);
|
||||
expect(extractedFiles[0]?.fileName).toBe('Metric 1');
|
||||
expect(extractedFiles[1]?.fileName).toBe('Metric 2');
|
||||
});
|
||||
|
||||
test('should handle empty messages array', () => {
|
||||
|
@ -159,7 +363,7 @@ describe('done-tool-file-selection', () => {
|
|||
expect(extractedFiles).toEqual([]);
|
||||
});
|
||||
|
||||
test('should handle messages without tool results', () => {
|
||||
test('should ignore messages without tool results', () => {
|
||||
const mockMessages: ModelMessage[] = [
|
||||
{
|
||||
role: 'user',
|
||||
|
@ -174,5 +378,57 @@ describe('done-tool-file-selection', () => {
|
|||
const extractedFiles = extractFilesFromToolCalls(mockMessages);
|
||||
expect(extractedFiles).toEqual([]);
|
||||
});
|
||||
|
||||
test('should handle invalid JSON in tool result', () => {
|
||||
const mockMessages: ModelMessage[] = [
|
||||
{
|
||||
role: 'tool',
|
||||
content: [
|
||||
{
|
||||
type: 'tool-result',
|
||||
output: {
|
||||
type: 'json',
|
||||
value: 'invalid json',
|
||||
},
|
||||
toolName: CREATE_METRICS_TOOL_NAME,
|
||||
toolCallId: 'tool-invalid',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const extractedFiles = extractFilesFromToolCalls(mockMessages);
|
||||
expect(extractedFiles).toEqual([]);
|
||||
});
|
||||
|
||||
test('should ignore unknown tool names', () => {
|
||||
const mockMessages: ModelMessage[] = [
|
||||
{
|
||||
role: 'tool',
|
||||
content: [
|
||||
{
|
||||
type: 'tool-result',
|
||||
output: {
|
||||
type: 'json',
|
||||
value: JSON.stringify({
|
||||
files: [
|
||||
{
|
||||
id: randomUUID(),
|
||||
name: 'Some File',
|
||||
version_number: 1,
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
toolName: 'unknownTool',
|
||||
toolCallId: 'tool-unknown',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const extractedFiles = extractFilesFromToolCalls(mockMessages);
|
||||
expect(extractedFiles).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,20 +1,13 @@
|
|||
import type { ChatMessageResponseMessage } from '@buster/server-shared/chats';
|
||||
import type { ModelMessage } from 'ai';
|
||||
import * as yaml from 'yaml';
|
||||
import { CREATE_DASHBOARDS_TOOL_NAME } from '../../../visualization-tools/dashboards/create-dashboards-tool/create-dashboards-tool';
|
||||
import type { CreateDashboardsOutput } from '../../../visualization-tools/dashboards/create-dashboards-tool/create-dashboards-tool';
|
||||
import { MODIFY_DASHBOARDS_TOOL_NAME } from '../../../visualization-tools/dashboards/modify-dashboards-tool/modify-dashboards-tool';
|
||||
import type { ModifyDashboardsOutput } from '../../../visualization-tools/dashboards/modify-dashboards-tool/modify-dashboards-tool';
|
||||
import { CREATE_METRICS_TOOL_NAME } from '../../../visualization-tools/metrics/create-metrics-tool/create-metrics-tool';
|
||||
import type { CreateMetricsOutput } from '../../../visualization-tools/metrics/create-metrics-tool/create-metrics-tool';
|
||||
import { MODIFY_METRICS_TOOL_NAME } from '../../../visualization-tools/metrics/modify-metrics-tool/modify-metrics-tool';
|
||||
import type { ModifyMetricsOutput } from '../../../visualization-tools/metrics/modify-metrics-tool/modify-metrics-tool';
|
||||
import { CREATE_REPORTS_TOOL_NAME } from '../../../visualization-tools/reports/create-reports-tool/create-reports-tool';
|
||||
import type { CreateReportsOutput } from '../../../visualization-tools/reports/create-reports-tool/create-reports-tool';
|
||||
import { MODIFY_REPORTS_TOOL_NAME } from '../../../visualization-tools/reports/modify-reports-tool/modify-reports-tool';
|
||||
import type { ModifyReportsOutput } from '../../../visualization-tools/reports/modify-reports-tool/modify-reports-tool';
|
||||
|
||||
// File tracking type similar to ExtractedFile from file-selection.ts
|
||||
// File tracking type
|
||||
interface ExtractedFile {
|
||||
id: string;
|
||||
fileType: 'metric' | 'dashboard' | 'report';
|
||||
|
@ -22,106 +15,79 @@ interface ExtractedFile {
|
|||
status: 'completed' | 'failed' | 'loading';
|
||||
operation?: 'created' | 'modified' | undefined;
|
||||
versionNumber?: number | undefined;
|
||||
parentDashboardId?: string | undefined; // Track which dashboard contains this metric
|
||||
}
|
||||
|
||||
// Track dashboard-metric relationships
|
||||
interface DashboardMetricRelationship {
|
||||
dashboardId: string;
|
||||
metricIds: string[];
|
||||
}
|
||||
|
||||
// Type for tool call content in assistant messages
|
||||
interface ToolCallContent {
|
||||
type: 'tool-call';
|
||||
toolName: string;
|
||||
input: unknown;
|
||||
toolCallId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract files from tool call responses in the conversation messages
|
||||
* Scans both tool results and assistant messages for file information
|
||||
* Focuses on tool result messages that contain file information
|
||||
*/
|
||||
export function extractFilesFromToolCalls(messages: ModelMessage[]): ExtractedFile[] {
|
||||
const files: ExtractedFile[] = [];
|
||||
const dashboardMetricRelationships: DashboardMetricRelationship[] = [];
|
||||
|
||||
console.info('[done-tool-file-selection] Starting file extraction from messages', {
|
||||
messageCount: messages.length,
|
||||
});
|
||||
|
||||
// First pass: Extract all files and build dashboard-metric relationships
|
||||
// Process each message looking for tool results
|
||||
for (const message of messages) {
|
||||
if (message.role === 'assistant') {
|
||||
// Look for tool calls in assistant messages
|
||||
if (Array.isArray(message.content)) {
|
||||
for (const content of message.content) {
|
||||
if (content && typeof content === 'object') {
|
||||
// Check if this is a tool call - the trace shows structure like:
|
||||
// { type: 'tool-call', toolName: 'createMetrics', input: {...}, toolCallId: '...' }
|
||||
const contentObj = content as ToolCallContent;
|
||||
|
||||
if (contentObj.type === 'tool-call' && contentObj.toolName && contentObj.input) {
|
||||
console.info('[done-tool-file-selection] Found tool call', {
|
||||
toolName: contentObj.toolName,
|
||||
hasInput: !!contentObj.input,
|
||||
toolCallId: contentObj.toolCallId,
|
||||
});
|
||||
|
||||
// Handle different tool types
|
||||
switch (contentObj.toolName) {
|
||||
case CREATE_DASHBOARDS_TOOL_NAME:
|
||||
extractDashboardMetricRelationships(
|
||||
contentObj.input,
|
||||
dashboardMetricRelationships
|
||||
);
|
||||
extractFilesFromToolInput(contentObj.input, 'dashboard', files);
|
||||
break;
|
||||
|
||||
case CREATE_METRICS_TOOL_NAME:
|
||||
extractFilesFromToolInput(contentObj.input, 'metric', files);
|
||||
break;
|
||||
|
||||
case CREATE_REPORTS_TOOL_NAME:
|
||||
extractFilesFromToolInput(contentObj.input, 'report', files);
|
||||
break;
|
||||
|
||||
case MODIFY_DASHBOARDS_TOOL_NAME:
|
||||
extractFilesFromToolInput(contentObj.input, 'dashboard', files, 'modified');
|
||||
break;
|
||||
|
||||
case MODIFY_METRICS_TOOL_NAME:
|
||||
extractFilesFromToolInput(contentObj.input, 'metric', files, 'modified');
|
||||
break;
|
||||
|
||||
case MODIFY_REPORTS_TOOL_NAME:
|
||||
extractFilesFromToolInput(contentObj.input, 'report', files, 'modified');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Also extract files from structured content
|
||||
extractFilesFromStructuredContent(content, files);
|
||||
}
|
||||
} else if (typeof message.content === 'string') {
|
||||
extractFilesFromAssistantMessage(message.content, files);
|
||||
}
|
||||
} else if (message.role === 'tool') {
|
||||
// Tool messages have content that contains the tool result
|
||||
console.info('[done-tool-file-selection] Processing message', {
|
||||
role: message.role,
|
||||
contentType: Array.isArray(message.content) ? 'array' : typeof message.content,
|
||||
});
|
||||
|
||||
if (message.role === 'tool') {
|
||||
// Tool messages contain the actual results
|
||||
const toolContent = message.content;
|
||||
|
||||
// Parse tool results based on content structure
|
||||
if (Array.isArray(toolContent)) {
|
||||
// Handle array of tool results
|
||||
for (const result of toolContent) {
|
||||
if (result && typeof result === 'object' && 'result' in result) {
|
||||
processToolOutput(result.result, files, dashboardMetricRelationships);
|
||||
for (const content of toolContent) {
|
||||
console.info('[done-tool-file-selection] Processing tool content item', {
|
||||
hasType: 'type' in (content || {}),
|
||||
type: (content as any)?.type,
|
||||
hasToolName: 'toolName' in (content || {}),
|
||||
toolName: (content as any)?.toolName,
|
||||
hasOutput: 'output' in (content || {}),
|
||||
contentKeys: content ? Object.keys(content) : [],
|
||||
});
|
||||
|
||||
if (content && typeof content === 'object') {
|
||||
// Check if this is a tool-result type
|
||||
if ('type' in content && content.type === 'tool-result') {
|
||||
// Extract the tool name and output
|
||||
const toolName = (content as any).toolName;
|
||||
const output = (content as any).output;
|
||||
|
||||
console.info('[done-tool-file-selection] Found tool-result', {
|
||||
toolName,
|
||||
hasOutput: !!output,
|
||||
outputType: output?.type,
|
||||
});
|
||||
|
||||
if (output && output.type === 'json' && output.value) {
|
||||
try {
|
||||
// Check if output.value is already an object or needs parsing
|
||||
const parsedOutput = typeof output.value === 'string'
|
||||
? JSON.parse(output.value)
|
||||
: output.value;
|
||||
processToolOutput(toolName, parsedOutput, files);
|
||||
} catch (error) {
|
||||
console.warn('[done-tool-file-selection] Failed to parse JSON output', {
|
||||
toolName,
|
||||
error,
|
||||
valueType: typeof output.value,
|
||||
value: output.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
// Also check if the content itself has files directly (backward compatibility)
|
||||
else if ('files' in content || 'file' in content) {
|
||||
console.info('[done-tool-file-selection] Found direct file content in tool result');
|
||||
processDirectFileContent(content as any, files);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (toolContent && typeof toolContent === 'object') {
|
||||
// Handle single tool result object
|
||||
processToolOutput(toolContent, files, dashboardMetricRelationships);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -131,321 +97,193 @@ export function extractFilesFromToolCalls(messages: ModelMessage[]): ExtractedFi
|
|||
metrics: files.filter((f) => f.fileType === 'metric').length,
|
||||
dashboards: files.filter((f) => f.fileType === 'dashboard').length,
|
||||
reports: files.filter((f) => f.fileType === 'report').length,
|
||||
relationships: dashboardMetricRelationships.length,
|
||||
});
|
||||
|
||||
// Deduplicate files by ID, keeping highest version
|
||||
const deduplicatedFiles = deduplicateFilesByVersion(files);
|
||||
|
||||
console.info('[done-tool-file-selection] After deduplication', {
|
||||
totalFiles: deduplicatedFiles.length,
|
||||
fileIds: deduplicatedFiles.map((f) => ({ id: f.id, type: f.fileType, operation: f.operation })),
|
||||
});
|
||||
|
||||
// Apply selection rules based on file types and relationships
|
||||
const selectedFiles = applyFileSelectionRules(deduplicatedFiles, dashboardMetricRelationships);
|
||||
|
||||
console.info('[done-tool-file-selection] Final selected files', {
|
||||
totalSelected: selectedFiles.length,
|
||||
selectedIds: selectedFiles.map((f) => ({ id: f.id, type: f.fileType, name: f.fileName })),
|
||||
totalSelected: deduplicatedFiles.length,
|
||||
selectedIds: deduplicatedFiles.map((f) => ({ id: f.id, type: f.fileType, name: f.fileName })),
|
||||
});
|
||||
|
||||
return selectedFiles;
|
||||
return deduplicatedFiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract files from assistant message content
|
||||
* Process tool output based on tool name
|
||||
*/
|
||||
function extractFilesFromAssistantMessage(content: string, files: ExtractedFile[]): void {
|
||||
// Assistant messages might contain JSON data or structured information about files
|
||||
// We'll try to parse it if it looks like it contains file data
|
||||
try {
|
||||
// Check if content contains file-like structures
|
||||
if (content.includes('"file_type"') || content.includes('"files"')) {
|
||||
// Try to extract JSON objects from the content
|
||||
const jsonMatches = content.match(/\{[^{}]*\}/g);
|
||||
if (jsonMatches) {
|
||||
for (const match of jsonMatches) {
|
||||
try {
|
||||
const obj = JSON.parse(match);
|
||||
if (obj && typeof obj === 'object') {
|
||||
processFileObject(obj, files);
|
||||
}
|
||||
} catch {
|
||||
// Ignore parse errors for individual matches
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore if we can't parse the content
|
||||
}
|
||||
}
|
||||
function processToolOutput(toolName: string, output: any, files: ExtractedFile[]): void {
|
||||
console.info('[done-tool-file-selection] Processing tool output', {
|
||||
toolName,
|
||||
hasFiles: 'files' in (output || {}),
|
||||
hasFile: 'file' in (output || {}),
|
||||
outputKeys: output ? Object.keys(output) : [],
|
||||
});
|
||||
|
||||
/**
|
||||
* Extract files from structured content (like tool calls or reasoning entries)
|
||||
*/
|
||||
function extractFilesFromStructuredContent(content: unknown, files: ExtractedFile[]): void {
|
||||
if (!content || typeof content !== 'object') return;
|
||||
// Handle different tool types based on their name constants
|
||||
switch (toolName) {
|
||||
case CREATE_METRICS_TOOL_NAME:
|
||||
case MODIFY_METRICS_TOOL_NAME:
|
||||
processMetricsOutput(output, files, toolName === MODIFY_METRICS_TOOL_NAME ? 'modified' : 'created');
|
||||
break;
|
||||
|
||||
const obj = content as Record<string, unknown>;
|
||||
case CREATE_DASHBOARDS_TOOL_NAME:
|
||||
case MODIFY_DASHBOARDS_TOOL_NAME:
|
||||
processDashboardsOutput(output, files, toolName === MODIFY_DASHBOARDS_TOOL_NAME ? 'modified' : 'created');
|
||||
break;
|
||||
|
||||
// Check if this is a files reasoning entry
|
||||
if (obj.type === 'files' && obj.files && typeof obj.files === 'object') {
|
||||
const filesObj = obj.files as Record<string, unknown>;
|
||||
for (const fileId in filesObj) {
|
||||
const file = filesObj[fileId];
|
||||
if (file && typeof file === 'object') {
|
||||
processFileObject(file, files);
|
||||
}
|
||||
}
|
||||
}
|
||||
case CREATE_REPORTS_TOOL_NAME:
|
||||
case MODIFY_REPORTS_TOOL_NAME:
|
||||
processReportsOutput(output, files, toolName === MODIFY_REPORTS_TOOL_NAME ? 'modified' : 'created');
|
||||
break;
|
||||
|
||||
// Check if this contains file information directly
|
||||
if ('file_type' in obj && 'file_name' in obj && 'id' in obj) {
|
||||
processFileObject(obj, files);
|
||||
}
|
||||
|
||||
// Recursively check nested structures
|
||||
for (const key in obj) {
|
||||
const value = obj[key];
|
||||
if (value && typeof value === 'object') {
|
||||
if (Array.isArray(value)) {
|
||||
for (const item of value) {
|
||||
extractFilesFromStructuredContent(item, files);
|
||||
}
|
||||
} else {
|
||||
extractFilesFromStructuredContent(value, files);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a file object and add it to the files array
|
||||
*/
|
||||
function processFileObject(obj: unknown, files: ExtractedFile[]): void {
|
||||
if (!obj || typeof obj !== 'object') return;
|
||||
|
||||
const file = obj as Record<string, unknown>;
|
||||
|
||||
// Check if this looks like a file object
|
||||
if (
|
||||
file.id &&
|
||||
(file.file_type || file.fileType) &&
|
||||
(file.file_name || file.fileName || file.name)
|
||||
) {
|
||||
const fileType = (file.file_type || file.fileType) as string;
|
||||
const fileName = (file.file_name || file.fileName || file.name) as string;
|
||||
const id = file.id as string;
|
||||
const versionNumber = (file.version_number || file.versionNumber || 1) as number;
|
||||
|
||||
// Only add valid file types
|
||||
if (fileType === 'metric' || fileType === 'dashboard' || fileType === 'report') {
|
||||
files.push({
|
||||
id,
|
||||
fileType: fileType as 'metric' | 'dashboard' | 'report',
|
||||
fileName,
|
||||
status: 'completed',
|
||||
operation: 'created',
|
||||
versionNumber,
|
||||
default:
|
||||
console.info('[done-tool-file-selection] Unknown tool name, skipping', {
|
||||
toolName,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a tool output and extract file information
|
||||
* Process metrics output
|
||||
*/
|
||||
function processToolOutput(
|
||||
output: unknown,
|
||||
files: ExtractedFile[],
|
||||
_dashboardMetricRelationships: DashboardMetricRelationship[]
|
||||
): void {
|
||||
// Check if this is a metrics tool output
|
||||
if (isMetricsToolOutput(output)) {
|
||||
const operation = detectOperation(output.message);
|
||||
|
||||
// Extract successfully created/modified metric files
|
||||
if (output.files && Array.isArray(output.files)) {
|
||||
for (const file of output.files) {
|
||||
files.push({
|
||||
id: file.id,
|
||||
fileType: 'metric',
|
||||
fileName: file.name,
|
||||
status: 'completed',
|
||||
operation,
|
||||
versionNumber: file.version_number,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if this is a dashboards tool output
|
||||
if (isDashboardsToolOutput(output)) {
|
||||
const operation = detectOperation(output.message);
|
||||
|
||||
// Extract successfully created/modified dashboard files
|
||||
if (output.files && Array.isArray(output.files)) {
|
||||
for (const file of output.files) {
|
||||
files.push({
|
||||
id: file.id,
|
||||
fileType: 'dashboard',
|
||||
fileName: file.name,
|
||||
status: 'completed',
|
||||
operation,
|
||||
versionNumber: file.version_number,
|
||||
});
|
||||
|
||||
// Extract metric IDs from dashboard content if available
|
||||
// This requires looking at the original tool call arguments
|
||||
// We'll track relationships when we see createDashboards tool calls
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if this is a create reports tool output
|
||||
if (isCreateReportsToolOutput(output)) {
|
||||
const operation = detectOperation(output.message);
|
||||
|
||||
// Extract successfully created report files
|
||||
if (output.files && Array.isArray(output.files)) {
|
||||
for (const file of output.files) {
|
||||
files.push({
|
||||
id: file.id,
|
||||
fileType: 'report',
|
||||
fileName: file.name,
|
||||
status: 'completed',
|
||||
operation,
|
||||
versionNumber: file.version_number,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if this is a modify reports tool output
|
||||
if (isModifyReportsToolOutput(output)) {
|
||||
// For modify reports tool, extract from the file object
|
||||
if (output.file && typeof output.file === 'object') {
|
||||
files.push({
|
||||
id: output.file.id,
|
||||
fileType: 'report',
|
||||
fileName: output.file.name,
|
||||
status: 'completed',
|
||||
operation: 'modified',
|
||||
versionNumber: output.file.version_number,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if output is from create/modify metrics tool
|
||||
*/
|
||||
function isMetricsToolOutput(output: unknown): output is CreateMetricsOutput | ModifyMetricsOutput {
|
||||
if (!output || typeof output !== 'object') return false;
|
||||
|
||||
const obj = output as Record<string, unknown>;
|
||||
|
||||
// Check for required properties
|
||||
if (!('files' in obj) || !('message' in obj)) return false;
|
||||
if (!Array.isArray(obj.files)) return false;
|
||||
|
||||
// Check if all files are metrics
|
||||
return obj.files.every((file: unknown) => {
|
||||
if (!file || typeof file !== 'object') return false;
|
||||
const fileObj = file as Record<string, unknown>;
|
||||
return fileObj.file_type === 'metric';
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if output is from create/modify dashboards tool
|
||||
*/
|
||||
function isDashboardsToolOutput(
|
||||
output: unknown
|
||||
): output is CreateDashboardsOutput | ModifyDashboardsOutput {
|
||||
if (!output || typeof output !== 'object') return false;
|
||||
|
||||
const obj = output as Record<string, unknown>;
|
||||
|
||||
// Check for required properties
|
||||
if (!('files' in obj) || !('message' in obj)) return false;
|
||||
if (!Array.isArray(obj.files)) return false;
|
||||
|
||||
// Check if all files are dashboards
|
||||
return obj.files.every((file: unknown) => {
|
||||
if (!file || typeof file !== 'object') return false;
|
||||
const fileObj = file as Record<string, unknown>;
|
||||
return fileObj.file_type === 'dashboard';
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if output is from create reports tool
|
||||
*/
|
||||
function isCreateReportsToolOutput(output: unknown): output is CreateReportsOutput {
|
||||
if (!output || typeof output !== 'object') return false;
|
||||
|
||||
const obj = output as Record<string, unknown>;
|
||||
|
||||
// Check for create reports output structure
|
||||
if ('files' in obj && 'message' in obj && 'failed_files' in obj) {
|
||||
if (!Array.isArray(obj.files)) return false;
|
||||
|
||||
// Check if files have report-specific properties (id, name, version_number)
|
||||
return obj.files.every((file: unknown) => {
|
||||
if (!file || typeof file !== 'object') return false;
|
||||
const fileObj = file as Record<string, unknown>;
|
||||
return 'id' in fileObj && 'name' in fileObj && 'version_number' in fileObj;
|
||||
function processMetricsOutput(output: any, files: ExtractedFile[], operation: 'created' | 'modified'): void {
|
||||
if (output.files && Array.isArray(output.files)) {
|
||||
console.info('[done-tool-file-selection] Processing metrics files', {
|
||||
count: output.files.length,
|
||||
operation,
|
||||
});
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if output is from modify reports tool
|
||||
*/
|
||||
function isModifyReportsToolOutput(output: unknown): output is ModifyReportsOutput {
|
||||
if (!output || typeof output !== 'object') return false;
|
||||
|
||||
const obj = output as Record<string, unknown>;
|
||||
|
||||
// Check for modify reports output structure (has success, message, and file properties)
|
||||
if ('success' in obj && 'message' in obj && 'file' in obj) {
|
||||
const file = obj.file;
|
||||
if (file && typeof file === 'object') {
|
||||
const fileObj = file as Record<string, unknown>;
|
||||
// Check for required file properties
|
||||
return (
|
||||
'id' in fileObj &&
|
||||
'name' in fileObj &&
|
||||
'version_number' in fileObj &&
|
||||
'content' in fileObj &&
|
||||
'updated_at' in fileObj
|
||||
);
|
||||
|
||||
for (const file of output.files) {
|
||||
// Handle both possible structures
|
||||
const fileName = file.file_name || file.name;
|
||||
const fileType = file.file_type || 'metric';
|
||||
|
||||
console.info('[done-tool-file-selection] Processing metric file', {
|
||||
id: file.id,
|
||||
fileName,
|
||||
fileType,
|
||||
hasFileName: !!fileName,
|
||||
fileKeys: Object.keys(file),
|
||||
});
|
||||
|
||||
if (file.id && fileName) {
|
||||
files.push({
|
||||
id: file.id,
|
||||
fileType: fileType as 'metric' | 'dashboard' | 'report',
|
||||
fileName: fileName,
|
||||
status: file.status || 'completed',
|
||||
operation,
|
||||
versionNumber: file.version_number || 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect if files were created or modified based on the message
|
||||
* Process dashboards output
|
||||
*/
|
||||
function detectOperation(message: string): 'created' | 'modified' | undefined {
|
||||
if (!message) return undefined;
|
||||
|
||||
const lowerMessage = message.toLowerCase();
|
||||
if (lowerMessage.includes('modified') || lowerMessage.includes('updated')) {
|
||||
return 'modified';
|
||||
}
|
||||
if (lowerMessage.includes('created') || lowerMessage.includes('creating')) {
|
||||
return 'created';
|
||||
function processDashboardsOutput(output: any, files: ExtractedFile[], operation: 'created' | 'modified'): void {
|
||||
if (output.files && Array.isArray(output.files)) {
|
||||
console.info('[done-tool-file-selection] Processing dashboard files', {
|
||||
count: output.files.length,
|
||||
operation,
|
||||
});
|
||||
|
||||
for (const file of output.files) {
|
||||
// Handle both possible structures
|
||||
const fileName = file.file_name || file.name;
|
||||
const fileType = file.file_type || 'dashboard';
|
||||
|
||||
if (file.id && fileName) {
|
||||
files.push({
|
||||
id: file.id,
|
||||
fileType: fileType as 'metric' | 'dashboard' | 'report',
|
||||
fileName: fileName,
|
||||
status: file.status || 'completed',
|
||||
operation,
|
||||
versionNumber: file.version_number || 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 'created'; // Default to created if not specified
|
||||
/**
|
||||
* Process reports output
|
||||
*/
|
||||
function processReportsOutput(output: any, files: ExtractedFile[], operation: 'created' | 'modified'): void {
|
||||
// Reports can have either files array or single file property
|
||||
if (output.files && Array.isArray(output.files)) {
|
||||
console.info('[done-tool-file-selection] Processing report files array', {
|
||||
count: output.files.length,
|
||||
operation,
|
||||
});
|
||||
|
||||
for (const file of output.files) {
|
||||
// Handle both possible structures
|
||||
const fileName = file.file_name || file.name;
|
||||
const fileType = file.file_type || 'report';
|
||||
|
||||
if (file.id && fileName) {
|
||||
files.push({
|
||||
id: file.id,
|
||||
fileType: fileType as 'metric' | 'dashboard' | 'report',
|
||||
fileName: fileName,
|
||||
status: file.status || 'completed',
|
||||
operation,
|
||||
versionNumber: file.version_number || 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (output.file && typeof output.file === 'object') {
|
||||
// Handle single file for modify reports
|
||||
const file = output.file;
|
||||
const fileName = file.file_name || file.name;
|
||||
const fileType = file.file_type || 'report';
|
||||
|
||||
console.info('[done-tool-file-selection] Processing single report file', {
|
||||
id: file.id,
|
||||
fileName,
|
||||
operation,
|
||||
});
|
||||
|
||||
if (file.id && fileName) {
|
||||
files.push({
|
||||
id: file.id,
|
||||
fileType: fileType as 'metric' | 'dashboard' | 'report',
|
||||
fileName: fileName,
|
||||
status: file.status || 'completed',
|
||||
operation,
|
||||
versionNumber: file.version_number || 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process direct file content (backward compatibility)
|
||||
*/
|
||||
function processDirectFileContent(content: any, files: ExtractedFile[]): void {
|
||||
if (content.files && Array.isArray(content.files)) {
|
||||
for (const file of content.files) {
|
||||
const fileName = file.file_name || file.name;
|
||||
const fileType = file.file_type || 'metric';
|
||||
|
||||
if (file.id && fileName) {
|
||||
files.push({
|
||||
id: file.id,
|
||||
fileType: fileType as 'metric' | 'dashboard' | 'report',
|
||||
fileName: fileName,
|
||||
status: file.status || 'completed',
|
||||
operation: 'created',
|
||||
versionNumber: file.version_number || 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -467,277 +305,6 @@ function deduplicateFilesByVersion(files: ExtractedFile[]): ExtractedFile[] {
|
|||
return Array.from(deduplicated.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract dashboard-metric relationships from createDashboards tool arguments
|
||||
*/
|
||||
function extractDashboardMetricRelationships(
|
||||
args: unknown,
|
||||
relationships: DashboardMetricRelationship[]
|
||||
): void {
|
||||
console.info('[done-tool-file-selection] Extracting dashboard-metric relationships from args');
|
||||
try {
|
||||
const argsObj = args as Record<string, unknown>;
|
||||
if (argsObj.files && Array.isArray(argsObj.files)) {
|
||||
console.info('[done-tool-file-selection] Found files in args', {
|
||||
fileCount: argsObj.files.length,
|
||||
});
|
||||
for (const file of argsObj.files as Record<string, unknown>[]) {
|
||||
if (file.yml_content) {
|
||||
// Parse the YAML content to extract metric IDs
|
||||
try {
|
||||
const dashboardYaml = yaml.parse(file.yml_content as string);
|
||||
if (dashboardYaml?.rows && Array.isArray(dashboardYaml.rows)) {
|
||||
const metricIds: string[] = [];
|
||||
for (const row of dashboardYaml.rows) {
|
||||
if (row.items && Array.isArray(row.items)) {
|
||||
for (const item of row.items) {
|
||||
if (item.id) {
|
||||
metricIds.push(item.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We don't have the dashboard ID yet (it's generated during execution),
|
||||
// but we can use the dashboard name as a temporary identifier
|
||||
// and update it later when we see the result
|
||||
if (metricIds.length > 0 && file.name) {
|
||||
console.info('[done-tool-file-selection] Found dashboard with metrics', {
|
||||
dashboardName: file.name,
|
||||
metricIds,
|
||||
});
|
||||
relationships.push({
|
||||
dashboardId: file.name as string, // Use name as temporary ID
|
||||
metricIds,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (yamlError) {
|
||||
// Ignore YAML parsing errors
|
||||
console.warn('Failed to parse dashboard YAML for relationships:', yamlError);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore errors in relationship extraction
|
||||
console.warn('Failed to extract dashboard-metric relationships:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply file selection rules based on file types and relationships
|
||||
* Rules:
|
||||
* 1. If only metrics are created → show all metrics
|
||||
* 2. If only dashboards are created → show only dashboards
|
||||
* 3. If a metric that belongs to a dashboard is modified → show the parent dashboard
|
||||
*/
|
||||
function applyFileSelectionRules(
|
||||
files: ExtractedFile[],
|
||||
relationships: DashboardMetricRelationship[]
|
||||
): ExtractedFile[] {
|
||||
console.info('[done-tool-file-selection] Applying selection rules', {
|
||||
totalFiles: files.length,
|
||||
relationships: relationships.length,
|
||||
});
|
||||
|
||||
// Separate files by type
|
||||
const metrics = files.filter((f) => f.fileType === 'metric');
|
||||
const dashboards = files.filter((f) => f.fileType === 'dashboard');
|
||||
const reports = files.filter((f) => f.fileType === 'report');
|
||||
|
||||
console.info('[done-tool-file-selection] Files by type', {
|
||||
metrics: metrics.map((m) => ({ id: m.id, name: m.fileName, operation: m.operation })),
|
||||
dashboards: dashboards.map((d) => ({ id: d.id, name: d.fileName, operation: d.operation })),
|
||||
reports: reports.length,
|
||||
});
|
||||
|
||||
// Build a map of metric ID to dashboard IDs (a metric can belong to multiple dashboards)
|
||||
const metricToDashboards = new Map<string, Set<string>>();
|
||||
|
||||
// First, map dashboard names to their IDs from the files
|
||||
const dashboardNameToId = new Map<string, string>();
|
||||
for (const dashboard of dashboards) {
|
||||
dashboardNameToId.set(dashboard.fileName, dashboard.id);
|
||||
}
|
||||
|
||||
// Then build the metric-to-dashboard mapping
|
||||
for (const relationship of relationships) {
|
||||
// Try to find the actual dashboard ID from the name
|
||||
const dashboardId = dashboardNameToId.get(relationship.dashboardId) || relationship.dashboardId;
|
||||
|
||||
console.info('[done-tool-file-selection] Processing relationship', {
|
||||
dashboardIdOrName: relationship.dashboardId,
|
||||
resolvedDashboardId: dashboardId,
|
||||
metricIds: relationship.metricIds,
|
||||
});
|
||||
|
||||
for (const metricId of relationship.metricIds) {
|
||||
if (!metricToDashboards.has(metricId)) {
|
||||
metricToDashboards.set(metricId, new Set());
|
||||
}
|
||||
metricToDashboards.get(metricId)?.add(dashboardId);
|
||||
}
|
||||
}
|
||||
|
||||
console.info('[done-tool-file-selection] Metric to dashboard mapping', {
|
||||
mappings: Array.from(metricToDashboards.entries()).map(([metricId, dashboardIds]) => ({
|
||||
metricId,
|
||||
belongsToDashboards: Array.from(dashboardIds),
|
||||
})),
|
||||
});
|
||||
|
||||
// Note: Dashboard-metric relationships are handled by the relationship extraction above
|
||||
|
||||
// Apply selection rules
|
||||
const selectedFiles: ExtractedFile[] = [];
|
||||
|
||||
// Always include reports
|
||||
selectedFiles.push(...reports);
|
||||
|
||||
// Check if we have both metrics and dashboards
|
||||
const hasMetrics = metrics.length > 0;
|
||||
const hasDashboards = dashboards.length > 0;
|
||||
|
||||
if (hasMetrics && !hasDashboards) {
|
||||
// Rule 1: Only metrics exist → show all metrics
|
||||
console.info('[done-tool-file-selection] Rule 1: Only metrics exist, showing all metrics');
|
||||
selectedFiles.push(...metrics);
|
||||
} else if (hasDashboards && !hasMetrics) {
|
||||
// Rule 2: Only dashboards exist → show dashboards
|
||||
console.info('[done-tool-file-selection] Rule 2: Only dashboards exist, showing dashboards');
|
||||
selectedFiles.push(...dashboards);
|
||||
} else if (hasMetrics && hasDashboards) {
|
||||
console.info('[done-tool-file-selection] Both metrics and dashboards exist');
|
||||
// We have both metrics and dashboards
|
||||
// Check if any modified metrics belong to dashboards
|
||||
const modifiedMetrics = metrics.filter((m) => m.operation === 'modified');
|
||||
const parentDashboardIds = new Set<string>();
|
||||
|
||||
console.info('[done-tool-file-selection] Checking for modified metrics', {
|
||||
modifiedMetrics: modifiedMetrics.map((m) => ({ id: m.id, name: m.fileName })),
|
||||
});
|
||||
|
||||
for (const metric of modifiedMetrics) {
|
||||
const dashboardIds = metricToDashboards.get(metric.id);
|
||||
if (dashboardIds) {
|
||||
console.info('[done-tool-file-selection] Modified metric belongs to dashboards', {
|
||||
metricId: metric.id,
|
||||
dashboardIds: Array.from(dashboardIds),
|
||||
});
|
||||
dashboardIds.forEach((id) => parentDashboardIds.add(id));
|
||||
}
|
||||
}
|
||||
|
||||
// If modified metrics belong to dashboards, show those dashboards
|
||||
if (parentDashboardIds.size > 0) {
|
||||
console.info(
|
||||
'[done-tool-file-selection] Rule 3: Modified metrics belong to dashboards, showing parent dashboards',
|
||||
{
|
||||
parentDashboardIds: Array.from(parentDashboardIds),
|
||||
}
|
||||
);
|
||||
const parentDashboards = dashboards.filter((d) => parentDashboardIds.has(d.id));
|
||||
selectedFiles.push(...parentDashboards);
|
||||
|
||||
// Also include any standalone metrics (not in dashboards)
|
||||
const standaloneMetrics = metrics.filter((m) => !metricToDashboards.has(m.id));
|
||||
console.info('[done-tool-file-selection] Including standalone metrics', {
|
||||
standaloneMetrics: standaloneMetrics.map((m) => ({ id: m.id, name: m.fileName })),
|
||||
});
|
||||
selectedFiles.push(...standaloneMetrics);
|
||||
} else {
|
||||
// Default: show dashboards if they exist, otherwise show metrics
|
||||
if (dashboards.length > 0) {
|
||||
console.info('[done-tool-file-selection] Default: Showing dashboards');
|
||||
selectedFiles.push(...dashboards);
|
||||
} else {
|
||||
console.info('[done-tool-file-selection] Default: No dashboards, showing metrics');
|
||||
selectedFiles.push(...metrics);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove duplicates
|
||||
const uniqueFiles = new Map<string, ExtractedFile>();
|
||||
for (const file of selectedFiles) {
|
||||
if (
|
||||
!uniqueFiles.has(file.id) ||
|
||||
(file.versionNumber || 1) > (uniqueFiles.get(file.id)?.versionNumber || 1)
|
||||
) {
|
||||
uniqueFiles.set(file.id, file);
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(uniqueFiles.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract files from tool input (for tool calls in assistant messages)
|
||||
*/
|
||||
function extractFilesFromToolInput(
|
||||
input: unknown,
|
||||
fileType: 'metric' | 'dashboard' | 'report',
|
||||
files: ExtractedFile[],
|
||||
operation: 'created' | 'modified' = 'created'
|
||||
): void {
|
||||
if (!input || typeof input !== 'object') return;
|
||||
|
||||
const inputObj = input as Record<string, unknown>;
|
||||
|
||||
// Handle files array in input
|
||||
if (inputObj.files && Array.isArray(inputObj.files)) {
|
||||
for (const file of inputObj.files as Record<string, unknown>[]) {
|
||||
if (file.name) {
|
||||
const fileName = file.name as string;
|
||||
// Generate a temporary ID based on file type and name
|
||||
const tempId = `${fileType}_${fileName.replace(/[^a-zA-Z0-9]/g, '_')}_temp`;
|
||||
|
||||
files.push({
|
||||
id: tempId,
|
||||
fileType,
|
||||
fileName,
|
||||
status: 'loading' as const,
|
||||
operation,
|
||||
versionNumber: 1,
|
||||
});
|
||||
|
||||
console.info('[done-tool-file-selection] Extracted file from tool input', {
|
||||
tempId,
|
||||
fileType,
|
||||
fileName,
|
||||
operation,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle single file in input (for modify tools)
|
||||
if (inputObj.file && typeof inputObj.file === 'object') {
|
||||
const file = inputObj.file as Record<string, unknown>;
|
||||
if (file.name || file.id) {
|
||||
const fileName = (file.name || file.id) as string;
|
||||
const tempId = `${fileType}_${fileName.replace(/[^a-zA-Z0-9]/g, '_')}_temp`;
|
||||
|
||||
files.push({
|
||||
id: tempId,
|
||||
fileType,
|
||||
fileName,
|
||||
status: 'loading' as const,
|
||||
operation,
|
||||
versionNumber: 1,
|
||||
});
|
||||
|
||||
console.info('[done-tool-file-selection] Extracted single file from tool input', {
|
||||
tempId,
|
||||
fileType,
|
||||
fileName,
|
||||
operation,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create file response messages for selected files
|
||||
*/
|
||||
|
@ -769,4 +336,4 @@ export function createFileResponseMessages(files: ExtractedFile[]): ChatMessageR
|
|||
],
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,410 +0,0 @@
|
|||
import type { ChatMessageReasoningMessage } from '@buster/server-shared/chats';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
type ExtractedFile,
|
||||
createFileResponseMessages,
|
||||
extractFilesFromReasoning,
|
||||
selectFilesForResponse,
|
||||
} from './file-selection';
|
||||
|
||||
describe('file-selection', () => {
|
||||
describe('extractFilesFromReasoning', () => {
|
||||
it('should extract created metrics with version numbers', () => {
|
||||
const reasoningHistory: ChatMessageReasoningMessage[] = [
|
||||
{
|
||||
id: 'test-1',
|
||||
type: 'files',
|
||||
title: 'Created 2 metrics',
|
||||
status: 'completed',
|
||||
file_ids: ['metric-1', 'metric-2'],
|
||||
files: {
|
||||
'metric-1': {
|
||||
id: 'metric-1',
|
||||
file_type: 'metric',
|
||||
file_name: 'Revenue Metric',
|
||||
version_number: 1,
|
||||
status: 'completed',
|
||||
file: {
|
||||
text: '{"name": "Revenue Metric", "sql": "SELECT revenue FROM sales"}',
|
||||
},
|
||||
},
|
||||
'metric-2': {
|
||||
id: 'metric-2',
|
||||
file_type: 'metric',
|
||||
file_name: 'Growth Metric',
|
||||
version_number: 1,
|
||||
status: 'completed',
|
||||
file: {
|
||||
text: '{"name": "Growth Metric", "sql": "SELECT growth FROM analytics"}',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const files = extractFilesFromReasoning(reasoningHistory);
|
||||
|
||||
expect(files).toHaveLength(2);
|
||||
expect(files[0]).toMatchObject({
|
||||
id: 'metric-1',
|
||||
fileType: 'metric',
|
||||
fileName: 'Revenue Metric',
|
||||
status: 'completed',
|
||||
operation: 'created',
|
||||
versionNumber: 1,
|
||||
});
|
||||
expect(files[1]).toMatchObject({
|
||||
id: 'metric-2',
|
||||
fileType: 'metric',
|
||||
fileName: 'Growth Metric',
|
||||
status: 'completed',
|
||||
operation: 'created',
|
||||
versionNumber: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('should extract modified dashboards with version numbers', () => {
|
||||
const reasoningHistory: ChatMessageReasoningMessage[] = [
|
||||
{
|
||||
id: 'test-1',
|
||||
type: 'files',
|
||||
title: 'Modified 1 dashboard',
|
||||
status: 'completed',
|
||||
file_ids: ['dashboard-1'],
|
||||
files: {
|
||||
'dashboard-1': {
|
||||
id: 'dashboard-1',
|
||||
file_type: 'dashboard',
|
||||
file_name: 'Sales Dashboard',
|
||||
version_number: 3,
|
||||
status: 'completed',
|
||||
file: {
|
||||
text: '{"name": "Sales Dashboard", "rows": [{"id": 1, "items": [{"id": "metric-1"}], "columnSizes": [12]}]}',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const files = extractFilesFromReasoning(reasoningHistory);
|
||||
|
||||
expect(files).toHaveLength(1);
|
||||
expect(files[0]).toMatchObject({
|
||||
id: 'dashboard-1',
|
||||
fileType: 'dashboard',
|
||||
fileName: 'Sales Dashboard',
|
||||
status: 'completed',
|
||||
operation: 'modified',
|
||||
versionNumber: 3,
|
||||
});
|
||||
});
|
||||
|
||||
it('should build metric-to-dashboard relationships', () => {
|
||||
const reasoningHistory: ChatMessageReasoningMessage[] = [
|
||||
{
|
||||
id: 'test-1',
|
||||
type: 'files',
|
||||
title: 'Created 1 metric',
|
||||
status: 'completed',
|
||||
file_ids: ['metric-1'],
|
||||
files: {
|
||||
'metric-1': {
|
||||
id: 'metric-1',
|
||||
file_type: 'metric',
|
||||
file_name: 'Revenue Metric',
|
||||
version_number: 1,
|
||||
status: 'completed',
|
||||
file: {
|
||||
text: '{"name": "Revenue Metric"}',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'test-2',
|
||||
type: 'files',
|
||||
title: 'Created 1 dashboard',
|
||||
status: 'completed',
|
||||
file_ids: ['dashboard-1'],
|
||||
files: {
|
||||
'dashboard-1': {
|
||||
id: 'dashboard-1',
|
||||
file_type: 'dashboard',
|
||||
file_name: 'Sales Dashboard',
|
||||
version_number: 1,
|
||||
status: 'completed',
|
||||
file: {
|
||||
text: '{"name": "Sales Dashboard", "rows": [{"id": 1, "items": [{"id": "metric-1"}], "columnSizes": [12]}]}',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const files = extractFilesFromReasoning(reasoningHistory);
|
||||
const metric = files.find((f) => f.id === 'metric-1');
|
||||
|
||||
expect(metric?.containedInDashboards).toEqual(['dashboard-1']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('selectFilesForResponse', () => {
|
||||
it('should return dashboard when metric inside it was modified', () => {
|
||||
const files: ExtractedFile[] = [
|
||||
{
|
||||
id: 'metric-1',
|
||||
fileType: 'metric',
|
||||
fileName: 'Revenue Metric',
|
||||
status: 'completed',
|
||||
operation: 'modified',
|
||||
versionNumber: 2,
|
||||
containedInDashboards: ['dashboard-1'],
|
||||
},
|
||||
{
|
||||
id: 'dashboard-1',
|
||||
fileType: 'dashboard',
|
||||
fileName: 'Sales Dashboard',
|
||||
status: 'completed',
|
||||
operation: 'created',
|
||||
versionNumber: 1,
|
||||
ymlContent:
|
||||
'{"name": "Sales Dashboard", "rows": [{"id": 1, "items": [{"id": "metric-1"}], "columnSizes": [12]}]}',
|
||||
},
|
||||
];
|
||||
|
||||
const selected = selectFilesForResponse(files);
|
||||
|
||||
expect(selected).toHaveLength(1);
|
||||
expect(selected[0]?.id).toBe('dashboard-1');
|
||||
});
|
||||
|
||||
it('should return standalone modified metrics', () => {
|
||||
const files: ExtractedFile[] = [
|
||||
{
|
||||
id: 'metric-1',
|
||||
fileType: 'metric',
|
||||
fileName: 'Revenue Metric',
|
||||
status: 'completed',
|
||||
operation: 'modified',
|
||||
versionNumber: 2,
|
||||
containedInDashboards: [],
|
||||
},
|
||||
{
|
||||
id: 'metric-2',
|
||||
fileType: 'metric',
|
||||
fileName: 'Growth Metric',
|
||||
status: 'completed',
|
||||
operation: 'modified',
|
||||
versionNumber: 3,
|
||||
containedInDashboards: [],
|
||||
},
|
||||
];
|
||||
|
||||
const selected = selectFilesForResponse(files);
|
||||
|
||||
expect(selected).toHaveLength(2);
|
||||
expect(selected.map((f) => f.id)).toEqual(['metric-1', 'metric-2']);
|
||||
});
|
||||
|
||||
it('should return dashboard and standalone metric when both exist', () => {
|
||||
const files: ExtractedFile[] = [
|
||||
{
|
||||
id: 'metric-1',
|
||||
fileType: 'metric',
|
||||
fileName: 'Revenue Metric',
|
||||
status: 'completed',
|
||||
operation: 'modified',
|
||||
versionNumber: 2,
|
||||
containedInDashboards: ['dashboard-1'],
|
||||
},
|
||||
{
|
||||
id: 'metric-2',
|
||||
fileType: 'metric',
|
||||
fileName: 'Standalone Metric',
|
||||
status: 'completed',
|
||||
operation: 'modified',
|
||||
versionNumber: 1,
|
||||
containedInDashboards: [],
|
||||
},
|
||||
{
|
||||
id: 'dashboard-1',
|
||||
fileType: 'dashboard',
|
||||
fileName: 'Sales Dashboard',
|
||||
status: 'completed',
|
||||
operation: 'created',
|
||||
versionNumber: 1,
|
||||
ymlContent:
|
||||
'{"name": "Sales Dashboard", "rows": [{"id": 1, "items": [{"id": "metric-1"}], "columnSizes": [12]}]}',
|
||||
},
|
||||
];
|
||||
|
||||
const selected = selectFilesForResponse(files);
|
||||
|
||||
expect(selected).toHaveLength(2);
|
||||
expect(selected.map((f) => f.id).sort()).toEqual(['dashboard-1', 'metric-2'].sort());
|
||||
});
|
||||
|
||||
it('should prioritize dashboards over metrics in standard cases', () => {
|
||||
const files: ExtractedFile[] = [
|
||||
{
|
||||
id: 'metric-1',
|
||||
fileType: 'metric',
|
||||
fileName: 'Revenue Metric',
|
||||
status: 'completed',
|
||||
operation: 'created',
|
||||
versionNumber: 1,
|
||||
},
|
||||
{
|
||||
id: 'dashboard-1',
|
||||
fileType: 'dashboard',
|
||||
fileName: 'Sales Dashboard',
|
||||
status: 'completed',
|
||||
operation: 'created',
|
||||
versionNumber: 1,
|
||||
},
|
||||
];
|
||||
|
||||
const selected = selectFilesForResponse(files);
|
||||
|
||||
expect(selected).toHaveLength(1);
|
||||
expect(selected[0]?.id).toBe('dashboard-1');
|
||||
});
|
||||
|
||||
it('should deduplicate files by ID and keep highest version', () => {
|
||||
const files: ExtractedFile[] = [
|
||||
{
|
||||
id: 'dashboard-1',
|
||||
fileType: 'dashboard',
|
||||
fileName: 'Sales Dashboard',
|
||||
status: 'completed',
|
||||
operation: 'created',
|
||||
versionNumber: 1,
|
||||
},
|
||||
{
|
||||
id: 'dashboard-1',
|
||||
fileType: 'dashboard',
|
||||
fileName: 'Sales Dashboard',
|
||||
status: 'completed',
|
||||
operation: 'modified',
|
||||
versionNumber: 2,
|
||||
},
|
||||
{
|
||||
id: 'metric-1',
|
||||
fileType: 'metric',
|
||||
fileName: 'Revenue Metric',
|
||||
status: 'completed',
|
||||
operation: 'created',
|
||||
versionNumber: 3,
|
||||
},
|
||||
{
|
||||
id: 'metric-1',
|
||||
fileType: 'metric',
|
||||
fileName: 'Revenue Metric',
|
||||
status: 'completed',
|
||||
operation: 'modified',
|
||||
versionNumber: 1,
|
||||
},
|
||||
];
|
||||
|
||||
const selected = selectFilesForResponse(files);
|
||||
|
||||
expect(selected).toHaveLength(2);
|
||||
|
||||
const dashboard = selected.find((f) => f.fileType === 'dashboard');
|
||||
const metric = selected.find((f) => f.fileType === 'metric');
|
||||
|
||||
expect(dashboard?.versionNumber).toBe(2);
|
||||
expect(dashboard?.operation).toBe('modified');
|
||||
|
||||
expect(metric?.versionNumber).toBe(3);
|
||||
expect(metric?.operation).toBe('created');
|
||||
});
|
||||
|
||||
it('should default missing version numbers to 1 during deduplication', () => {
|
||||
const files: ExtractedFile[] = [
|
||||
{
|
||||
id: 'metric-1',
|
||||
fileType: 'metric',
|
||||
fileName: 'Revenue Metric',
|
||||
status: 'completed',
|
||||
operation: 'created',
|
||||
// No versionNumber provided (should default to 1)
|
||||
},
|
||||
{
|
||||
id: 'metric-1',
|
||||
fileType: 'metric',
|
||||
fileName: 'Revenue Metric',
|
||||
status: 'completed',
|
||||
operation: 'modified',
|
||||
versionNumber: 2,
|
||||
},
|
||||
];
|
||||
|
||||
const selected = selectFilesForResponse(files);
|
||||
|
||||
expect(selected).toHaveLength(1);
|
||||
expect(selected[0]?.versionNumber).toBe(2);
|
||||
expect(selected[0]?.operation).toBe('modified');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createFileResponseMessages', () => {
|
||||
it('should create response messages with correct version numbers', () => {
|
||||
const files: ExtractedFile[] = [
|
||||
{
|
||||
id: 'metric-1',
|
||||
fileType: 'metric',
|
||||
fileName: 'Revenue Metric',
|
||||
status: 'completed',
|
||||
operation: 'modified',
|
||||
versionNumber: 3,
|
||||
},
|
||||
{
|
||||
id: 'dashboard-1',
|
||||
fileType: 'dashboard',
|
||||
fileName: 'Sales Dashboard',
|
||||
status: 'completed',
|
||||
operation: 'created',
|
||||
versionNumber: 1,
|
||||
},
|
||||
];
|
||||
|
||||
const messages = createFileResponseMessages(files);
|
||||
|
||||
expect(messages).toHaveLength(2);
|
||||
expect(messages[0]).toMatchObject({
|
||||
id: 'metric-1',
|
||||
type: 'file',
|
||||
file_type: 'metric',
|
||||
file_name: 'Revenue Metric',
|
||||
version_number: 3,
|
||||
});
|
||||
expect((messages[0] as any).metadata?.[0]?.message).toBe('Metric modified successfully');
|
||||
|
||||
expect(messages[1]).toMatchObject({
|
||||
id: 'dashboard-1',
|
||||
type: 'file',
|
||||
file_type: 'dashboard',
|
||||
file_name: 'Sales Dashboard',
|
||||
version_number: 1,
|
||||
});
|
||||
expect((messages[1] as any).metadata?.[0]?.message).toBe('Dashboard created successfully');
|
||||
});
|
||||
|
||||
it('should default to version 1 if version number is missing', () => {
|
||||
const files: ExtractedFile[] = [
|
||||
{
|
||||
id: 'metric-1',
|
||||
fileType: 'metric',
|
||||
fileName: 'Revenue Metric',
|
||||
status: 'completed',
|
||||
// No versionNumber provided
|
||||
},
|
||||
];
|
||||
|
||||
const messages = createFileResponseMessages(files);
|
||||
|
||||
expect((messages[0] as any).version_number).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,505 +0,0 @@
|
|||
import type {
|
||||
ChatMessageReasoningMessage,
|
||||
ChatMessageResponseMessage,
|
||||
} from '@buster/server-shared/chats';
|
||||
|
||||
// Helper functions to check for failure indicators
|
||||
function hasFailureIndicators(entry: ChatMessageReasoningMessage): boolean {
|
||||
return (
|
||||
entry.status === 'failed' ||
|
||||
(entry.title?.toLowerCase().includes('error') ?? false) ||
|
||||
(entry.title?.toLowerCase().includes('failed') ?? false)
|
||||
);
|
||||
}
|
||||
|
||||
function hasFileFailureIndicators(file: { status?: string; file_name?: string }): boolean {
|
||||
return (
|
||||
file.status === 'failed' ||
|
||||
file.status === 'error' ||
|
||||
(file.file_name?.toLowerCase().includes('error') ?? false) ||
|
||||
(file.file_name?.toLowerCase().includes('failed') ?? false)
|
||||
);
|
||||
}
|
||||
|
||||
// File tracking types
|
||||
export interface ExtractedFile {
|
||||
id: string;
|
||||
fileType: 'metric' | 'dashboard' | 'report';
|
||||
fileName: string;
|
||||
status: 'completed' | 'failed' | 'loading';
|
||||
ymlContent?: string;
|
||||
markdownContent?: string; // For reports
|
||||
operation?: 'created' | 'modified' | undefined; // Track if file was created or modified
|
||||
versionNumber?: number | undefined; // Version number from the file operation
|
||||
containedInDashboards?: string[]; // Dashboard IDs that contain this metric (for metrics only)
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract successfully created/modified files from reasoning history
|
||||
* Enhanced with multiple safety checks to prevent failed files from being included
|
||||
*/
|
||||
export function extractFilesFromReasoning(
|
||||
reasoningHistory: ChatMessageReasoningMessage[]
|
||||
): ExtractedFile[] {
|
||||
const files: ExtractedFile[] = [];
|
||||
|
||||
for (const entry of reasoningHistory) {
|
||||
// Multi-layer safety checks:
|
||||
// 1. Must be a files entry with completed status
|
||||
// 2. Must not have any failure indicators (additional safety net)
|
||||
// 3. Individual files must have completed status
|
||||
if (
|
||||
entry.type === 'files' &&
|
||||
entry.status === 'completed' &&
|
||||
entry.files &&
|
||||
!hasFailureIndicators(entry)
|
||||
) {
|
||||
// Detect operation type from the entry title
|
||||
const operation = detectOperationType(entry.title);
|
||||
|
||||
for (const fileId of entry.file_ids || []) {
|
||||
const file = entry.files[fileId];
|
||||
|
||||
// Enhanced file validation:
|
||||
// - File must exist and have completed status
|
||||
// - File must not have error indicators
|
||||
// - File must have required properties (file_type, file_name)
|
||||
if (
|
||||
file &&
|
||||
file.status === 'completed' &&
|
||||
file.file_type &&
|
||||
file.file_name &&
|
||||
!hasFileFailureIndicators(file)
|
||||
) {
|
||||
const extractedFile: ExtractedFile = {
|
||||
id: fileId,
|
||||
fileType: file.file_type as 'metric' | 'dashboard' | 'report',
|
||||
fileName: file.file_name,
|
||||
status: 'completed',
|
||||
operation: operation || undefined,
|
||||
versionNumber: file.version_number || undefined,
|
||||
};
|
||||
|
||||
// Add appropriate content based on file type
|
||||
if (file.file_type === 'report') {
|
||||
extractedFile.markdownContent = file.file?.text || '';
|
||||
} else {
|
||||
extractedFile.ymlContent = file.file?.text || '';
|
||||
}
|
||||
|
||||
files.push(extractedFile);
|
||||
} else {
|
||||
// Log why file was rejected for debugging
|
||||
console.warn(`Rejecting file for response: ${fileId}`, {
|
||||
fileId,
|
||||
fileName: file?.file_name || 'unknown',
|
||||
fileStatus: file?.status || 'unknown',
|
||||
hasFile: !!file,
|
||||
hasFileType: !!file?.file_type,
|
||||
hasFileName: !!file?.file_name,
|
||||
hasFailureIndicators: file ? hasFileFailureIndicators(file) : false,
|
||||
entryId: entry.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build metric-to-dashboard relationships
|
||||
buildMetricToDashboardRelationships(files);
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect if a file was created or modified based on the entry title
|
||||
*/
|
||||
function detectOperationType(title?: string): 'created' | 'modified' | undefined {
|
||||
if (!title) return undefined;
|
||||
|
||||
const lowerTitle = title.toLowerCase();
|
||||
if (lowerTitle.includes('created') || lowerTitle.includes('creating')) {
|
||||
return 'created';
|
||||
}
|
||||
if (lowerTitle.includes('modified') || lowerTitle.includes('modifying')) {
|
||||
return 'modified';
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse dashboard YML content to extract metric IDs
|
||||
*/
|
||||
function extractMetricIdsFromDashboard(ymlContent: string): string[] {
|
||||
try {
|
||||
// First try to parse as JSON (for test data and already parsed content)
|
||||
let dashboardData: unknown;
|
||||
try {
|
||||
dashboardData = JSON.parse(ymlContent);
|
||||
} catch {
|
||||
// If JSON parsing fails, try parsing as YAML
|
||||
// Since we don't have a YAML parser imported here, we'll use a simple regex approach
|
||||
// to extract metric IDs from the YAML content
|
||||
const metricIds: string[] = [];
|
||||
|
||||
// Look for UUID patterns in the content
|
||||
// This regex matches UUIDs in the format: id: "uuid" or id: uuid
|
||||
const uuidRegex =
|
||||
/id:\s*["']?([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})["']?/gi;
|
||||
let match: RegExpExecArray | null = null;
|
||||
|
||||
match = uuidRegex.exec(ymlContent);
|
||||
while (match !== null) {
|
||||
if (match[1]) {
|
||||
metricIds.push(match[1]);
|
||||
}
|
||||
match = uuidRegex.exec(ymlContent);
|
||||
}
|
||||
|
||||
// Remove duplicates
|
||||
return [...new Set(metricIds)];
|
||||
}
|
||||
|
||||
// If we successfully parsed as JSON, extract metric IDs from the structure
|
||||
const metricIds: string[] = [];
|
||||
|
||||
if (
|
||||
dashboardData &&
|
||||
typeof dashboardData === 'object' &&
|
||||
'rows' in dashboardData &&
|
||||
Array.isArray(dashboardData.rows)
|
||||
) {
|
||||
for (const row of dashboardData.rows) {
|
||||
if (row && typeof row === 'object' && 'items' in row && Array.isArray(row.items)) {
|
||||
for (const item of row.items) {
|
||||
if (item && typeof item === 'object' && 'id' in item && typeof item.id === 'string') {
|
||||
metricIds.push(item.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return metricIds;
|
||||
} catch (error) {
|
||||
console.warn('Failed to parse dashboard content for metric extraction:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deduplicate files by ID, keeping the highest version number
|
||||
*/
|
||||
function deduplicateFilesByVersion(files: ExtractedFile[]): ExtractedFile[] {
|
||||
const deduplicated = new Map<string, ExtractedFile>();
|
||||
|
||||
for (const file of files) {
|
||||
const existingFile = deduplicated.get(file.id);
|
||||
const fileVersion = file.versionNumber || 1;
|
||||
const existingVersion = existingFile?.versionNumber || 1;
|
||||
|
||||
if (!existingFile || fileVersion > existingVersion) {
|
||||
if (existingFile && fileVersion > existingVersion) {
|
||||
console.info('[File Selection] Replacing file with higher version:', {
|
||||
fileId: file.id,
|
||||
fileName: file.fileName,
|
||||
oldVersion: existingVersion,
|
||||
newVersion: fileVersion,
|
||||
});
|
||||
}
|
||||
deduplicated.set(file.id, file);
|
||||
} else if (fileVersion < existingVersion) {
|
||||
console.info('[File Selection] Skipping file with lower version:', {
|
||||
fileId: file.id,
|
||||
fileName: file.fileName,
|
||||
currentVersion: existingVersion,
|
||||
skippedVersion: fileVersion,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(deduplicated.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Build metric-to-dashboard relationships from extracted files
|
||||
*/
|
||||
function buildMetricToDashboardRelationships(files: ExtractedFile[]): void {
|
||||
// First pass: collect all dashboard-to-metric mappings
|
||||
const dashboardToMetrics = new Map<string, string[]>();
|
||||
|
||||
for (const file of files) {
|
||||
if (file.fileType === 'dashboard' && file.ymlContent) {
|
||||
const metricIds = extractMetricIdsFromDashboard(file.ymlContent);
|
||||
if (metricIds.length > 0) {
|
||||
dashboardToMetrics.set(file.id, metricIds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: add dashboard relationships to metrics
|
||||
for (const file of files) {
|
||||
if (file.fileType === 'metric') {
|
||||
file.containedInDashboards = [];
|
||||
|
||||
// Check which dashboards contain this metric
|
||||
for (const [dashboardId, metricIds] of dashboardToMetrics) {
|
||||
if (metricIds.includes(file.id)) {
|
||||
file.containedInDashboards.push(dashboardId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply intelligent selection logic for files to return
|
||||
* Enhanced priority logic that considers modified files and dashboard-metric relationships
|
||||
*/
|
||||
export function selectFilesForResponse(
|
||||
files: ExtractedFile[],
|
||||
dashboardContext?: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
versionNumber: number;
|
||||
metricIds: string[];
|
||||
}>
|
||||
): ExtractedFile[] {
|
||||
// Debug logging
|
||||
console.info('[File Selection] Starting file selection:', {
|
||||
totalFiles: files.length,
|
||||
dashboardContextCount: dashboardContext?.length || 0,
|
||||
dashboardContextProvided: dashboardContext !== undefined,
|
||||
dashboardContextIsArray: Array.isArray(dashboardContext),
|
||||
fileTypes: files.map((f) => ({ id: f.id, type: f.fileType, operation: f.operation })),
|
||||
});
|
||||
|
||||
// Additional debug logging for dashboard context
|
||||
if (dashboardContext === undefined) {
|
||||
console.info('[File Selection] Dashboard context is undefined');
|
||||
} else if (dashboardContext === null) {
|
||||
console.info('[File Selection] Dashboard context is null');
|
||||
} else if (dashboardContext.length === 0) {
|
||||
console.info('[File Selection] Dashboard context is empty array');
|
||||
} else {
|
||||
console.info('[File Selection] Dashboard context details:', {
|
||||
dashboardCount: dashboardContext.length,
|
||||
dashboards: dashboardContext.map((d) => ({
|
||||
id: d.id,
|
||||
name: d.name,
|
||||
metricCount: d.metricIds.length,
|
||||
metricIds: d.metricIds,
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
// Separate dashboards, metrics, and reports
|
||||
const dashboards = files.filter((f) => f.fileType === 'dashboard');
|
||||
const metrics = files.filter((f) => f.fileType === 'metric');
|
||||
const reports = files.filter((f) => f.fileType === 'report');
|
||||
|
||||
console.info('[File Selection] File breakdown:', {
|
||||
dashboards: dashboards.length,
|
||||
metrics: metrics.length,
|
||||
reports: reports.length,
|
||||
modifiedMetrics: metrics.filter((m) => m.operation === 'modified').length,
|
||||
modifiedReports: reports.filter((r) => r.operation === 'modified').length,
|
||||
});
|
||||
|
||||
// Track which dashboards need to be included due to modified metrics
|
||||
const dashboardsToInclude = new Set<string>();
|
||||
const contextDashboardsToInclude: ExtractedFile[] = [];
|
||||
|
||||
// First, check if any modified metrics belong to dashboards from the current session
|
||||
for (const metric of metrics) {
|
||||
if (metric.operation === 'modified' && metric.containedInDashboards) {
|
||||
// This metric was modified and belongs to dashboard(s)
|
||||
for (const dashboardId of metric.containedInDashboards) {
|
||||
// Check if this dashboard exists in our current file set
|
||||
const dashboardExists = files.some(
|
||||
(f) => f.id === dashboardId && f.fileType === 'dashboard'
|
||||
);
|
||||
if (dashboardExists) {
|
||||
dashboardsToInclude.add(dashboardId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Second, check if any modified metrics belong to dashboards from the database context
|
||||
if (dashboardContext && dashboardContext.length > 0) {
|
||||
for (const metric of metrics) {
|
||||
if (metric.operation === 'modified') {
|
||||
console.info('[File Selection] Found modified metric:', {
|
||||
metricId: metric.id,
|
||||
metricName: metric.fileName,
|
||||
checkingAgainstDashboards: dashboardContext.length,
|
||||
});
|
||||
|
||||
// Check if this metric ID is in any dashboard from context
|
||||
for (const contextDashboard of dashboardContext) {
|
||||
console.info('[File Selection] Checking dashboard:', {
|
||||
dashboardId: contextDashboard.id,
|
||||
dashboardName: contextDashboard.name,
|
||||
dashboardMetricIds: contextDashboard.metricIds,
|
||||
lookingForMetricId: metric.id,
|
||||
metricIdInDashboard: contextDashboard.metricIds.includes(metric.id),
|
||||
});
|
||||
|
||||
if (contextDashboard.metricIds.includes(metric.id)) {
|
||||
console.info('[File Selection] Modified metric found in dashboard:', {
|
||||
metricId: metric.id,
|
||||
dashboardId: contextDashboard.id,
|
||||
dashboardName: contextDashboard.name,
|
||||
});
|
||||
|
||||
// Convert context dashboard to ExtractedFile format
|
||||
const dashboardFile: ExtractedFile = {
|
||||
id: contextDashboard.id,
|
||||
fileType: 'dashboard',
|
||||
fileName: contextDashboard.name,
|
||||
status: 'completed',
|
||||
versionNumber: contextDashboard.versionNumber,
|
||||
containedInDashboards: [],
|
||||
operation: undefined, // These are existing dashboards, not created/modified
|
||||
};
|
||||
|
||||
// Only add if not already in our files or contextDashboardsToInclude
|
||||
const alreadyIncluded =
|
||||
files.some((f) => f.id === dashboardFile.id) ||
|
||||
contextDashboardsToInclude.some((f) => f.id === dashboardFile.id);
|
||||
|
||||
if (!alreadyIncluded) {
|
||||
contextDashboardsToInclude.push(dashboardFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build final selection based on priority rules
|
||||
const selectedFiles: ExtractedFile[] = [];
|
||||
|
||||
// 1. First priority: Dashboards from context that contain modified metrics
|
||||
if (contextDashboardsToInclude.length > 0) {
|
||||
console.info('[File Selection] Adding context dashboards:', {
|
||||
count: contextDashboardsToInclude.length,
|
||||
dashboards: contextDashboardsToInclude.map((d) => ({ id: d.id, name: d.fileName })),
|
||||
});
|
||||
selectedFiles.push(...contextDashboardsToInclude);
|
||||
} else {
|
||||
console.info('[File Selection] No context dashboards to include');
|
||||
}
|
||||
|
||||
// 2. Second priority: Dashboards from current session that contain modified metrics
|
||||
if (dashboardsToInclude.size > 0) {
|
||||
const affectedDashboards = dashboards.filter((d) => dashboardsToInclude.has(d.id));
|
||||
selectedFiles.push(...affectedDashboards);
|
||||
}
|
||||
|
||||
// 3. Third priority: Other dashboards that were directly created/modified
|
||||
const otherDashboards = dashboards.filter((d) => !dashboardsToInclude.has(d.id));
|
||||
selectedFiles.push(...otherDashboards);
|
||||
|
||||
// 4. Always include all reports (reports are standalone documents)
|
||||
selectedFiles.push(...reports);
|
||||
|
||||
// 5. Determine which metrics to include
|
||||
if (selectedFiles.length > 0) {
|
||||
// Check if we have any dashboards in the selection
|
||||
const hasDashboards = selectedFiles.some((f) => f.fileType === 'dashboard');
|
||||
|
||||
if (hasDashboards) {
|
||||
// 2. Standalone metrics that are NOT already represented in selected dashboards
|
||||
const metricsInDashboards = new Set<string>();
|
||||
|
||||
// Check metrics in session dashboards
|
||||
for (const dashboard of selectedFiles.filter((f) => f.ymlContent)) {
|
||||
if (dashboard.ymlContent) {
|
||||
const metricIds = extractMetricIdsFromDashboard(dashboard.ymlContent);
|
||||
for (const id of metricIds) {
|
||||
metricsInDashboards.add(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check metrics in context dashboards
|
||||
if (dashboardContext) {
|
||||
for (const dashboard of selectedFiles) {
|
||||
const contextDashboard = dashboardContext.find((d) => d.id === dashboard.id);
|
||||
if (contextDashboard) {
|
||||
for (const metricId of contextDashboard.metricIds) {
|
||||
metricsInDashboards.add(metricId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Include standalone metrics (not in any returned dashboard)
|
||||
// Apply priority logic: when dashboards are present, exclude standalone created metrics
|
||||
const standaloneMetrics = metrics.filter((m) => !metricsInDashboards.has(m.id));
|
||||
|
||||
// Check if any standalone metrics are the result of deduplication
|
||||
const originalMetrics = files.filter((f) => f.fileType === 'metric');
|
||||
const hasDeduplicatedMetrics = standaloneMetrics.some((metric) => {
|
||||
const duplicates = originalMetrics.filter((m) => m.id === metric.id);
|
||||
return duplicates.length > 1;
|
||||
});
|
||||
|
||||
if (hasDeduplicatedMetrics) {
|
||||
// Include all standalone metrics when deduplication occurred
|
||||
selectedFiles.push(...standaloneMetrics);
|
||||
} else {
|
||||
const standaloneModifiedMetrics = standaloneMetrics.filter(
|
||||
(m) => m.operation === 'modified'
|
||||
);
|
||||
selectedFiles.push(...standaloneModifiedMetrics);
|
||||
}
|
||||
} else {
|
||||
// No dashboards selected, include all metrics
|
||||
selectedFiles.push(...metrics);
|
||||
}
|
||||
} else {
|
||||
// No dashboards selected, just return metrics
|
||||
selectedFiles.push(...metrics);
|
||||
}
|
||||
|
||||
// Apply final deduplication to handle any remaining duplicates
|
||||
const finalSelection = deduplicateFilesByVersion(selectedFiles);
|
||||
|
||||
console.info('[File Selection] Final selection after deduplication:', {
|
||||
totalSelected: finalSelection.length,
|
||||
selectedFiles: finalSelection.map((f) => ({
|
||||
id: f.id,
|
||||
type: f.fileType,
|
||||
name: f.fileName,
|
||||
operation: f.operation,
|
||||
version: f.versionNumber || 1,
|
||||
})),
|
||||
});
|
||||
|
||||
return finalSelection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create file response messages for selected files
|
||||
*/
|
||||
export function createFileResponseMessages(files: ExtractedFile[]): ChatMessageResponseMessage[] {
|
||||
return files.map((file) => ({
|
||||
id: file.id, // Use the actual file ID instead of generating a new UUID
|
||||
type: 'file' as const,
|
||||
file_type: file.fileType,
|
||||
file_name: file.fileName,
|
||||
version_number: file.versionNumber || 1, // Use the actual version number from the file
|
||||
filter_version_id: null,
|
||||
metadata: [
|
||||
{
|
||||
status: 'completed' as const,
|
||||
message: `${file.fileType === 'dashboard' ? 'Dashboard' : file.fileType === 'report' ? 'Report' : 'Metric'} ${file.operation || 'created'} successfully`,
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
],
|
||||
}));
|
||||
}
|
|
@ -161,8 +161,8 @@ async function processDashboardFile(
|
|||
const id = dashboardId || randomUUID();
|
||||
|
||||
// Collect all metric IDs from rows if they exist
|
||||
const metricIds: string[] = dashboard.config.rows
|
||||
? dashboard.config.rows.flatMap((row) => row.items).map((item) => item.id)
|
||||
const metricIds: string[] = dashboard.rows
|
||||
? dashboard.rows.flatMap((row) => row.items).map((item) => item.id)
|
||||
: [];
|
||||
|
||||
// Validate metric IDs if any exist
|
||||
|
@ -190,7 +190,7 @@ async function processDashboardFile(
|
|||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
version_number: 1,
|
||||
content: dashboard.config, // Store the DashboardConfig directly
|
||||
content: { rows: dashboard.rows }, // Extract the config properties
|
||||
};
|
||||
|
||||
return {
|
||||
|
@ -374,8 +374,8 @@ const createDashboardFiles = wrapTraced(
|
|||
|
||||
// Create associations between metrics and dashboards
|
||||
for (const sp of successfulProcessing) {
|
||||
const metricIds: string[] = sp.dashboard.config.rows
|
||||
? sp.dashboard.config.rows.flatMap((row) => row.items).map((item) => item.id)
|
||||
const metricIds: string[] = sp.dashboard.rows
|
||||
? sp.dashboard.rows.flatMap((row) => row.items).map((item) => item.id)
|
||||
: [];
|
||||
|
||||
if (metricIds.length > 0) {
|
||||
|
|
|
@ -12,20 +12,20 @@ Creates dashboard configuration files with YAML content following the dashboard
|
|||
# - id: 1 # Required row ID (integer)
|
||||
# items:
|
||||
# - id: metric-uuid-1 # UUIDv4 of an existing metric, NO quotes
|
||||
# column_sizes: [12] # Required - must sum to exactly 12
|
||||
# columnSizes: [12] # Required - must sum to exactly 12
|
||||
# - id: 2 # REQUIRED
|
||||
# items:
|
||||
# - id: metric-uuid-2
|
||||
# - id: metric-uuid-3
|
||||
# column_sizes:
|
||||
# columnSizes:
|
||||
# - 6
|
||||
# - 6
|
||||
#
|
||||
# Rules:
|
||||
# 1. Each row can have up to 4 items
|
||||
# 2. Each row must have a unique ID
|
||||
# 3. column_sizes is required and must specify the width for each item
|
||||
# 4. Sum of column_sizes in a row must be exactly 12
|
||||
# 3. columnSizes is required and must specify the width for each item
|
||||
# 4. Sum of columnSizes in a row must be exactly 12
|
||||
# 5. Each column size must be at least 3
|
||||
# 6. All arrays should follow the YML array syntax using `-` and should NOT USE `[]` formatting.
|
||||
# 7. Don't use comments. The ones in the example are just for explanation
|
||||
|
@ -65,7 +65,7 @@ properties:
|
|||
description: UUIDv4 identifier of an existing metric
|
||||
required:
|
||||
- id
|
||||
column_sizes:
|
||||
columnSizes:
|
||||
type: array
|
||||
description: Required array of column sizes (must sum to exactly 12)
|
||||
items:
|
||||
|
@ -75,7 +75,7 @@ properties:
|
|||
required:
|
||||
- id
|
||||
- items
|
||||
- column_sizes
|
||||
- columnSizes
|
||||
required:
|
||||
- name
|
||||
- description
|
||||
|
@ -84,9 +84,9 @@ required:
|
|||
**Key Requirements:**
|
||||
- `name`: Dashboard title (no underscores, descriptive name)
|
||||
- `description`: Natural language explanation of the dashboard's purpose and contents
|
||||
- `rows`: Array of row objects, each with unique ID, items (metric UUIDs), and column_sizes
|
||||
- `rows`: Array of row objects, each with unique ID, items (metric UUIDs), and columnSizes
|
||||
- Each row must have 1-4 items maximum
|
||||
- `column_sizes` must sum to exactly 12 and each size must be >= 3
|
||||
- `columnSizes` must sum to exactly 12 and each size must be >= 3
|
||||
- All metric IDs must be valid UUIDs of existing metrics
|
||||
- Row IDs must be unique integers (typically 1, 2, 3, etc.)
|
||||
|
||||
|
@ -104,13 +104,13 @@ rows:
|
|||
- id: 1
|
||||
items:
|
||||
- id: 550e8400-e29b-41d4-a716-446655440001
|
||||
column_sizes:
|
||||
columnSizes:
|
||||
- 12
|
||||
- id: 2
|
||||
items:
|
||||
- id: 550e8400-e29b-41d4-a716-446655440002
|
||||
- id: 550e8400-e29b-41d4-a716-446655440003
|
||||
column_sizes:
|
||||
columnSizes:
|
||||
- 6
|
||||
- 6
|
||||
- id: 3
|
||||
|
@ -118,7 +118,7 @@ rows:
|
|||
- id: 550e8400-e29b-41d4-a716-446655440004
|
||||
- id: 550e8400-e29b-41d4-a716-446655440005
|
||||
- id: 550e8400-e29b-41d4-a716-446655440006
|
||||
column_sizes:
|
||||
columnSizes:
|
||||
- 4
|
||||
- 4
|
||||
- 4
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { ReasoningMessageSchema, ResponseMessageSchema } from '@buster/database';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { ChatMessageSchema } from './chat-message.types';
|
||||
import { ReasoningMessageSchema, ResponseMessageSchema } from '@buster/database';
|
||||
|
||||
describe('ChatMessageSchema', () => {
|
||||
it('should parse a valid complete chat message', () => {
|
||||
|
|
|
@ -36,11 +36,12 @@ export const DashboardSchema = z.object({
|
|||
file_name: z.string(),
|
||||
});
|
||||
|
||||
export const DashboardYmlSchema = z.object({
|
||||
name: z.string(),
|
||||
description: z.string(),
|
||||
config: DashboardConfigSchema,
|
||||
});
|
||||
export const DashboardYmlSchema = z
|
||||
.object({
|
||||
name: z.string(),
|
||||
description: z.string(),
|
||||
})
|
||||
.merge(DashboardConfigSchema);
|
||||
|
||||
// Export inferred types
|
||||
export type DashboardConfig = z.infer<typeof DashboardConfigSchema>;
|
||||
|
|
Loading…
Reference in New Issue