fix(ai) - workflow tool outputs optim + display fix (#21500)
<!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21500?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
This commit is contained in:
@@ -198,6 +198,8 @@ export const useAgentChatSubscription = (threadId: string | null) => {
|
||||
const startReadLoop = async (readable: ReadableStream<UIMessageChunk>) => {
|
||||
const messageStream = readUIMessageStream({ stream: readable });
|
||||
|
||||
let lastUsageCountedMessageId: string | null = null;
|
||||
|
||||
for await (const message of messageStream) {
|
||||
const extendedMessage = message as ExtendedUIMessage;
|
||||
|
||||
@@ -225,7 +227,13 @@ export const useAgentChatSubscription = (threadId: string | null) => {
|
||||
}
|
||||
| undefined;
|
||||
|
||||
if (isDefined(metadata?.usage) && isDefined(metadata?.model)) {
|
||||
if (
|
||||
isDefined(metadata?.usage) &&
|
||||
isDefined(metadata?.model) &&
|
||||
lastUsageCountedMessageId !== extendedMessage.id
|
||||
) {
|
||||
lastUsageCountedMessageId = extendedMessage.id;
|
||||
|
||||
const usage = metadata.usage;
|
||||
const model = metadata.model;
|
||||
|
||||
|
||||
+6
-2
@@ -74,9 +74,13 @@ Always rely on tool schema definitions:
|
||||
- Follow schema definitions exactly for field names, types, and structures
|
||||
- Schema includes validation rules and common patterns
|
||||
|
||||
## Validation
|
||||
## Validation Strategy
|
||||
|
||||
The \`create_complete_workflow\` and \`update_workflow_version_step\` tools automatically run validation after their operation and include the results in the response. Review any reported errors and fix them before activating the workflow.
|
||||
Build steps fully configured up front so the workflow is correct on the first try. Mutation tools (\`create_complete_workflow\`, \`update_workflow_version_step\`) return a compact validation summary (error codes, messages, suggestions) — fix any reported errors.
|
||||
|
||||
Do NOT call \`validate_workflow\` after every change:
|
||||
- When making several step edits in a row, pass \`validate: false\` to \`update_workflow_version_step\` to skip per-edit validation.
|
||||
- Call \`validate_workflow\` exactly ONCE at the end, before activating. It returns the full report including warnings and available variable paths.
|
||||
|
||||
|
||||
## Approach
|
||||
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
import { createUpdateWorkflowVersionStepTool } from 'src/modules/workflow/workflow-tools/tools/update-workflow-version-step.tool';
|
||||
|
||||
const mockStep = {
|
||||
id: 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
|
||||
name: 'Send email',
|
||||
type: 'SEND_EMAIL',
|
||||
valid: true,
|
||||
settings: { input: {} },
|
||||
};
|
||||
|
||||
const buildTool = ({
|
||||
validationResult = { valid: true, errors: [], warnings: [] },
|
||||
}: {
|
||||
validationResult?: object;
|
||||
} = {}) => {
|
||||
const workflowVersionStepService = {
|
||||
updateWorkflowVersionStep: jest.fn().mockResolvedValue(mockStep),
|
||||
};
|
||||
const workflowValidationService = {
|
||||
validateWorkflowVersion: jest.fn().mockResolvedValue(validationResult),
|
||||
};
|
||||
|
||||
const tool = createUpdateWorkflowVersionStepTool(
|
||||
{
|
||||
workflowVersionStepService,
|
||||
workflowValidationService,
|
||||
} as never,
|
||||
{ workspaceId: 'workspace-id' },
|
||||
);
|
||||
|
||||
return { tool, workflowVersionStepService, workflowValidationService };
|
||||
};
|
||||
|
||||
const baseInput = {
|
||||
workflowVersionId: 'b3b8a4f0-0000-4000-8000-000000000000',
|
||||
step: mockStep,
|
||||
} as unknown as Parameters<
|
||||
ReturnType<typeof createUpdateWorkflowVersionStepTool>['execute']
|
||||
>[0];
|
||||
|
||||
describe('createUpdateWorkflowVersionStepTool', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should validate by default and return a compact summary', async () => {
|
||||
const { tool, workflowValidationService } = buildTool({
|
||||
validationResult: {
|
||||
valid: false,
|
||||
errors: [
|
||||
{
|
||||
severity: 'error',
|
||||
code: 'DANGLING_REFERENCE',
|
||||
message: 'Unknown variable',
|
||||
availablePaths: ['{{trigger.x}}'],
|
||||
},
|
||||
],
|
||||
warnings: [{ severity: 'warning', code: 'NO_STEPS', message: 'w' }],
|
||||
},
|
||||
});
|
||||
|
||||
const result = (await tool.execute(baseInput)) as Record<string, unknown>;
|
||||
|
||||
expect(
|
||||
workflowValidationService.validateWorkflowVersion,
|
||||
).toHaveBeenCalled();
|
||||
|
||||
const validation = result.validation as Record<string, unknown>;
|
||||
|
||||
expect(validation.valid).toBe(false);
|
||||
expect(validation.errorCount).toBe(1);
|
||||
expect(validation.warningCount).toBe(1);
|
||||
expect(validation).not.toHaveProperty('warnings');
|
||||
expect((validation.errors as object[])[0]).not.toHaveProperty(
|
||||
'availablePaths',
|
||||
);
|
||||
});
|
||||
|
||||
it('should skip validation entirely when validate is false', async () => {
|
||||
const { tool, workflowValidationService } = buildTool();
|
||||
|
||||
const result = (await tool.execute({
|
||||
...baseInput,
|
||||
validate: false,
|
||||
})) as Record<string, unknown>;
|
||||
|
||||
expect(
|
||||
workflowValidationService.validateWorkflowVersion,
|
||||
).not.toHaveBeenCalled();
|
||||
expect(result).not.toHaveProperty('validation');
|
||||
});
|
||||
|
||||
it('should still return the step result when validation throws', async () => {
|
||||
const { tool, workflowValidationService } = buildTool();
|
||||
|
||||
workflowValidationService.validateWorkflowVersion.mockRejectedValue(
|
||||
new Error('boom'),
|
||||
);
|
||||
|
||||
const result = (await tool.execute(baseInput)) as Record<string, unknown>;
|
||||
|
||||
expect(result.validationError).toBe('boom');
|
||||
expect(result).not.toHaveProperty('validation');
|
||||
});
|
||||
});
|
||||
+6
-4
@@ -18,6 +18,7 @@ import {
|
||||
type WorkflowToolContext,
|
||||
type WorkflowToolDependencies,
|
||||
} from 'src/modules/workflow/workflow-tools/types/workflow-tool-dependencies.type';
|
||||
import { summarizeValidation } from 'src/modules/workflow/workflow-tools/utils/summarize-validation.util';
|
||||
import { type WorkflowTrigger } from 'src/modules/workflow/workflow-trigger/types/workflow-trigger.type';
|
||||
|
||||
const createCompleteWorkflowSchema = z.object({
|
||||
@@ -106,7 +107,9 @@ IMPORTANT: The tool schema provides comprehensive field descriptions, examples,
|
||||
- Variable reference syntax: {{trigger.fieldName}} for trigger data, {{<step-id>.result.fieldName}} for step outputs (step-id is the step's UUID, not its name)
|
||||
- Error handling options
|
||||
|
||||
This is the most efficient way for AI to create workflows as it handles all the complexity in one call.`,
|
||||
This is the most efficient way for AI to create workflows as it handles all the complexity in one call.
|
||||
|
||||
The response includes a compact validation summary. For the full validation report with available variable paths, call validate_workflow once after your edits — not after every change.`,
|
||||
inputSchema: createCompleteWorkflowSchema,
|
||||
execute: async (parameters: {
|
||||
name: string;
|
||||
@@ -197,9 +200,8 @@ This is the most efficient way for AI to create workflows as it handles all the
|
||||
workflowId,
|
||||
workflowVersionId,
|
||||
name: parameters.name,
|
||||
trigger: parameters.trigger,
|
||||
steps: parameters.steps,
|
||||
validation,
|
||||
stepIds: parameters.steps.map((step) => step.id),
|
||||
validation: summarizeValidation(validation),
|
||||
},
|
||||
recordReferences: [
|
||||
{
|
||||
|
||||
+20
-3
@@ -6,6 +6,7 @@ import {
|
||||
type WorkflowToolContext,
|
||||
type WorkflowToolDependencies,
|
||||
} from 'src/modules/workflow/workflow-tools/types/workflow-tool-dependencies.type';
|
||||
import { summarizeValidation } from 'src/modules/workflow/workflow-tools/utils/summarize-validation.util';
|
||||
|
||||
const updateWorkflowVersionStepSchema = z.object({
|
||||
workflowVersionId: z
|
||||
@@ -15,8 +16,19 @@ const updateWorkflowVersionStepSchema = z.object({
|
||||
step: z
|
||||
.union([workflowActionSchema])
|
||||
.describe('The updated step configuration'),
|
||||
validate: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(true)
|
||||
.describe(
|
||||
'Run a quick validation and return a compact summary (default true). Set to false when making several edits in a row, then call validate_workflow once at the end instead.',
|
||||
),
|
||||
});
|
||||
|
||||
type UpdateWorkflowVersionStepToolInput = UpdateWorkflowVersionStepInput & {
|
||||
validate?: boolean;
|
||||
};
|
||||
|
||||
export const createUpdateWorkflowVersionStepTool = (
|
||||
deps: Pick<
|
||||
WorkflowToolDependencies,
|
||||
@@ -26,9 +38,9 @@ export const createUpdateWorkflowVersionStepTool = (
|
||||
) => ({
|
||||
name: 'update_workflow_version_step' as const,
|
||||
description:
|
||||
'Update an existing step in a workflow version. This modifies the step configuration.',
|
||||
'Update an existing step in a workflow version. This modifies the step configuration. Returns a compact validation summary; for the full report with available variable paths, call validate_workflow once after your edits — not after every change.',
|
||||
inputSchema: updateWorkflowVersionStepSchema,
|
||||
execute: async (parameters: UpdateWorkflowVersionStepInput) => {
|
||||
execute: async (parameters: UpdateWorkflowVersionStepToolInput) => {
|
||||
let result;
|
||||
|
||||
try {
|
||||
@@ -45,15 +57,20 @@ export const createUpdateWorkflowVersionStepTool = (
|
||||
};
|
||||
}
|
||||
|
||||
if (parameters.validate === false) {
|
||||
return result;
|
||||
}
|
||||
|
||||
try {
|
||||
const validation =
|
||||
await deps.workflowValidationService.validateWorkflowVersion({
|
||||
workspaceId: context.workspaceId,
|
||||
workflowVersionId: parameters.workflowVersionId,
|
||||
});
|
||||
|
||||
return {
|
||||
...result,
|
||||
validation,
|
||||
validation: summarizeValidation(validation),
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
import { type WorkflowValidationResult } from 'twenty-shared/workflow';
|
||||
|
||||
import { summarizeValidation } from 'src/modules/workflow/workflow-tools/utils/summarize-validation.util';
|
||||
|
||||
describe('summarizeValidation', () => {
|
||||
it('should return a minimal summary for a valid workflow', () => {
|
||||
const result: WorkflowValidationResult = {
|
||||
valid: true,
|
||||
errors: [],
|
||||
warnings: [],
|
||||
};
|
||||
|
||||
expect(summarizeValidation(result)).toEqual({
|
||||
valid: true,
|
||||
errorCount: 0,
|
||||
warningCount: 0,
|
||||
errors: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('should keep error essentials and drop availablePaths and hint', () => {
|
||||
const result: WorkflowValidationResult = {
|
||||
valid: false,
|
||||
errors: [
|
||||
{
|
||||
severity: 'error',
|
||||
code: 'DANGLING_REFERENCE',
|
||||
message: 'Unknown variable {{step-1.foo}}',
|
||||
stepId: 'step-2',
|
||||
path: 'settings.input.body',
|
||||
hint: 'some long hint',
|
||||
suggestions: ['{{step-1.bar}}'],
|
||||
availablePaths: ['{{step-1.bar}}', '{{step-1.baz}}', '{{trigger.x}}'],
|
||||
},
|
||||
],
|
||||
warnings: [],
|
||||
};
|
||||
|
||||
const summary = summarizeValidation(result);
|
||||
|
||||
expect(summary.valid).toBe(false);
|
||||
expect(summary.errorCount).toBe(1);
|
||||
expect(summary.errors[0]).toEqual({
|
||||
code: 'DANGLING_REFERENCE',
|
||||
message: 'Unknown variable {{step-1.foo}}',
|
||||
stepId: 'step-2',
|
||||
path: 'settings.input.body',
|
||||
suggestions: ['{{step-1.bar}}'],
|
||||
});
|
||||
expect(summary.errors[0]).not.toHaveProperty('availablePaths');
|
||||
expect(summary.errors[0]).not.toHaveProperty('hint');
|
||||
});
|
||||
|
||||
it('should collapse warnings to a count', () => {
|
||||
const result: WorkflowValidationResult = {
|
||||
valid: true,
|
||||
errors: [],
|
||||
warnings: [
|
||||
{
|
||||
severity: 'warning',
|
||||
code: 'UNREACHABLE_STEP',
|
||||
message: 'Step is unreachable',
|
||||
},
|
||||
{
|
||||
severity: 'warning',
|
||||
code: 'TRIGGER_HAS_NO_NEXT_STEP',
|
||||
message: 'Trigger has no next step',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const summary = summarizeValidation(result);
|
||||
|
||||
expect(summary.warningCount).toBe(2);
|
||||
expect(summary).not.toHaveProperty('warnings');
|
||||
expect(summary.hint).toContain('validate_workflow');
|
||||
});
|
||||
|
||||
it('should omit empty suggestions', () => {
|
||||
const result: WorkflowValidationResult = {
|
||||
valid: false,
|
||||
errors: [
|
||||
{
|
||||
severity: 'error',
|
||||
code: 'NO_STEPS',
|
||||
message: 'The workflow has no steps.',
|
||||
suggestions: [],
|
||||
},
|
||||
],
|
||||
warnings: [],
|
||||
};
|
||||
|
||||
expect(summarizeValidation(result).errors[0]).not.toHaveProperty(
|
||||
'suggestions',
|
||||
);
|
||||
});
|
||||
});
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
import {
|
||||
type WorkflowValidationIssue,
|
||||
type WorkflowValidationResult,
|
||||
} from 'twenty-shared/workflow';
|
||||
|
||||
export type WorkflowValidationSummary = {
|
||||
valid: boolean;
|
||||
errorCount: number;
|
||||
warningCount: number;
|
||||
errors: Array<
|
||||
Pick<
|
||||
WorkflowValidationIssue,
|
||||
'code' | 'stepId' | 'path' | 'message' | 'suggestions'
|
||||
>
|
||||
>;
|
||||
hint?: string;
|
||||
};
|
||||
|
||||
export const summarizeValidation = (
|
||||
result: WorkflowValidationResult,
|
||||
): WorkflowValidationSummary => {
|
||||
const summary: WorkflowValidationSummary = {
|
||||
valid: result.valid,
|
||||
errorCount: result.errors.length,
|
||||
warningCount: result.warnings.length,
|
||||
errors: result.errors.map(
|
||||
({ code, stepId, path, message, suggestions }) => ({
|
||||
code,
|
||||
stepId,
|
||||
path,
|
||||
message,
|
||||
...(suggestions && suggestions.length > 0 ? { suggestions } : {}),
|
||||
}),
|
||||
),
|
||||
};
|
||||
|
||||
if (!result.valid || result.warnings.length > 0) {
|
||||
summary.hint =
|
||||
'Compact summary. For the full report including warnings and available variable paths, call validate_workflow.';
|
||||
}
|
||||
|
||||
return summary;
|
||||
};
|
||||
Reference in New Issue
Block a user