Skip to content

Commit c8131c8

Browse files
committed
move and split input schema properties transform logic
1 parent 56dcfb9 commit c8131c8

File tree

4 files changed

+177
-21
lines changed

4 files changed

+177
-21
lines changed

src/tools/actor.ts

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,9 @@ import type { ProgressTracker } from '../utils/progress.js';
2323
import { getActorDefinition } from './build.js';
2424
import {
2525
actorNameToToolName,
26-
addEnumsToDescriptionsWithExamples,
27-
buildNestedProperties,
28-
encodeDotPropertyNames,
29-
filterSchemaProperties,
3026
fixedAjvCompile,
3127
getToolSchemaID,
32-
markInputPropertiesAsRequired,
33-
shortenProperties,
28+
transformActorInputSchemaProperties,
3429
} from './utils.js';
3530

3631
const ajv = new Ajv({ coerceTypes: 'array', strict: false });
@@ -136,12 +131,7 @@ export async function getNormalActorsAsTools(
136131
if (actorDefinitionPruned) {
137132
const schemaID = getToolSchemaID(actorDefinitionPruned.actorFullName);
138133
if (actorDefinitionPruned.input && 'properties' in actorDefinitionPruned.input && actorDefinitionPruned.input) {
139-
actorDefinitionPruned.input.properties = markInputPropertiesAsRequired(actorDefinitionPruned.input);
140-
actorDefinitionPruned.input.properties = buildNestedProperties(actorDefinitionPruned.input.properties);
141-
actorDefinitionPruned.input.properties = filterSchemaProperties(actorDefinitionPruned.input.properties);
142-
actorDefinitionPruned.input.properties = shortenProperties(actorDefinitionPruned.input.properties);
143-
actorDefinitionPruned.input.properties = addEnumsToDescriptionsWithExamples(actorDefinitionPruned.input.properties);
144-
actorDefinitionPruned.input.properties = encodeDotPropertyNames(actorDefinitionPruned.input.properties);
134+
actorDefinitionPruned.input.properties = transformActorInputSchemaProperties(actorDefinitionPruned.input);
145135
// Add schema $id, each valid JSON schema should have a unique $id
146136
// see https://json-schema.org/understanding-json-schema/basics#declaring-a-unique-identifier
147137
actorDefinitionPruned.input.$id = schemaID;

src/tools/utils.ts

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { ValidateFunction } from 'ajv';
22
import type Ajv from 'ajv';
33

44
import { ACTOR_ENUM_MAX_LENGTH, ACTOR_MAX_DESCRIPTION_LENGTH } from '../const.js';
5-
import type { IActorInputSchema, ISchemaProperties } from '../types.js';
5+
import type { ActorInputSchemaProperties, IActorInputSchema, ISchemaProperties } from '../types.js';
66

77
export function actorNameToToolName(actorName: string): string {
88
return actorName
@@ -97,6 +97,12 @@ export function buildNestedProperties(properties: Record<string, ISchemaProperti
9797
*
9898
* @param properties
9999
*/
100+
101+
/**
102+
* Filters schema properties to include only the necessary fields.
103+
* This is done to reduce the size of the input schema and to make it more readable.
104+
* @param properties
105+
*/
100106
export function filterSchemaProperties(properties: { [key: string]: ISchemaProperties }): {
101107
[key: string]: ISchemaProperties
102108
} {
@@ -113,19 +119,31 @@ export function filterSchemaProperties(properties: { [key: string]: ISchemaPrope
113119
items: property.items,
114120
required: property.required,
115121
};
122+
}
123+
return filteredProperties;
124+
}
125+
126+
/**
127+
* For array properties missing items.type, infers and sets the type using inferArrayItemType.
128+
* @param properties
129+
*/
130+
export function inferArrayItemsTypeIfMissing(properties: { [key: string]: ISchemaProperties }): {
131+
[key: string]: ISchemaProperties
132+
} {
133+
for (const [, property] of Object.entries(properties)) {
116134
if (property.type === 'array' && !property.items?.type) {
117135
const itemsType = inferArrayItemType(property);
118136
if (itemsType) {
119-
filteredProperties[key].items = {
120-
...filteredProperties[key].items,
121-
title: filteredProperties[key].title ?? 'Item',
122-
description: filteredProperties[key].description ?? 'Item',
137+
property.items = {
138+
...property.items,
139+
title: property.title ?? 'Item',
140+
description: property.description ?? 'Item',
123141
type: itemsType,
124142
};
125143
}
126144
}
127145
}
128-
return filteredProperties;
146+
return properties;
129147
}
130148

131149
/**
@@ -279,3 +297,16 @@ export function decodeDotPropertyNames(properties: Record<string, unknown>): Rec
279297
}
280298
return decodedProperties;
281299
}
300+
301+
export function transformActorInputSchemaProperties(input: IActorInputSchema): ActorInputSchemaProperties {
302+
// Deep clone input to avoid mutating the original object
303+
const inputClone: IActorInputSchema = JSON.parse(JSON.stringify(input));
304+
let transformedProperties = markInputPropertiesAsRequired(inputClone);
305+
transformedProperties = buildNestedProperties(transformedProperties);
306+
transformedProperties = filterSchemaProperties(transformedProperties);
307+
transformedProperties = inferArrayItemsTypeIfMissing(transformedProperties);
308+
transformedProperties = shortenProperties(transformedProperties);
309+
transformedProperties = addEnumsToDescriptionsWithExamples(transformedProperties);
310+
transformedProperties = encodeDotPropertyNames(transformedProperties);
311+
return transformedProperties;
312+
}

src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,3 +286,5 @@ export type PromptBase = Prompt & {
286286
*/
287287
render: (args: Record<string, string>) => string;
288288
};
289+
290+
export type ActorInputSchemaProperties = Record<string, ISchemaProperties>;

tests/unit/tools.utils.test.ts

Lines changed: 136 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import { describe, expect, it } from 'vitest';
22

33
import { ACTOR_ENUM_MAX_LENGTH, ACTOR_MAX_DESCRIPTION_LENGTH } from '../../src/const.js';
44
import { buildNestedProperties, decodeDotPropertyNames, encodeDotPropertyNames,
5-
markInputPropertiesAsRequired, shortenProperties } from '../../src/tools/utils.js';
5+
markInputPropertiesAsRequired, shortenProperties,
6+
transformActorInputSchemaProperties } from '../../src/tools/utils.js';
67
import type { IActorInputSchema, ISchemaProperties } from '../../src/types.js';
78

89
describe('buildNestedProperties', () => {
@@ -231,7 +232,13 @@ describe('shortenProperties', () => {
231232

232233
// Check that enum was shortened
233234
expect(result.prop1.enum).toBeDefined();
234-
expect(result.prop1.enum!.length).toBeLessThan(enumValues.length);
235+
if (result.prop1.enum) {
236+
expect(result.prop1.enum.length).toBeLessThan(30);
237+
const totalEnumLen = result.prop1.enum.reduce((sum, v) => sum + v.length, 0);
238+
expect(totalEnumLen).toBeLessThanOrEqual(ACTOR_ENUM_MAX_LENGTH);
239+
} else {
240+
expect(result.prop1.enum).toBeUndefined();
241+
}
235242

236243
// Calculate total character length of enum values
237244
const totalLength = result.prop1.enum!.reduce((sum, val) => sum + val.length, 0);
@@ -259,7 +266,13 @@ describe('shortenProperties', () => {
259266

260267
// Check that items.enum was shortened
261268
expect(result.prop1.items?.enum).toBeDefined();
262-
expect(result.prop1.items!.enum!.length).toBeLessThan(enumValues.length);
269+
if (result.prop1.items?.enum) {
270+
expect(result.prop1.items.enum.length).toBeLessThan(enumValues.length);
271+
const totalLength = result.prop1.items.enum.reduce((sum, val) => sum + val.length, 0);
272+
expect(totalLength).toBeLessThanOrEqual(ACTOR_ENUM_MAX_LENGTH);
273+
} else {
274+
expect(result.prop1.items?.enum).toBeUndefined();
275+
}
263276

264277
// Calculate total character length of enum values
265278
const totalLength = result.prop1.items!.enum!.reduce((sum, val) => sum + val.length, 0);
@@ -368,3 +381,123 @@ describe('decodeDotPropertyNames', () => {
368381
expect(result).toEqual(input);
369382
});
370383
});
384+
385+
// ----------------------
386+
// Tests for transformActorInputSchemaProperties
387+
// ----------------------
388+
describe('transformActorInputSchemaProperties', () => {
389+
it('should apply all transformations in the correct order', () => {
390+
const input = {
391+
title: 'Test',
392+
type: 'object',
393+
required: ['foo.bar', 'enumProp'],
394+
properties: {
395+
'foo.bar': {
396+
type: 'string',
397+
title: 'Foo Bar',
398+
description: 'desc',
399+
},
400+
proxy: {
401+
type: 'object',
402+
editor: 'proxy',
403+
title: 'Proxy',
404+
description: 'Proxy desc',
405+
properties: {},
406+
},
407+
sources: {
408+
type: 'array',
409+
editor: 'requestListSources',
410+
title: 'Sources',
411+
description: 'Sources desc',
412+
},
413+
enumProp: {
414+
type: 'string',
415+
title: 'Enum',
416+
description: 'Enum desc',
417+
enum: Array.from({ length: 30 }, (_, i) => `val${i}`),
418+
},
419+
longDesc: {
420+
type: 'string',
421+
title: 'Long',
422+
description: 'a'.repeat(ACTOR_MAX_DESCRIPTION_LENGTH + 10),
423+
},
424+
},
425+
};
426+
const result = transformActorInputSchemaProperties(input);
427+
// 1. markInputPropertiesAsRequired: required fields get **REQUIRED** in description
428+
expect(result['foo-dot-bar'].description).toContain('**REQUIRED**');
429+
expect(result.enumProp.description).toContain('**REQUIRED**');
430+
// 2. buildNestedProperties: proxy gets useApifyProxy, sources gets url
431+
expect(result.proxy.properties).toBeDefined();
432+
expect(result.proxy.properties?.useApifyProxy).toBeDefined();
433+
expect(result.sources.items).toBeDefined();
434+
expect(result.sources.items?.properties?.url).toBeDefined();
435+
// 3. filterSchemaProperties: only allowed fields present
436+
expect(Object.keys(result['foo-dot-bar'])).toEqual(
437+
expect.arrayContaining(['title', 'description', 'type', 'default', 'prefill', 'properties', 'items', 'required', 'enum']),
438+
);
439+
// 4. shortenProperties: longDesc is truncated, enumProp.enum is shortened
440+
expect(result.longDesc.description.length).toBeLessThanOrEqual(ACTOR_MAX_DESCRIPTION_LENGTH + 3);
441+
if (result.enumProp.enum) {
442+
expect(result.enumProp.enum.length).toBeLessThanOrEqual(30);
443+
const totalEnumLen = result.enumProp.enum.reduce((sum, v) => sum + v.length, 0);
444+
expect(totalEnumLen).toBeLessThanOrEqual(ACTOR_ENUM_MAX_LENGTH);
445+
} else {
446+
// If enum is too long, it may be set to undefined
447+
expect(result.enumProp.enum).toBeUndefined();
448+
}
449+
// 5. addEnumsToDescriptionsWithExamples: enum values in description
450+
expect(result.enumProp.description).toMatch(/Possible values:/);
451+
// 6. encodeDotPropertyNames: foo.bar becomes foo-dot-bar
452+
expect(result['foo-dot-bar']).toBeDefined();
453+
expect(result['foo.bar']).toBeUndefined();
454+
});
455+
456+
it('should handle input with no required, no enums, no dots', () => {
457+
const input = {
458+
title: 'Simple',
459+
type: 'object',
460+
properties: {
461+
simple: {
462+
type: 'string',
463+
title: 'Simple',
464+
description: 'desc',
465+
},
466+
},
467+
};
468+
const result = transformActorInputSchemaProperties(input);
469+
expect(result.simple.description).toBe('desc');
470+
expect(result.simple.enum).toBeUndefined();
471+
expect(result.simple).toBeDefined();
472+
});
473+
474+
it('should encode all dotted property names', () => {
475+
const input = {
476+
title: 'Dots',
477+
type: 'object',
478+
properties: {
479+
'a.b': { type: 'string', title: 'A B', description: 'desc' },
480+
'c.d.e': { type: 'number', title: 'CDE', description: 'desc2' },
481+
},
482+
};
483+
const result = transformActorInputSchemaProperties(input);
484+
expect(result['a-dot-b']).toBeDefined();
485+
expect(result['c-dot-d-dot-e']).toBeDefined();
486+
expect(result['a.b']).toBeUndefined();
487+
expect(result['c.d.e']).toBeUndefined();
488+
});
489+
490+
it('should not mutate the input object', () => {
491+
const input = {
492+
title: 'Immut',
493+
type: 'object',
494+
required: ['foo'],
495+
properties: {
496+
foo: { type: 'string', title: 'Foo', description: 'desc' },
497+
},
498+
};
499+
const inputCopy = JSON.parse(JSON.stringify(input));
500+
transformActorInputSchemaProperties(input);
501+
expect(input).toEqual(inputCopy);
502+
});
503+
});

0 commit comments

Comments
 (0)