Infer array current item schema (#15115)
This PR allows to infer the schema of the current item of an iterator step: - iterator step receive a variable - added an util that navigate to the array in schema - navigateOutputSchemaProperty - use the array value in schema to generate a new schema - used the existing getFunctionOutputSchema that I renamed and moved to twenty-shared Also cleaned a bit the existing schema for AI. Before https://github.com/user-attachments/assets/9767fc89-3524-4bfb-b1ab-8abe92084767 After https://github.com/user-attachments/assets/3650c1d2-14f2-44f9-b10c-e649fe04128d
This commit is contained in:
+106
@@ -0,0 +1,106 @@
|
||||
import { buildOutputSchemaFromValue } from '../buildOutputSchemaFromValue';
|
||||
|
||||
describe('buildOutputSchemaFromValue', () => {
|
||||
it('should compute outputSchema properly for mixed types', () => {
|
||||
const testResult = {
|
||||
a: null,
|
||||
b: 'b',
|
||||
c: { cc: 1 },
|
||||
d: true,
|
||||
e: [1, 2, 3],
|
||||
};
|
||||
const expectedOutputSchema = {
|
||||
a: {
|
||||
isLeaf: true,
|
||||
type: 'unknown',
|
||||
value: null,
|
||||
label: 'a',
|
||||
},
|
||||
b: {
|
||||
isLeaf: true,
|
||||
type: 'string',
|
||||
value: 'b',
|
||||
label: 'b',
|
||||
},
|
||||
c: {
|
||||
isLeaf: false,
|
||||
type: 'object',
|
||||
label: 'c',
|
||||
value: {
|
||||
cc: {
|
||||
isLeaf: true,
|
||||
type: 'number',
|
||||
value: 1,
|
||||
label: 'cc',
|
||||
},
|
||||
},
|
||||
},
|
||||
d: {
|
||||
isLeaf: true,
|
||||
type: 'boolean',
|
||||
value: true,
|
||||
label: 'd',
|
||||
},
|
||||
e: {
|
||||
isLeaf: true,
|
||||
type: 'array',
|
||||
value: [1, 2, 3],
|
||||
label: 'e',
|
||||
},
|
||||
};
|
||||
expect(buildOutputSchemaFromValue(testResult)).toEqual(
|
||||
expectedOutputSchema,
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle nested objects', () => {
|
||||
const testResult = {
|
||||
user: {
|
||||
name: 'John',
|
||||
address: {
|
||||
street: '123 Main St',
|
||||
city: 'New York',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = buildOutputSchemaFromValue(testResult);
|
||||
|
||||
expect(result.user).toEqual({
|
||||
isLeaf: false,
|
||||
type: 'object',
|
||||
label: 'user',
|
||||
value: {
|
||||
name: {
|
||||
isLeaf: true,
|
||||
type: 'string',
|
||||
value: 'John',
|
||||
label: 'name',
|
||||
},
|
||||
address: {
|
||||
isLeaf: false,
|
||||
type: 'object',
|
||||
label: 'address',
|
||||
value: {
|
||||
street: {
|
||||
isLeaf: true,
|
||||
type: 'string',
|
||||
value: '123 Main St',
|
||||
label: 'street',
|
||||
},
|
||||
city: {
|
||||
isLeaf: true,
|
||||
type: 'string',
|
||||
value: 'New York',
|
||||
label: 'city',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return empty object for empty input', () => {
|
||||
expect(buildOutputSchemaFromValue({})).toEqual({});
|
||||
});
|
||||
});
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
import { type BaseOutputSchemaV2 } from '../../types/base-output-schema.type';
|
||||
import { navigateOutputSchemaProperty } from '../navigateOutputSchemaProperty';
|
||||
|
||||
describe('navigateOutputSchemaProperty', () => {
|
||||
const testSchema: BaseOutputSchemaV2 = {
|
||||
result: {
|
||||
isLeaf: false,
|
||||
type: 'object',
|
||||
label: 'result',
|
||||
value: {
|
||||
items: {
|
||||
isLeaf: true,
|
||||
type: 'array',
|
||||
label: 'items',
|
||||
value: [
|
||||
{ id: 1, name: 'Item 1' },
|
||||
{ id: 2, name: 'Item 2' },
|
||||
],
|
||||
},
|
||||
metadata: {
|
||||
isLeaf: false,
|
||||
type: 'object',
|
||||
label: 'metadata',
|
||||
value: {
|
||||
count: {
|
||||
isLeaf: true,
|
||||
type: 'number',
|
||||
label: 'count',
|
||||
value: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
name: {
|
||||
isLeaf: true,
|
||||
type: 'string',
|
||||
label: 'name',
|
||||
value: 'Test',
|
||||
},
|
||||
};
|
||||
|
||||
it('should navigate to direct property', () => {
|
||||
const result = navigateOutputSchemaProperty({
|
||||
schema: testSchema,
|
||||
propertyPath: ['name'],
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
isLeaf: true,
|
||||
type: 'string',
|
||||
label: 'name',
|
||||
value: 'Test',
|
||||
});
|
||||
});
|
||||
|
||||
it('should navigate to nested property', () => {
|
||||
const result = navigateOutputSchemaProperty({
|
||||
schema: testSchema,
|
||||
propertyPath: ['result', 'items'],
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
isLeaf: true,
|
||||
type: 'array',
|
||||
label: 'items',
|
||||
value: [
|
||||
{ id: 1, name: 'Item 1' },
|
||||
{ id: 2, name: 'Item 2' },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should navigate to deeply nested property', () => {
|
||||
const result = navigateOutputSchemaProperty({
|
||||
schema: testSchema,
|
||||
propertyPath: ['result', 'metadata', 'count'],
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
isLeaf: true,
|
||||
type: 'number',
|
||||
label: 'count',
|
||||
value: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return undefined for missing property', () => {
|
||||
const result = navigateOutputSchemaProperty({
|
||||
schema: testSchema,
|
||||
propertyPath: ['nonexistent'],
|
||||
});
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return undefined for missing nested property', () => {
|
||||
const result = navigateOutputSchemaProperty({
|
||||
schema: testSchema,
|
||||
propertyPath: ['result', 'nonexistent'],
|
||||
});
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return undefined for empty property path', () => {
|
||||
const result = navigateOutputSchemaProperty({
|
||||
schema: testSchema,
|
||||
propertyPath: [],
|
||||
});
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return undefined when trying to navigate through a leaf', () => {
|
||||
const result = navigateOutputSchemaProperty({
|
||||
schema: testSchema,
|
||||
propertyPath: ['name', 'invalid'],
|
||||
});
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
import { isDefined } from '@/utils';
|
||||
import {
|
||||
type BaseOutputSchemaV2,
|
||||
type LeafType,
|
||||
} from '@/workflow/workflow-schema/types/base-output-schema.type';
|
||||
import { isObject } from '@sniptt/guards';
|
||||
|
||||
const getValueType = (value: any): LeafType => {
|
||||
if (!isDefined(value) || value === null) {
|
||||
return 'unknown';
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
return 'string';
|
||||
}
|
||||
if (typeof value === 'number') {
|
||||
return 'number';
|
||||
}
|
||||
if (typeof value === 'boolean') {
|
||||
return 'boolean';
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return 'array';
|
||||
}
|
||||
return 'unknown';
|
||||
};
|
||||
|
||||
export const buildOutputSchemaFromValue = (
|
||||
testResult: object,
|
||||
): BaseOutputSchemaV2 => {
|
||||
return testResult
|
||||
? Object.entries(testResult).reduce(
|
||||
(acc: BaseOutputSchemaV2, [key, value]) => {
|
||||
if (isObject(value) && !Array.isArray(value)) {
|
||||
acc[key] = {
|
||||
isLeaf: false,
|
||||
type: 'object',
|
||||
label: key,
|
||||
value: buildOutputSchemaFromValue(value),
|
||||
};
|
||||
} else {
|
||||
acc[key] = {
|
||||
isLeaf: true,
|
||||
value,
|
||||
type: getValueType(value),
|
||||
label: key,
|
||||
};
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
)
|
||||
: {};
|
||||
};
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
import { isDefined } from '@/utils';
|
||||
import {
|
||||
type BaseOutputSchemaV2,
|
||||
type Leaf,
|
||||
type Node,
|
||||
} from '@/workflow/workflow-schema/types/base-output-schema.type';
|
||||
|
||||
export const navigateOutputSchemaProperty = ({
|
||||
schema,
|
||||
propertyPath,
|
||||
}: {
|
||||
schema: BaseOutputSchemaV2;
|
||||
propertyPath: string[];
|
||||
}): Leaf | Node | undefined => {
|
||||
if (propertyPath.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let currentSchema: BaseOutputSchemaV2 = schema;
|
||||
let currentField: Leaf | Node | undefined;
|
||||
|
||||
for (const pathSegment of propertyPath) {
|
||||
currentField = currentSchema[pathSegment];
|
||||
|
||||
if (!isDefined(currentField)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (currentField.isLeaf) {
|
||||
const isLastSegment =
|
||||
pathSegment === propertyPath[propertyPath.length - 1];
|
||||
return isLastSegment ? currentField : undefined;
|
||||
}
|
||||
|
||||
currentSchema = currentField.value;
|
||||
}
|
||||
|
||||
return currentField;
|
||||
};
|
||||
Reference in New Issue
Block a user