mirror of https://github.com/buster-so/buster.git
Merge pull request #1035 from buster-so/dallin-bus-1868-disable-createreports-tool-from-creating-multiple-reports-at
dallin bus 1868 disable createreports tool from creating multiple reports at
This commit is contained in:
commit
d05fa10c62
|
@ -58,8 +58,8 @@ async function generateTitleWithLLM(messages: ModelMessage[]): Promise<string> {
|
|||
const { object } = await generateObject({
|
||||
model: Haiku35,
|
||||
headers: {
|
||||
'anthropic-beta': 'fine-grained-tool-streaming-2025-05-14,context-1m-2025-08-07',
|
||||
anthropic_beta: 'fine-grained-tool-streaming-2025-05-14,context-1m-2025-08-07',
|
||||
'anthropic-beta': 'fine-grained-tool-streaming-2025-05-14',
|
||||
anthropic_beta: 'fine-grained-tool-streaming-2025-05-14',
|
||||
},
|
||||
schema: llmOutputSchema,
|
||||
messages: titleMessages,
|
||||
|
|
|
@ -94,13 +94,24 @@ export function extractAllFilesForChatUpdate(messages: ModelMessage[]): Extracte
|
|||
const contentObj = content as { toolCallId?: string; input?: unknown };
|
||||
const toolCallId = contentObj.toolCallId;
|
||||
const input = contentObj.input as {
|
||||
name?: string;
|
||||
content?: string;
|
||||
// Legacy support for old array structure
|
||||
files?: Array<{ yml_content?: string; content?: string }>;
|
||||
};
|
||||
if (toolCallId && input && input.files && Array.isArray(input.files)) {
|
||||
for (const file of input.files) {
|
||||
const reportContent = file.yml_content || file.content;
|
||||
if (reportContent) {
|
||||
createReportContents.set(toolCallId, reportContent);
|
||||
|
||||
// Handle new single-file structure
|
||||
if (toolCallId && input) {
|
||||
if (input.content) {
|
||||
// New structure: single report
|
||||
createReportContents.set(toolCallId, input.content);
|
||||
} else if (input.files && Array.isArray(input.files)) {
|
||||
// Legacy structure: array of files
|
||||
for (const file of input.files) {
|
||||
const reportContent = file.yml_content || file.content;
|
||||
if (reportContent) {
|
||||
createReportContents.set(toolCallId, reportContent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -198,18 +209,32 @@ export function extractFilesFromToolCalls(messages: ModelMessage[]): ExtractedFi
|
|||
const contentObj = content as { toolCallId?: string; input?: unknown };
|
||||
const toolCallId = contentObj.toolCallId;
|
||||
const input = contentObj.input as {
|
||||
name?: string;
|
||||
content?: string;
|
||||
// Legacy support for old array structure
|
||||
files?: Array<{ yml_content?: string; content?: string }>;
|
||||
};
|
||||
if (toolCallId && input && input.files && Array.isArray(input.files)) {
|
||||
for (const file of input.files) {
|
||||
// Check for both yml_content and content fields
|
||||
const reportContent = file.yml_content || file.content;
|
||||
if (reportContent) {
|
||||
createReportContents.set(toolCallId, reportContent);
|
||||
console.info('[done-tool-file-selection] Stored report content for toolCallId', {
|
||||
toolCallId,
|
||||
contentLength: reportContent.length,
|
||||
});
|
||||
|
||||
if (toolCallId && input) {
|
||||
if (input.content) {
|
||||
// New structure: single report
|
||||
createReportContents.set(toolCallId, input.content);
|
||||
console.info('[done-tool-file-selection] Stored report content for toolCallId', {
|
||||
toolCallId,
|
||||
contentLength: input.content.length,
|
||||
});
|
||||
} else if (input.files && Array.isArray(input.files)) {
|
||||
// Legacy structure: array of files
|
||||
for (const file of input.files) {
|
||||
// Check for both yml_content and content fields
|
||||
const reportContent = file.yml_content || file.content;
|
||||
if (reportContent) {
|
||||
createReportContents.set(toolCallId, reportContent);
|
||||
console.info('[done-tool-file-selection] Stored report content for toolCallId', {
|
||||
toolCallId,
|
||||
contentLength: reportContent.length,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -493,8 +518,47 @@ function processCreateReportsOutput(
|
|||
const reportsOutput = output as CreateReportsOutput;
|
||||
let reportInfo: ReportInfo | undefined;
|
||||
|
||||
if ('files' in reportsOutput && reportsOutput.files && Array.isArray(reportsOutput.files)) {
|
||||
console.info('[done-tool-file-selection] Processing create report files array', {
|
||||
// Handle new single-file structure
|
||||
if ('file' in reportsOutput && reportsOutput.file && typeof reportsOutput.file === 'object') {
|
||||
const file = reportsOutput.file;
|
||||
console.info('[done-tool-file-selection] Processing create report single file', {
|
||||
toolCallId,
|
||||
hasContent: toolCallId && createReportContents ? createReportContents.has(toolCallId) : false,
|
||||
});
|
||||
|
||||
const fileName = file.name;
|
||||
if (file.id && fileName) {
|
||||
// Get the content from the create report input using toolCallId
|
||||
const content =
|
||||
toolCallId && createReportContents ? createReportContents.get(toolCallId) : undefined;
|
||||
|
||||
files.push({
|
||||
id: file.id,
|
||||
fileType: 'report_file',
|
||||
fileName: fileName,
|
||||
status: 'completed',
|
||||
operation: 'created',
|
||||
versionNumber: file.version_number || 1,
|
||||
content: content, // Store the content from the input
|
||||
});
|
||||
|
||||
// Track this as the last report if we have content
|
||||
if (content) {
|
||||
reportInfo = {
|
||||
id: file.id,
|
||||
content: content,
|
||||
versionNumber: file.version_number || 1,
|
||||
operation: 'created',
|
||||
};
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
'files' in reportsOutput &&
|
||||
reportsOutput.files &&
|
||||
Array.isArray(reportsOutput.files)
|
||||
) {
|
||||
// Legacy support for array structure
|
||||
console.info('[done-tool-file-selection] Processing create report files array (legacy)', {
|
||||
count: reportsOutput.files.length,
|
||||
toolCallId,
|
||||
hasContent: toolCallId && createReportContents ? createReportContents.has(toolCallId) : false,
|
||||
|
|
|
@ -24,13 +24,11 @@ import {
|
|||
|
||||
// Define TOOL_KEYS locally since we removed them from the helper
|
||||
const TOOL_KEYS = {
|
||||
files: 'files' as const,
|
||||
name: 'name' as const,
|
||||
content: 'content' as const,
|
||||
} satisfies {
|
||||
files: keyof CreateReportsInput;
|
||||
name: keyof CreateReportsInput['files'][number];
|
||||
content: keyof CreateReportsInput['files'][number];
|
||||
name: keyof CreateReportsInput;
|
||||
content: keyof CreateReportsInput;
|
||||
};
|
||||
|
||||
type VersionHistory = (typeof reportFiles.$inferSelect)['versionHistory'];
|
||||
|
@ -57,124 +55,81 @@ export function createCreateReportsDelta(context: CreateReportsContext, state: C
|
|||
const parseResult = OptimisticJsonParser.parse(state.argsText || '');
|
||||
|
||||
if (parseResult.parsed) {
|
||||
// Extract files array from parsed result
|
||||
const filesArray = getOptimisticValue<unknown[]>(
|
||||
// Extract name and content from parsed result
|
||||
const name = getOptimisticValue<string>(parseResult.extractedValues, TOOL_KEYS.name, '');
|
||||
const rawContent = getOptimisticValue<string>(
|
||||
parseResult.extractedValues,
|
||||
TOOL_KEYS.files,
|
||||
[]
|
||||
TOOL_KEYS.content,
|
||||
''
|
||||
);
|
||||
// Unescape JSON string sequences, then normalize any double-escaped characters
|
||||
const content = rawContent ? normalizeEscapedText(unescapeJsonString(rawContent)) : '';
|
||||
|
||||
if (filesArray && Array.isArray(filesArray)) {
|
||||
// Track which reports need to be created
|
||||
const reportsToCreate: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
index: number;
|
||||
}> = [];
|
||||
// Only process if we have at least a name
|
||||
if (name) {
|
||||
// Check if report already exists in state to preserve its ID
|
||||
const existingFile = state.file;
|
||||
|
||||
// Track which reports need content updates
|
||||
const contentUpdates: Array<{
|
||||
reportId: string;
|
||||
content: string;
|
||||
}> = [];
|
||||
let reportId: string;
|
||||
let needsCreation = false;
|
||||
|
||||
// Update state files with streamed data
|
||||
const updatedFiles: CreateReportStateFile[] = [];
|
||||
if (existingFile?.id) {
|
||||
// Report already exists, use its ID
|
||||
reportId = existingFile.id;
|
||||
} else {
|
||||
// New report, generate ID and mark for creation
|
||||
reportId = randomUUID();
|
||||
needsCreation = true;
|
||||
}
|
||||
|
||||
filesArray.forEach((file, index) => {
|
||||
if (file && typeof file === 'object') {
|
||||
const fileObj = file as Record<string, unknown>;
|
||||
const name = getOptimisticValue<string>(
|
||||
new Map(Object.entries(fileObj)),
|
||||
TOOL_KEYS.name,
|
||||
''
|
||||
);
|
||||
const rawContent = getOptimisticValue<string>(
|
||||
new Map(Object.entries(fileObj)),
|
||||
TOOL_KEYS.content,
|
||||
''
|
||||
);
|
||||
// Unescape JSON string sequences, then normalize any double-escaped characters
|
||||
const content = rawContent ? normalizeEscapedText(unescapeJsonString(rawContent)) : '';
|
||||
|
||||
// Only add files that have at least a name
|
||||
if (name) {
|
||||
// Check if this file already exists in state to preserve its ID
|
||||
const existingFile = state.files?.[index];
|
||||
|
||||
let reportId: string;
|
||||
|
||||
if (existingFile?.id) {
|
||||
// Report already exists, use its ID
|
||||
reportId = existingFile.id;
|
||||
} else {
|
||||
// New report, generate ID and mark for creation
|
||||
reportId = randomUUID();
|
||||
reportsToCreate.push({ id: reportId, name, index });
|
||||
// Update state with the single report
|
||||
state.file = {
|
||||
id: reportId,
|
||||
file_name: name,
|
||||
file_type: 'report_file',
|
||||
version_number: 1,
|
||||
file: content
|
||||
? {
|
||||
text: content,
|
||||
}
|
||||
: undefined,
|
||||
status: 'loading',
|
||||
};
|
||||
|
||||
updatedFiles.push({
|
||||
id: reportId,
|
||||
file_name: name,
|
||||
file_type: 'report_file',
|
||||
version_number: 1,
|
||||
file: content
|
||||
? {
|
||||
text: content,
|
||||
}
|
||||
: undefined,
|
||||
status: 'loading',
|
||||
});
|
||||
// Track that we created/modified this report in this message
|
||||
state.reportModifiedInMessage = true;
|
||||
|
||||
// Track that we created/modified this report in this message
|
||||
if (!state.reportsModifiedInMessage) {
|
||||
state.reportsModifiedInMessage = new Set();
|
||||
}
|
||||
state.reportsModifiedInMessage.add(reportId);
|
||||
|
||||
// If we have content and a report ID, update the content
|
||||
if (content && reportId) {
|
||||
contentUpdates.push({ reportId, content });
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
state.files = updatedFiles;
|
||||
|
||||
// Create new reports in the database if needed
|
||||
if (reportsToCreate.length > 0 && context.userId && context.organizationId) {
|
||||
// Create new report in the database if needed
|
||||
if (needsCreation && context.userId && context.organizationId) {
|
||||
try {
|
||||
await db.transaction(async (tx) => {
|
||||
// Insert report files
|
||||
const reportRecords = reportsToCreate.map((report) => {
|
||||
const now = new Date().toISOString();
|
||||
return {
|
||||
id: report.id,
|
||||
name: report.name,
|
||||
content: '', // Start with empty content, will be updated via streaming
|
||||
organizationId: context.organizationId,
|
||||
createdBy: context.userId,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
deletedAt: null,
|
||||
publiclyAccessible: false,
|
||||
publiclyEnabledBy: null,
|
||||
publicExpiryDate: null,
|
||||
versionHistory: createInitialReportVersionHistory('', now),
|
||||
publicPassword: null,
|
||||
workspaceSharing: 'none' as const,
|
||||
workspaceSharingEnabledBy: null,
|
||||
workspaceSharingEnabledAt: null,
|
||||
};
|
||||
});
|
||||
await tx.insert(reportFiles).values(reportRecords);
|
||||
// Insert report file
|
||||
const now = new Date().toISOString();
|
||||
const reportRecord = {
|
||||
id: reportId,
|
||||
name: name,
|
||||
content: '', // Start with empty content, will be updated via streaming
|
||||
organizationId: context.organizationId,
|
||||
createdBy: context.userId,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
deletedAt: null,
|
||||
publiclyAccessible: false,
|
||||
publiclyEnabledBy: null,
|
||||
publicExpiryDate: null,
|
||||
versionHistory: createInitialReportVersionHistory('', now),
|
||||
publicPassword: null,
|
||||
workspaceSharing: 'none' as const,
|
||||
workspaceSharingEnabledBy: null,
|
||||
workspaceSharingEnabledAt: null,
|
||||
};
|
||||
await tx.insert(reportFiles).values(reportRecord);
|
||||
|
||||
// Insert asset permissions
|
||||
const assetPermissionRecords = reportRecords.map((record) => ({
|
||||
// Insert asset permission
|
||||
const assetPermissionRecord = {
|
||||
identityId: context.userId,
|
||||
identityType: 'user' as const,
|
||||
assetId: record.id,
|
||||
assetId: reportId,
|
||||
assetType: 'report_file' as const,
|
||||
role: 'owner' as const,
|
||||
createdAt: new Date().toISOString(),
|
||||
|
@ -182,70 +137,63 @@ export function createCreateReportsDelta(context: CreateReportsContext, state: C
|
|||
deletedAt: null,
|
||||
createdBy: context.userId,
|
||||
updatedBy: context.userId,
|
||||
}));
|
||||
await tx.insert(assetPermissions).values(assetPermissionRecords);
|
||||
};
|
||||
await tx.insert(assetPermissions).values(assetPermissionRecord);
|
||||
});
|
||||
|
||||
console.info('[create-reports] Created reports in database', {
|
||||
count: reportsToCreate.length,
|
||||
reportIds: reportsToCreate.map((r) => r.id),
|
||||
console.info('[create-reports] Created report in database', {
|
||||
reportId,
|
||||
name,
|
||||
});
|
||||
|
||||
// Note: Response messages are only created in execute phase after checking for metrics
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : 'Database creation failed';
|
||||
console.error('[create-reports] Error creating reports in database:', {
|
||||
console.error('[create-reports] Error creating report in database:', {
|
||||
error: errorMessage,
|
||||
reportCount: reportsToCreate.length,
|
||||
reportId,
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
});
|
||||
|
||||
// Mark all reports as failed with error message
|
||||
reportsToCreate.forEach(({ id }) => {
|
||||
const stateFile = state.files?.find((f) => f.id === id);
|
||||
if (stateFile) {
|
||||
stateFile.status = 'failed';
|
||||
stateFile.error = `Failed to create report in database: ${errorMessage}`;
|
||||
}
|
||||
});
|
||||
// Mark report as failed with error message
|
||||
if (state.file) {
|
||||
state.file.status = 'failed';
|
||||
state.file.error = `Failed to create report in database: ${errorMessage}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update report content for all reports that have content
|
||||
if (contentUpdates.length > 0) {
|
||||
for (const update of contentUpdates) {
|
||||
try {
|
||||
await updateReportContent({
|
||||
reportId: update.reportId,
|
||||
content: update.content,
|
||||
});
|
||||
// Update report content if we have content
|
||||
if (content && reportId) {
|
||||
try {
|
||||
await updateReportContent({
|
||||
reportId: reportId,
|
||||
content: content,
|
||||
});
|
||||
|
||||
// Keep the file status as 'loading' during streaming
|
||||
// Status will be updated to 'completed' in the execute phase
|
||||
const stateFile = state.files?.find((f) => f.id === update.reportId);
|
||||
if (stateFile) {
|
||||
// Ensure status remains 'loading' during delta phase
|
||||
stateFile.status = 'loading';
|
||||
}
|
||||
// Keep the file status as 'loading' during streaming
|
||||
// Status will be updated to 'completed' in the execute phase
|
||||
if (state.file) {
|
||||
// Ensure status remains 'loading' during delta phase
|
||||
state.file.status = 'loading';
|
||||
}
|
||||
|
||||
// Note: Response messages should only be created in execute phase
|
||||
// after all processing is complete
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Content update failed';
|
||||
console.error('[create-reports] Error updating report content:', {
|
||||
reportId: update.reportId,
|
||||
error: errorMessage,
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
});
|
||||
// Note: Response messages should only be created in execute phase
|
||||
// after all processing is complete
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Content update failed';
|
||||
console.error('[create-reports] Error updating report content:', {
|
||||
reportId: reportId,
|
||||
error: errorMessage,
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
});
|
||||
|
||||
// Keep file as loading during delta phase even on error
|
||||
// The execute phase will handle final status
|
||||
const stateFile = state.files?.find((f) => f.id === update.reportId);
|
||||
if (stateFile) {
|
||||
stateFile.status = 'loading';
|
||||
stateFile.error = `Failed to update report content: ${errorMessage}`;
|
||||
}
|
||||
// Keep file as loading during delta phase even on error
|
||||
// The execute phase will handle final status
|
||||
if (state.file) {
|
||||
state.file.status = 'loading';
|
||||
state.file.error = `Failed to update report content: ${errorMessage}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -55,35 +55,29 @@ describe('create-reports-execute', () => {
|
|||
|
||||
state = {
|
||||
toolCallId: 'tool-call-123',
|
||||
files: [],
|
||||
file: undefined,
|
||||
startTime: Date.now(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('responseMessages creation', () => {
|
||||
it('should add all reports to responseMessages', async () => {
|
||||
it('should add report to responseMessages', async () => {
|
||||
// Setup state with a successful report
|
||||
state.files = [
|
||||
{
|
||||
id: 'report-1',
|
||||
file_name: 'Sales Report Q4',
|
||||
file_type: 'report_file',
|
||||
version_number: 1,
|
||||
status: 'completed',
|
||||
},
|
||||
];
|
||||
state.file = {
|
||||
id: 'report-1',
|
||||
file_name: 'Sales Report Q4',
|
||||
file_type: 'report_file',
|
||||
version_number: 1,
|
||||
status: 'completed',
|
||||
};
|
||||
|
||||
const input: CreateReportsInput = {
|
||||
files: [
|
||||
{
|
||||
name: 'Sales Report Q4',
|
||||
content: `
|
||||
# Sales Report Q4
|
||||
<metric metricId="24db2cc8-79b0-488f-bd45-8b5412d1bf08" />
|
||||
Sales increased by 25%.
|
||||
`,
|
||||
},
|
||||
],
|
||||
name: 'Sales Report Q4',
|
||||
content: `
|
||||
# Sales Report Q4
|
||||
<metric metricId="24db2cc8-79b0-488f-bd45-8b5412d1bf08" />
|
||||
Sales increased by 25%.
|
||||
`,
|
||||
};
|
||||
|
||||
// Mock that the report contains metrics
|
||||
|
@ -110,26 +104,20 @@ describe('create-reports-execute', () => {
|
|||
|
||||
it('should add reports without metrics to responseMessages', async () => {
|
||||
// Setup state with a successful report
|
||||
state.files = [
|
||||
{
|
||||
id: 'report-2',
|
||||
file_name: 'Simple Report',
|
||||
file_type: 'report_file',
|
||||
version_number: 1,
|
||||
status: 'completed',
|
||||
},
|
||||
];
|
||||
state.file = {
|
||||
id: 'report-2',
|
||||
file_name: 'Simple Report',
|
||||
file_type: 'report_file',
|
||||
version_number: 1,
|
||||
status: 'completed',
|
||||
};
|
||||
|
||||
const input: CreateReportsInput = {
|
||||
files: [
|
||||
{
|
||||
name: 'Simple Report',
|
||||
content: `
|
||||
# Simple Report
|
||||
This report has no metrics, just text analysis.
|
||||
`,
|
||||
},
|
||||
],
|
||||
name: 'Simple Report',
|
||||
content: `
|
||||
# Simple Report
|
||||
This report has no metrics, just text analysis.
|
||||
`,
|
||||
};
|
||||
|
||||
const execute = createCreateReportsExecute(context, state);
|
||||
|
@ -152,78 +140,13 @@ describe('create-reports-execute', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should handle multiple reports all in responseMessages', async () => {
|
||||
// Setup state with multiple reports
|
||||
state.files = [
|
||||
{
|
||||
id: 'report-1',
|
||||
file_name: 'Report With Metrics',
|
||||
file_type: 'report_file',
|
||||
version_number: 1,
|
||||
status: 'completed',
|
||||
},
|
||||
{
|
||||
id: 'report-2',
|
||||
file_name: 'Report Without Metrics',
|
||||
file_type: 'report_file',
|
||||
version_number: 1,
|
||||
status: 'completed',
|
||||
},
|
||||
{
|
||||
id: 'report-3',
|
||||
file_name: 'Another Report With Metrics',
|
||||
file_type: 'report_file',
|
||||
version_number: 1,
|
||||
status: 'completed',
|
||||
},
|
||||
];
|
||||
|
||||
const input: CreateReportsInput = {
|
||||
files: [
|
||||
{
|
||||
name: 'Report With Metrics',
|
||||
content: '<metric metricId="uuid-1" />',
|
||||
},
|
||||
{
|
||||
name: 'Report Without Metrics',
|
||||
content: 'Just text',
|
||||
},
|
||||
{
|
||||
name: 'Another Report With Metrics',
|
||||
content: '<metric metricId="uuid-2" />',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const execute = createCreateReportsExecute(context, state);
|
||||
await execute(input);
|
||||
|
||||
// Check that all reports were added to responseMessages
|
||||
expect(mockUpdateMessageEntries).toHaveBeenCalled();
|
||||
// Get the last call (final entries) which should have responseMessages
|
||||
const lastCallIndex = mockUpdateMessageEntries.mock.calls.length - 1;
|
||||
const updateCall = mockUpdateMessageEntries.mock.calls[lastCallIndex]?.[0];
|
||||
|
||||
expect(updateCall?.responseMessages).toBeDefined();
|
||||
expect(updateCall?.responseMessages).toHaveLength(3); // All 3 reports
|
||||
|
||||
const responseIds = updateCall?.responseMessages?.map((msg: any) => msg.id) || [];
|
||||
expect(responseIds).toContain('report-1');
|
||||
expect(responseIds).toContain('report-3');
|
||||
expect(responseIds).toContain('report-2');
|
||||
});
|
||||
|
||||
it('should create initial entries on first execution', async () => {
|
||||
state.initialEntriesCreated = undefined;
|
||||
state.files = [];
|
||||
state.file = undefined;
|
||||
|
||||
const input: CreateReportsInput = {
|
||||
files: [
|
||||
{
|
||||
name: 'Test Report',
|
||||
content: 'Test content',
|
||||
},
|
||||
],
|
||||
name: 'Test Report',
|
||||
content: 'Test content',
|
||||
};
|
||||
|
||||
const execute = createCreateReportsExecute(context, state);
|
||||
|
@ -244,23 +167,17 @@ describe('create-reports-execute', () => {
|
|||
|
||||
it('should not create initial entries if already created', async () => {
|
||||
state.initialEntriesCreated = true;
|
||||
state.files = [
|
||||
{
|
||||
id: 'report-1',
|
||||
file_name: 'Test Report',
|
||||
file_type: 'report_file',
|
||||
version_number: 1,
|
||||
status: 'completed',
|
||||
},
|
||||
];
|
||||
state.file = {
|
||||
id: 'report-1',
|
||||
file_name: 'Test Report',
|
||||
file_type: 'report_file',
|
||||
version_number: 1,
|
||||
status: 'completed',
|
||||
};
|
||||
|
||||
const input: CreateReportsInput = {
|
||||
files: [
|
||||
{
|
||||
name: 'Test Report',
|
||||
content: '<metric metricId="uuid-1" />',
|
||||
},
|
||||
],
|
||||
name: 'Test Report',
|
||||
content: '<metric metricId="uuid-1" />',
|
||||
};
|
||||
|
||||
const execute = createCreateReportsExecute(context, state);
|
||||
|
@ -270,84 +187,59 @@ describe('create-reports-execute', () => {
|
|||
expect(mockUpdateMessageEntries).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should handle reports with failed status', async () => {
|
||||
// Simulate a scenario where one report was created during delta but another failed
|
||||
// Report 1 has an ID (was created), Report 2 has no ID (creation failed)
|
||||
state.files = [
|
||||
{
|
||||
id: 'report-1',
|
||||
file_name: 'Success Report',
|
||||
file_type: 'report_file',
|
||||
version_number: 1,
|
||||
status: 'completed',
|
||||
},
|
||||
{
|
||||
id: '', // No ID means report creation failed during delta
|
||||
file_name: 'Failed Report',
|
||||
file_type: 'report_file',
|
||||
version_number: 1,
|
||||
status: 'failed',
|
||||
error: 'Report creation failed during streaming',
|
||||
},
|
||||
];
|
||||
it('should handle report with failed status', async () => {
|
||||
// Simulate a scenario where report creation failed during delta
|
||||
state.file = {
|
||||
id: '', // No ID means report creation failed during delta
|
||||
file_name: 'Failed Report',
|
||||
file_type: 'report_file',
|
||||
version_number: 1,
|
||||
status: 'failed',
|
||||
error: 'Report creation failed during streaming',
|
||||
};
|
||||
|
||||
const input: CreateReportsInput = {
|
||||
files: [
|
||||
{
|
||||
name: 'Success Report',
|
||||
content: '<metric metricId="uuid-1" />',
|
||||
},
|
||||
{
|
||||
name: 'Failed Report',
|
||||
content: '<metric metricId="uuid-2" />',
|
||||
},
|
||||
],
|
||||
name: 'Failed Report',
|
||||
content: '<metric metricId="uuid-2" />',
|
||||
};
|
||||
|
||||
const execute = createCreateReportsExecute(context, state);
|
||||
const result = await execute(input);
|
||||
|
||||
// Result should show one success and one failure
|
||||
// Report 1 succeeds because it has an ID, Report 2 fails because it has no ID
|
||||
expect(result.files).toHaveLength(1);
|
||||
expect(result.failed_files).toHaveLength(1);
|
||||
// Result should show failure
|
||||
expect(result.file).toBeUndefined();
|
||||
expect(result.error).toBeDefined();
|
||||
expect(result.error).toContain('Report creation failed during streaming');
|
||||
|
||||
// Only the successful report should be in responseMessages
|
||||
// Get the last call (final entries) which should have responseMessages
|
||||
// No responseMessages should be created for failed report
|
||||
// Get the last call (final entries)
|
||||
const lastCallIndex = mockUpdateMessageEntries.mock.calls.length - 1;
|
||||
const updateCall = mockUpdateMessageEntries.mock.calls[lastCallIndex]?.[0];
|
||||
expect(updateCall?.responseMessages).toHaveLength(1);
|
||||
expect(updateCall?.responseMessages?.[0]?.id).toBe('report-1');
|
||||
expect(updateCall?.responseMessages).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle missing messageId in context', async () => {
|
||||
// Remove messageId from context
|
||||
context.messageId = undefined;
|
||||
|
||||
state.files = [
|
||||
{
|
||||
id: 'report-1',
|
||||
file_name: 'Test Report',
|
||||
file_type: 'report_file',
|
||||
version_number: 1,
|
||||
status: 'completed',
|
||||
},
|
||||
];
|
||||
state.file = {
|
||||
id: 'report-1',
|
||||
file_name: 'Test Report',
|
||||
file_type: 'report_file',
|
||||
version_number: 1,
|
||||
status: 'completed',
|
||||
};
|
||||
|
||||
const input: CreateReportsInput = {
|
||||
files: [
|
||||
{
|
||||
name: 'Test Report',
|
||||
content: '<metric metricId="uuid-1" />',
|
||||
},
|
||||
],
|
||||
name: 'Test Report',
|
||||
content: '<metric metricId="uuid-1" />',
|
||||
};
|
||||
|
||||
const execute = createCreateReportsExecute(context, state);
|
||||
const result = await execute(input);
|
||||
|
||||
// Should complete successfully but not call updateMessageEntries
|
||||
expect(result.files).toHaveLength(1);
|
||||
expect(result.file).toBeDefined();
|
||||
expect(mockUpdateMessageEntries).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -24,7 +24,7 @@ import {
|
|||
createCreateReportsReasoningEntry,
|
||||
} from './helpers/create-reports-tool-transform-helper';
|
||||
|
||||
// Main create report files function - returns success status
|
||||
// Main create report function - returns success status
|
||||
const getReportCreationResults = wrapTraced(
|
||||
async (
|
||||
params: CreateReportsInput,
|
||||
|
@ -38,53 +38,37 @@ const getReportCreationResults = wrapTraced(
|
|||
if (!userId) {
|
||||
return {
|
||||
message: 'Unable to verify your identity. Please log in again.',
|
||||
files: [],
|
||||
failed_files: [],
|
||||
error: 'Authentication error',
|
||||
};
|
||||
}
|
||||
if (!organizationId) {
|
||||
return {
|
||||
message: 'Unable to access your organization. Please check your permissions.',
|
||||
files: [],
|
||||
failed_files: [],
|
||||
error: 'Authorization error',
|
||||
};
|
||||
}
|
||||
|
||||
// Reports have already been created and updated in the delta function
|
||||
// Report has already been created and updated in the delta function
|
||||
// Here we just return the final status
|
||||
const files = state?.files || [];
|
||||
const successfulFiles = files.filter((f) => f.id && f.file_name && f.status === 'completed');
|
||||
const failedFiles: Array<{ name: string; error: string }> = [];
|
||||
const file = state?.file;
|
||||
|
||||
// Check for any files that weren't successfully created
|
||||
params.files.forEach((inputFile, index) => {
|
||||
const stateFile = state?.files?.[index];
|
||||
if (!stateFile || !stateFile.id || stateFile.status === 'failed') {
|
||||
failedFiles.push({
|
||||
name: inputFile.name,
|
||||
error: stateFile?.error || 'Failed to create report',
|
||||
});
|
||||
}
|
||||
});
|
||||
if (!file || !file.id || file.status === 'failed') {
|
||||
return {
|
||||
message: `Failed to create report: ${file?.error || 'Unknown error'}`,
|
||||
error: file?.error || 'Failed to create report',
|
||||
};
|
||||
}
|
||||
|
||||
// Generate result message
|
||||
let message: string;
|
||||
if (failedFiles.length === 0) {
|
||||
message = `Successfully created ${successfulFiles.length} report file${successfulFiles.length !== 1 ? 's' : ''}.`;
|
||||
} else if (successfulFiles.length === 0) {
|
||||
message = `Failed to create all report files.`;
|
||||
} else {
|
||||
message = `Successfully created ${successfulFiles.length} report file${successfulFiles.length !== 1 ? 's' : ''}. Failed to create ${failedFiles.length} file${failedFiles.length !== 1 ? 's' : ''}.`;
|
||||
}
|
||||
const message = `Successfully created report file.`;
|
||||
|
||||
return {
|
||||
message,
|
||||
files: successfulFiles.map((f) => ({
|
||||
id: f.id,
|
||||
name: f.file_name || '',
|
||||
version_number: f.version_number,
|
||||
})),
|
||||
failed_files: failedFiles,
|
||||
file: {
|
||||
id: file.id,
|
||||
name: file.file_name || params.name,
|
||||
version_number: file.version_number,
|
||||
},
|
||||
};
|
||||
},
|
||||
{ name: 'Get Report Creation Results' }
|
||||
|
@ -123,38 +107,32 @@ export function createCreateReportsExecute(
|
|||
}
|
||||
}
|
||||
|
||||
// Ensure all reports that were created during delta have complete content from input
|
||||
// Ensure the report that was created during delta has complete content from input
|
||||
// IMPORTANT: The input is the source of truth for content, not any streaming updates
|
||||
// Delta phase creates reports with empty/partial content, execute phase ensures complete content
|
||||
console.info('[create-reports] Ensuring all reports have complete content from input');
|
||||
// Delta phase creates report with empty/partial content, execute phase ensures complete content
|
||||
console.info('[create-reports] Ensuring report has complete content from input');
|
||||
|
||||
for (let i = 0; i < input.files.length; i++) {
|
||||
const inputFile = input.files[i];
|
||||
if (!inputFile) continue;
|
||||
const { name, content } = input;
|
||||
|
||||
const { name, content } = inputFile;
|
||||
// Only update report that was successfully created during delta phase
|
||||
const reportId = state.file?.id;
|
||||
|
||||
// Only update reports that were successfully created during delta phase
|
||||
const reportId = state.files?.[i]?.id;
|
||||
|
||||
if (!reportId) {
|
||||
// Report wasn't created during delta - mark as failed
|
||||
console.warn('[create-reports] Report was not created during delta phase', { name });
|
||||
|
||||
if (!state.files) {
|
||||
state.files = [];
|
||||
}
|
||||
state.files[i] = {
|
||||
id: '',
|
||||
file_name: name,
|
||||
file_type: 'report_file',
|
||||
version_number: 1,
|
||||
status: 'failed',
|
||||
error: 'Report creation failed during streaming',
|
||||
};
|
||||
continue;
|
||||
}
|
||||
if (!reportId) {
|
||||
// Report wasn't created during delta - mark as failed
|
||||
console.warn('[create-reports] Report was not created during delta phase', { name });
|
||||
|
||||
state.file = {
|
||||
id: '',
|
||||
file_name: name,
|
||||
file_type: 'report_file',
|
||||
version_number: 1,
|
||||
status: 'failed',
|
||||
error: 'Report creation failed during streaming',
|
||||
file: {
|
||||
text: content,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
try {
|
||||
// Create initial version history for the report
|
||||
const now = new Date().toISOString();
|
||||
|
@ -217,23 +195,17 @@ export function createCreateReportsExecute(
|
|||
}
|
||||
|
||||
// Update state to reflect successful update
|
||||
if (!state.files) {
|
||||
state.files = [];
|
||||
}
|
||||
if (!state.files[i]) {
|
||||
state.files[i] = {
|
||||
id: reportId,
|
||||
file_name: name,
|
||||
file_type: 'report_file',
|
||||
version_number: 1,
|
||||
status: 'completed',
|
||||
};
|
||||
} else {
|
||||
const stateFile = state.files[i];
|
||||
if (stateFile) {
|
||||
stateFile.status = 'completed';
|
||||
}
|
||||
}
|
||||
// Include content so rawLlmMessage can be created properly
|
||||
state.file = {
|
||||
id: reportId,
|
||||
file_name: name,
|
||||
file_type: 'report_file',
|
||||
version_number: 1,
|
||||
status: 'completed',
|
||||
file: {
|
||||
text: content,
|
||||
},
|
||||
};
|
||||
|
||||
console.info('[create-reports] Successfully updated report with complete content', {
|
||||
reportId,
|
||||
|
@ -249,100 +221,66 @@ export function createCreateReportsExecute(
|
|||
});
|
||||
|
||||
// Update state to reflect failure
|
||||
if (!state.files) {
|
||||
state.files = [];
|
||||
}
|
||||
if (!state.files[i]) {
|
||||
state.files[i] = {
|
||||
id: reportId,
|
||||
file_name: name,
|
||||
file_type: 'report_file',
|
||||
version_number: 1,
|
||||
status: 'failed',
|
||||
error: errorMessage,
|
||||
};
|
||||
} else {
|
||||
const stateFile = state.files[i];
|
||||
if (stateFile) {
|
||||
stateFile.status = 'failed';
|
||||
stateFile.error = errorMessage;
|
||||
}
|
||||
}
|
||||
state.file = {
|
||||
id: reportId,
|
||||
file_name: name,
|
||||
file_type: 'report_file',
|
||||
version_number: 1,
|
||||
status: 'failed',
|
||||
error: errorMessage,
|
||||
file: {
|
||||
text: content,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Get the results (after ensuring all reports are properly created)
|
||||
const result = await getReportCreationResults(input, context, state);
|
||||
|
||||
// Update state files with final results
|
||||
// Update state file with final results
|
||||
if (result && typeof result === 'object') {
|
||||
const typedResult = result as CreateReportsOutput;
|
||||
// Ensure state.files is initialized for safe mutations below
|
||||
state.files = state.files ?? [];
|
||||
|
||||
// Mark any remaining files as completed/failed based on result
|
||||
if (state.files) {
|
||||
state.files.forEach((stateFile) => {
|
||||
if (stateFile.status === 'loading') {
|
||||
// Check if this file is in the success list
|
||||
const isSuccess = typedResult.files?.some((f) => f.id === stateFile.id);
|
||||
stateFile.status = isSuccess ? 'completed' : 'failed';
|
||||
}
|
||||
});
|
||||
// Mark file as completed/failed based on result
|
||||
if (state.file && state.file.status === 'loading') {
|
||||
state.file.status = typedResult.file ? 'completed' : 'failed';
|
||||
}
|
||||
|
||||
// Update last entries if we have a messageId
|
||||
if (context.messageId) {
|
||||
try {
|
||||
const finalStatus = typedResult.failed_files?.length ? 'failed' : 'completed';
|
||||
const finalStatus = typedResult.error ? 'failed' : 'completed';
|
||||
const toolCallId = state.toolCallId || `tool-${Date.now()}`;
|
||||
|
||||
// Update state for final status
|
||||
if (state.files) {
|
||||
state.files.forEach((f) => {
|
||||
if (!f.status || f.status === 'loading') {
|
||||
f.status = finalStatus === 'failed' ? 'failed' : 'completed';
|
||||
}
|
||||
});
|
||||
if (state.file && (!state.file.status || state.file.status === 'loading')) {
|
||||
state.file.status = finalStatus;
|
||||
}
|
||||
|
||||
// Check which reports contain metrics to determine if they should be in responseMessages
|
||||
// Check if report should be in responseMessages
|
||||
const responseMessages: ChatMessageResponseMessage[] = [];
|
||||
|
||||
// Check each report file for metrics
|
||||
if (state.files && typedResult.files) {
|
||||
for (const resultFile of typedResult.files) {
|
||||
// Skip if response message was already created during delta
|
||||
if (state.responseMessagesCreated?.has(resultFile.id)) {
|
||||
continue;
|
||||
}
|
||||
// Check if report file exists and hasn't been added to response
|
||||
if (state.file && typedResult.file && !state.responseMessageCreated) {
|
||||
responseMessages.push({
|
||||
id: state.file.id,
|
||||
type: 'file' as const,
|
||||
file_type: 'report_file' as const,
|
||||
file_name: state.file.file_name || typedResult.file.name,
|
||||
version_number: state.file.version_number || 1,
|
||||
filter_version_id: null,
|
||||
metadata: [
|
||||
{
|
||||
status: 'completed' as const,
|
||||
message: 'Report created successfully',
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Find the corresponding state file
|
||||
const stateFile = state.files.find((f) => f.id === resultFile.id);
|
||||
if (stateFile) {
|
||||
responseMessages.push({
|
||||
id: stateFile.id,
|
||||
type: 'file' as const,
|
||||
file_type: 'report_file' as const,
|
||||
file_name: stateFile.file_name || resultFile.name,
|
||||
version_number: stateFile.version_number || 1,
|
||||
filter_version_id: null,
|
||||
metadata: [
|
||||
{
|
||||
status: 'completed' as const,
|
||||
message: 'Report created successfully',
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Track that we've created a response message for this report
|
||||
if (!state.responseMessagesCreated) {
|
||||
state.responseMessagesCreated = new Set<string>();
|
||||
}
|
||||
state.responseMessagesCreated.add(resultFile.id);
|
||||
}
|
||||
}
|
||||
// Track that we've created a response message for this report
|
||||
state.responseMessageCreated = true;
|
||||
}
|
||||
|
||||
const reasoningEntry = createCreateReportsReasoningEntry(state, toolCallId);
|
||||
|
@ -350,9 +288,7 @@ export function createCreateReportsExecute(
|
|||
const rawLlmResultEntry = createRawToolResultEntry(
|
||||
toolCallId,
|
||||
CREATE_REPORTS_TOOL_NAME,
|
||||
{
|
||||
files: state.files,
|
||||
}
|
||||
typedResult
|
||||
);
|
||||
|
||||
const updates: Parameters<typeof updateMessageEntries>[0] = {
|
||||
|
@ -378,8 +314,8 @@ export function createCreateReportsExecute(
|
|||
|
||||
console.info('[create-reports] Updated last entries with final results', {
|
||||
messageId: context.messageId,
|
||||
successCount: typedResult.files?.length || 0,
|
||||
failedCount: typedResult.failed_files?.length || 0,
|
||||
success: !!typedResult.file,
|
||||
failed: !!typedResult.error,
|
||||
reportsWithMetrics: responseMessages.length,
|
||||
});
|
||||
} catch (error) {
|
||||
|
@ -392,8 +328,8 @@ export function createCreateReportsExecute(
|
|||
const executionTime = Date.now() - startTime;
|
||||
console.info('[create-reports] Execution completed', {
|
||||
executionTime: `${executionTime}ms`,
|
||||
filesCreated: result?.files?.length || 0,
|
||||
filesFailed: result?.failed_files?.length || 0,
|
||||
fileCreated: !!result?.file,
|
||||
failed: !!result?.error,
|
||||
});
|
||||
|
||||
return result as CreateReportsOutput;
|
||||
|
@ -418,22 +354,22 @@ export function createCreateReportsExecute(
|
|||
if (context.messageId) {
|
||||
try {
|
||||
const toolCallId = state.toolCallId || `tool-${Date.now()}`;
|
||||
// Update state files to failed status with error message
|
||||
if (state.files) {
|
||||
state.files.forEach((f) => {
|
||||
f.status = 'failed';
|
||||
f.error = f.error || errorMessage;
|
||||
});
|
||||
// Update state file to failed status with error message
|
||||
if (state.file) {
|
||||
state.file.status = 'failed';
|
||||
state.file.error = state.file.error || errorMessage;
|
||||
}
|
||||
|
||||
const reasoningEntry = createCreateReportsReasoningEntry(state, toolCallId);
|
||||
const rawLlmMessage = createCreateReportsRawLlmMessageEntry(state, toolCallId);
|
||||
const errorResult: CreateReportsOutput = {
|
||||
message: `Failed to create report: ${errorMessage}`,
|
||||
error: errorMessage,
|
||||
};
|
||||
const rawLlmResultEntry = createRawToolResultEntry(
|
||||
toolCallId,
|
||||
CREATE_REPORTS_TOOL_NAME,
|
||||
{
|
||||
files: state.files,
|
||||
}
|
||||
errorResult
|
||||
);
|
||||
|
||||
const updates: Parameters<typeof updateMessageEntries>[0] = {
|
||||
|
@ -463,19 +399,9 @@ export function createCreateReportsExecute(
|
|||
}
|
||||
|
||||
// Return error information to the agent
|
||||
const failedFiles: Array<{ name: string; error: string }> = [];
|
||||
input.files.forEach((inputFile, index) => {
|
||||
const stateFile = state.files?.[index];
|
||||
failedFiles.push({
|
||||
name: inputFile.name,
|
||||
error: stateFile?.error || errorMessage,
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
message: `Failed to create reports: ${errorMessage}`,
|
||||
files: [],
|
||||
failed_files: failedFiles,
|
||||
message: `Failed to create report: ${errorMessage}`,
|
||||
error: state.file?.error || errorMessage,
|
||||
} as CreateReportsOutput;
|
||||
}
|
||||
},
|
||||
|
|
|
@ -18,27 +18,22 @@ export function createCreateReportsFinish(
|
|||
return async (options: { input: CreateReportsInput } & ToolCallOptions) => {
|
||||
const input = options.input;
|
||||
|
||||
// Process final input
|
||||
if (input.files) {
|
||||
// Initialize state files if needed
|
||||
if (!state.files) {
|
||||
state.files = [];
|
||||
}
|
||||
// Process final input for single report
|
||||
if (input.name && input.content) {
|
||||
// Initialize state file if needed
|
||||
const existingFile = state.file;
|
||||
|
||||
// Set final state for all files
|
||||
state.files = input.files.map((file, index) => {
|
||||
const existingFile = state.files?.[index];
|
||||
return {
|
||||
id: existingFile?.id || randomUUID(),
|
||||
file_name: file.name,
|
||||
file_type: 'report_file',
|
||||
version_number: existingFile?.version_number || 1,
|
||||
file: {
|
||||
text: file.content,
|
||||
},
|
||||
status: existingFile?.status || 'loading',
|
||||
};
|
||||
});
|
||||
// Set final state for the single report
|
||||
state.file = {
|
||||
id: existingFile?.id || randomUUID(),
|
||||
file_name: input.name,
|
||||
file_type: 'report_file',
|
||||
version_number: existingFile?.version_number || 1,
|
||||
file: {
|
||||
text: input.content,
|
||||
},
|
||||
status: existingFile?.status || 'loading',
|
||||
};
|
||||
}
|
||||
|
||||
// Update database with final state
|
||||
|
@ -65,7 +60,7 @@ export function createCreateReportsFinish(
|
|||
|
||||
console.info('[create-reports] Finished input processing', {
|
||||
messageId: context.messageId,
|
||||
fileCount: state.files?.length || 0,
|
||||
reportCreated: !!state.file,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[create-reports] Error updating entries on finish:', error);
|
||||
|
|
|
@ -6,8 +6,8 @@ export function createReportsStart(_context: CreateReportsContext, state: Create
|
|||
// Reset state for new tool call to prevent contamination from previous calls
|
||||
state.toolCallId = options.toolCallId;
|
||||
state.argsText = undefined;
|
||||
state.files = [];
|
||||
state.file = undefined;
|
||||
state.startTime = Date.now();
|
||||
state.responseMessagesCreated = new Set<string>();
|
||||
state.responseMessageCreated = false;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
Creates report files with markdown content. Reports are used to document findings, analysis results, and insights in a structured markdown format. **This tool supports creating multiple reports in a single call; prefer using bulk creation over creating reports one by one.**
|
||||
Creates a report file with markdown content. Reports are used to document findings, analysis results, and insights in a structured markdown format. **This tool creates one report per call.**
|
||||
|
||||
# REPORT TYPES & FLOWS
|
||||
|
||||
|
|
|
@ -9,7 +9,8 @@ import CREATE_REPORTS_TOOL_DESCRIPTION from './create-reports-tool-description.t
|
|||
|
||||
export const CREATE_REPORTS_TOOL_NAME = 'createReports';
|
||||
|
||||
const CreateReportsInputFileSchema = z.object({
|
||||
// Input schema for the create reports tool - now accepts a single report
|
||||
const CreateReportsInputSchema = z.object({
|
||||
name: z
|
||||
.string()
|
||||
.describe(
|
||||
|
@ -18,36 +19,21 @@ const CreateReportsInputFileSchema = z.object({
|
|||
content: z
|
||||
.string()
|
||||
.describe(
|
||||
'The markdown content for the report. Should be well-structured with headers, sections, and clear analysis. Multiple reports can be created in one call by providing multiple entries in the files array. **Prefer creating reports in bulk.**'
|
||||
'The markdown content for the report. Should be well-structured with headers, sections, and clear analysis.'
|
||||
),
|
||||
});
|
||||
|
||||
// Input schema for the create reports tool
|
||||
const CreateReportsInputSchema = z.object({
|
||||
files: z
|
||||
.array(CreateReportsInputFileSchema)
|
||||
.min(1)
|
||||
.describe(
|
||||
'List of report file parameters to create. Each report should contain comprehensive markdown content with analysis, findings, and recommendations.'
|
||||
),
|
||||
});
|
||||
|
||||
const CreateReportsOutputFileSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
version_number: z.number(),
|
||||
});
|
||||
|
||||
const CreateReportsOutputFailedFileSchema = z.object({
|
||||
name: z.string(),
|
||||
error: z.string(),
|
||||
});
|
||||
|
||||
// Output schema for the create reports tool
|
||||
// Output schema for the create reports tool - now returns a single report or error
|
||||
const CreateReportsOutputSchema = z.object({
|
||||
message: z.string(),
|
||||
files: z.array(CreateReportsOutputFileSchema),
|
||||
failed_files: z.array(CreateReportsOutputFailedFileSchema),
|
||||
file: z
|
||||
.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
version_number: z.number(),
|
||||
})
|
||||
.optional(),
|
||||
error: z.string().optional(),
|
||||
});
|
||||
|
||||
// Context schema for the create reports tool
|
||||
|
@ -75,19 +61,17 @@ const CreateReportStateFileSchema = z.object({
|
|||
const CreateReportsStateSchema = z.object({
|
||||
toolCallId: z.string().optional(),
|
||||
argsText: z.string().optional(),
|
||||
files: z.array(CreateReportStateFileSchema).optional(),
|
||||
file: CreateReportStateFileSchema.optional(), // Changed from array to single file
|
||||
startTime: z.number().optional(),
|
||||
initialEntriesCreated: z.boolean().optional(),
|
||||
responseMessagesCreated: z.set(z.string()).optional(),
|
||||
reportsModifiedInMessage: z.set(z.string()).optional(),
|
||||
responseMessageCreated: z.boolean().optional(), // Changed from set to boolean
|
||||
reportModifiedInMessage: z.boolean().optional(), // Changed from set to boolean
|
||||
});
|
||||
|
||||
// Export types
|
||||
export type CreateReportsInput = z.infer<typeof CreateReportsInputSchema>;
|
||||
export type CreateReportsOutput = z.infer<typeof CreateReportsOutputSchema>;
|
||||
export type CreateReportsContext = z.infer<typeof CreateReportsContextSchema>;
|
||||
export type CreateReportsOutputFile = z.infer<typeof CreateReportsOutputFileSchema>;
|
||||
export type CreateReportsOutputFailedFile = z.infer<typeof CreateReportsOutputFailedFileSchema>;
|
||||
export type CreateReportsState = z.infer<typeof CreateReportsStateSchema>;
|
||||
export type CreateReportStateFile = z.infer<typeof CreateReportStateFileSchema>;
|
||||
|
||||
|
@ -96,9 +80,9 @@ export function createCreateReportsTool(context: CreateReportsContext) {
|
|||
// Initialize state for streaming
|
||||
const state: CreateReportsState = {
|
||||
argsText: undefined,
|
||||
files: [],
|
||||
file: undefined,
|
||||
toolCallId: undefined,
|
||||
reportsModifiedInMessage: new Set(),
|
||||
reportModifiedInMessage: false,
|
||||
};
|
||||
|
||||
// Create all functions with the context and state passed
|
||||
|
|
|
@ -19,61 +19,44 @@ export function createCreateReportsReasoningEntry(
|
|||
): ChatMessageReasoningMessage | undefined {
|
||||
state.toolCallId = toolCallId;
|
||||
|
||||
if (!state.files || state.files.length === 0) {
|
||||
if (!state.file || !state.file.file_name) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Build Record<string, ReasoningFile> as required by schema
|
||||
const filesRecord: Record<string, ChatMessageReasoningMessage_File> = {};
|
||||
const fileIds: string[] = [];
|
||||
for (const f of state.files) {
|
||||
// Skip undefined entries or entries that do not yet have a file_name
|
||||
if (!f || !f.file_name) continue;
|
||||
|
||||
// Type assertion to ensure proper typing
|
||||
const file = f as CreateReportStateFile;
|
||||
const id = file.id;
|
||||
fileIds.push(id);
|
||||
filesRecord[id] = {
|
||||
id,
|
||||
file_type: 'report_file',
|
||||
file_name: file.file_name ?? '',
|
||||
version_number: file.version_number,
|
||||
status: file.status,
|
||||
file: {
|
||||
text: file.file?.text || '',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// If nothing valid to show yet, skip emitting a files reasoning message
|
||||
if (fileIds.length === 0) return undefined;
|
||||
// Type assertion to ensure proper typing
|
||||
const file = state.file as CreateReportStateFile;
|
||||
const id = file.id;
|
||||
fileIds.push(id);
|
||||
filesRecord[id] = {
|
||||
id,
|
||||
file_type: 'report_file',
|
||||
file_name: file.file_name ?? '',
|
||||
version_number: file.version_number,
|
||||
status: file.status,
|
||||
file: {
|
||||
text: file.file?.text || '',
|
||||
},
|
||||
};
|
||||
|
||||
// Calculate title and status based on completion state
|
||||
let title = 'Creating reports...';
|
||||
let title = 'Creating report...';
|
||||
let status: 'loading' | 'completed' | 'failed' = 'loading';
|
||||
|
||||
// Check if all files have been processed (state has completion status)
|
||||
const completedFiles = state.files.filter((f) => f?.status === 'completed').length;
|
||||
const failedFiles = state.files.filter((f) => f?.status === 'failed').length;
|
||||
const totalFiles = state.files.length;
|
||||
|
||||
// If all files have a final status, we're complete
|
||||
const isComplete = completedFiles + failedFiles === totalFiles && totalFiles > 0;
|
||||
if (isComplete) {
|
||||
if (failedFiles === 0) {
|
||||
title = `Created ${completedFiles} ${completedFiles === 1 ? 'report_file' : 'reports'}`;
|
||||
status = 'completed';
|
||||
} else if (completedFiles === 0) {
|
||||
title = `Failed to create ${failedFiles} ${failedFiles === 1 ? 'report_file' : 'reports'}`;
|
||||
status = 'failed';
|
||||
} else {
|
||||
title = `Created ${completedFiles} of ${totalFiles} reports`;
|
||||
status = 'failed'; // Partial success is considered failed
|
||||
}
|
||||
// Check if file has been processed (state has completion status)
|
||||
if (state.file.status === 'completed') {
|
||||
title = 'Created report_file';
|
||||
status = 'completed';
|
||||
} else if (state.file.status === 'failed') {
|
||||
title = 'Failed to create report_file';
|
||||
status = 'failed';
|
||||
}
|
||||
|
||||
// Calculate elapsed time if complete
|
||||
const isComplete = state.file.status === 'completed' || state.file.status === 'failed';
|
||||
const secondaryTitle = isComplete ? formatElapsedTime(state.startTime) : undefined;
|
||||
|
||||
return {
|
||||
|
@ -94,8 +77,15 @@ export function createCreateReportsRawLlmMessageEntry(
|
|||
state: CreateReportsState,
|
||||
toolCallId: string
|
||||
): ModelMessage | undefined {
|
||||
// If we don't have files yet, skip emitting raw LLM entry
|
||||
if (!state.files || state.files.length === 0) return undefined;
|
||||
// If we don't have a file yet, skip emitting raw LLM entry
|
||||
if (!state.file) return undefined;
|
||||
|
||||
const typedFile = state.file as CreateReportStateFile;
|
||||
|
||||
// Only emit if we have valid name and content
|
||||
if (!typedFile.file_name || !typedFile.file?.text) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
role: 'assistant',
|
||||
|
@ -105,17 +95,8 @@ export function createCreateReportsRawLlmMessageEntry(
|
|||
toolCallId,
|
||||
toolName: CREATE_REPORTS_TOOL_NAME,
|
||||
input: {
|
||||
files: state.files
|
||||
.filter((file) => file != null) // Filter out null/undefined entries first
|
||||
.map((file) => {
|
||||
const typedFile = file as CreateReportStateFile;
|
||||
return {
|
||||
name: typedFile.file_name ?? '',
|
||||
yml_content: typedFile.file?.text ?? '',
|
||||
};
|
||||
})
|
||||
// Filter out clearly invalid entries
|
||||
.filter((f) => f.name && f.yml_content),
|
||||
name: typedFile.file_name,
|
||||
content: typedFile.file.text,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
Loading…
Reference in New Issue