refactor: enhance dashboard and metrics tools with improved descriptions and type handling

- Added the `GPT5` import to the analyst agent for enhanced functionality.
- Updated the `createModifyDashboardsStart` function to utilize `ToolCallOptions` for better input handling.
- Refactored dashboard and metrics tools to use helper functions for descriptions, improving maintainability.
- Ensured consistent type handling and removed redundant code in metrics delta processing.

These changes streamline the tools and improve the overall code quality.
This commit is contained in:
dal 2025-08-11 08:39:05 -06:00
parent 74ae581f73
commit e93a82a3f5
No known key found for this signature in database
GPG Key ID: 16F4B0E1E9F61122
9 changed files with 32 additions and 70 deletions

View File

@ -1,6 +1,7 @@
import { type ModelMessage, NoSuchToolError, hasToolCall, stepCountIs, streamText } from 'ai';
import { wrapTraced } from 'braintrust';
import z from 'zod';
import { GPT5 } from '../../llm/gpt-5';
import {
createCreateDashboardsTool,
createCreateMetricsTool,
@ -10,7 +11,6 @@ import {
} from '../../tools';
import { healToolWithLlm } from '../../utils/tool-call-repair';
import { getAnalystAgentSystemPrompt } from './get-analyst-agent-system-prompt';
import { GPT5 } from '../../llm/gpt-5';
const DEFAULT_CACHE_OPTIONS = {
anthropic: { cacheControl: { type: 'ephemeral', ttl: '1h' } },

View File

@ -113,4 +113,4 @@ export async function extractValuesWithLLM(
// Continue with empty values instead of failing
return [];
}
}
}

View File

@ -1,10 +1,10 @@
import { tool } from 'ai';
import { z } from 'zod';
import { getDashboardToolDescription } from '../helpers/get-dashboard-tool-descripton';
import { createCreateDashboardsDelta } from './create-dashboards-delta';
import { createCreateDashboardsExecute } from './create-dashboards-execute';
import { createCreateDashboardsFinish } from './create-dashboards-finish';
import { createCreateDashboardsStart } from './create-dashboards-start';
import { getDashboardToolDescription } from '../helpers/get-dashboard-tool-descripton';
// Input schema for the create dashboards tool
const CreateDashboardsInputSchema = z.object({

View File

@ -1,5 +1,5 @@
import { updateMessageEntries } from '@buster/database';
import type { ModelMessage } from 'ai';
import type { ModelMessage, ToolCallOptions } from 'ai';
import { wrapTraced } from 'braintrust';
import { createDashboardsReasoningMessage } from './helpers/modify-dashboards-transform-helper';
import type {
@ -14,7 +14,11 @@ export function createModifyDashboardsStart(
state: ModifyDashboardsState
) {
return wrapTraced(
async (input: ModifyDashboardsInput) => {
async (options: ToolCallOptions) => {
state.toolCallId = options.toolCallId;
const input = options.input as ModifyDashboardsInput;
// Log the start of dashboard modification
const fileCount = input.files?.length || 0;
const messageId = context.messageId;
@ -25,12 +29,6 @@ export function createModifyDashboardsStart(
timestamp: new Date().toISOString(),
});
// Initialize state
state.processingStartTime = Date.now();
state.toolCallId = `modify-dashboards-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
state.parsedArgs = input;
// Initialize files in state (with IDs from input)
state.files = input.files.map((file) => ({
id: file.id,
yml_content: file.yml_content,

View File

@ -1,5 +1,6 @@
import { tool } from 'ai';
import { z } from 'zod';
import { getDashboardToolDescription } from '../helpers/get-dashboard-tool-descripton';
import { createModifyDashboardsDelta } from './modify-dashboards-delta';
import { createModifyDashboardsExecute } from './modify-dashboards-execute';
import { createModifyDashboardsFinish } from './modify-dashboards-finish';
@ -98,46 +99,8 @@ export function createModifyDashboardsTool(context: ModifyDashboardsContext) {
const onInputDelta = createModifyDashboardsDelta(context, state);
const onInputAvailable = createModifyDashboardsFinish(context, state);
// Get the description from the original tool
const description = `Updates existing dashboard configuration files with new YAML content. Provide the complete YAML content for each dashboard, replacing the entire existing file. This tool is ideal for bulk modifications when you need to update multiple dashboards simultaneously. The system will preserve version history and perform all necessary validations on the new content. For each dashboard, you need its UUID and the complete updated YAML content. **Prefer modifying dashboards in bulk using this tool rather than one by one.**
## COMPLETE DASHBOARD YAML SCHEMA
# DASHBOARD CONFIGURATION - YML STRUCTURE
# ----------------------------------------
# Required fields:
#
# name: Your Dashboard Title # Do NOT use quotes for string values
# description: A description of the dashboard, its metrics, and its purpose. # NO quotes
# rows:
# - 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
# - id: 2 # REQUIRED
# items:
# - id: metric-uuid-2
# - id: metric-uuid-3
# column_sizes:
# - 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
# 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
# 8. String values generally should NOT use quotes unless they contain special characters (like :, {, }, [, ], ,, &, *, #, ?, |, -, <, >, =, !, %, @, \`) or start/end with whitespace.
# 9. If a string contains special characters or needs to preserve leading/trailing whitespace, enclose it in double quotes (\`"\`). Example: \`name: "Sales & Marketing Dashboard"\`
# 10. Avoid special characters in names and descriptions where possible, but if needed, use quotes as described in rule 9. UUIDs should NEVER be quoted.
**CRITICAL:** Follow the schema exactly - all metric IDs must reference existing metrics, column sizes must sum to 12, and row IDs must be unique. The tool will validate all metric references against the database.`;
return tool({
description,
description: getDashboardToolDescription(),
inputSchema: ModifyDashboardsInputSchema,
outputSchema: ModifyDashboardsOutputSchema,
execute,

View File

@ -1,5 +1,6 @@
import { updateMessageEntries } from '@buster/database';
import type { ChatMessageReasoningMessage } from '@buster/server-shared/chats';
import type { ToolCallOptions } from 'ai';
import {
OptimisticJsonParser,
getOptimisticValue,
@ -17,7 +18,7 @@ import {
// Factory function for onInputDelta callback
export function createCreateMetricsDelta(context: CreateMetricsContext, state: CreateMetricsState) {
return async (delta: string | Partial<CreateMetricsInput>) => {
return async (options: { inputTextDelta: string } & ToolCallOptions) => {
const messageId = context.messageId;
// Initialize files array if not already initialized
@ -26,8 +27,8 @@ export function createCreateMetricsDelta(context: CreateMetricsContext, state: C
}
// Handle string deltas (streaming JSON)
if (typeof delta === 'string') {
state.argsText = (state.argsText || '') + delta;
if (typeof options.inputTextDelta === 'string') {
state.argsText = (state.argsText || '') + options.inputTextDelta;
// Use optimistic parsing to extract values even from incomplete JSON
const parseResult = OptimisticJsonParser.parse(state.argsText);
@ -58,21 +59,21 @@ export function createCreateMetricsDelta(context: CreateMetricsContext, state: C
const ymlContent = file[CREATE_METRICS_KEYS.yml_content] as string;
// Check if file already exists in state
if (state.files![index]) {
if (state.files?.[index]) {
// Update existing file
state.files![index].name = name;
state.files![index].yml_content = ymlContent;
state.files[index].name = name;
state.files[index].yml_content = ymlContent;
} else {
// Add new file
state.files![index] = {
state.files[index] = {
name,
yml_content: ymlContent,
status: 'processing',
};
}
} else if (hasName && !state.files![index]) {
} else if (hasName && !state.files?.[index]) {
// Add placeholder with just name
state.files![index] = {
state.files[index] = {
name: file[CREATE_METRICS_KEYS.name] as string,
yml_content: '',
status: 'processing',
@ -117,20 +118,20 @@ export function createCreateMetricsDelta(context: CreateMetricsContext, state: C
}
} else {
// Handle object deltas (complete input)
if (delta.files) {
state.parsedArgs = delta as CreateMetricsInput;
state.files = delta.files.map((file) => ({
if (options.input.files) {
state.parsedArgs = options.input as CreateMetricsInput;
state.files = options.input.files.map((file) => ({
name: file.name,
yml_content: file.yml_content,
status: 'processing' as const,
status: 'processing',
}));
}
}
console.info('[create-metrics] Input delta processed', {
hasFiles: !!state.files.filter((f) => f).length,
fileCount: state.files.filter((f) => f).length,
processedCount: state.files.filter((f) => f?.yml_content).length,
hasFiles: !!state.files?.filter((f) => f).length,
fileCount: state.files?.filter((f) => f).length,
processedCount: state.files?.filter((f) => f?.yml_content).length,
messageId,
timestamp: new Date().toISOString(),
});

View File

@ -1,10 +1,10 @@
import { tool } from 'ai';
import { z } from 'zod';
import { getMetricToolDescription } from '../helpers/get-metric-tool-description';
import { createCreateMetricsDelta } from './create-metrics-delta';
import { createCreateMetricsExecute } from './create-metrics-execute';
import { createCreateMetricsFinish } from './create-metrics-finish';
import { createCreateMetricsStart } from './create-metrics-start';
import { getMetricToolDescription } from '../helpers/get-metric-tool-description';
const CreateMetricsInputSchema = z.object({
files: z

View File

@ -77,4 +77,4 @@ describe('Metric Tool Description Instructions', () => {
getMetricToolDescriptionPrompt(' '); // whitespace only
}).toThrow('SQL dialect guidance is required');
});
});
});

View File

@ -1,4 +1,4 @@
import { describe, it, expect } from 'vitest';
import { describe, expect, it } from 'vitest';
import { ensureTimeFrameQuoted } from './time-frame-helper';
describe('ensureTimeFrameQuoted', () => {
@ -87,4 +87,4 @@ timeFrame: '90d'
timeFrame: "1y"`;
expect(ensureTimeFrameQuoted(input)).toBe(expected);
});
});
});