Skip to content

Commit 3014adf

Browse files
committed
fix: modify schema after cache
1 parent 227823f commit 3014adf

File tree

4 files changed

+62
-49
lines changed

4 files changed

+62
-49
lines changed

src/tools/actor.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ export async function callActorGetDataset(
129129
*/
130130
export async function getNormalActorsAsTools(
131131
actorsInfo: ActorInfo[],
132+
fullActorSchema: boolean,
132133
): Promise<ToolEntry[]> {
133134
const tools: ToolEntry[] = [];
134135

@@ -139,7 +140,10 @@ export async function getNormalActorsAsTools(
139140
if (actorDefinitionPruned) {
140141
const schemaID = getToolSchemaID(actorDefinitionPruned.actorFullName);
141142
if (actorDefinitionPruned.input && 'properties' in actorDefinitionPruned.input && actorDefinitionPruned.input) {
142-
actorDefinitionPruned.input.properties = transformActorInputSchemaProperties(actorDefinitionPruned.input);
143+
actorDefinitionPruned.input.properties = transformActorInputSchemaProperties(
144+
actorDefinitionPruned.input,
145+
{ separateAdvancedInputs: !fullActorSchema },
146+
);
143147
// Add schema $id, each valid JSON schema should have a unique $id
144148
// see https://json-schema.org/understanding-json-schema/basics#declaring-a-unique-identifier
145149
actorDefinitionPruned.input.$id = schemaID;
@@ -219,17 +223,24 @@ export async function getActorsAsTools(
219223

220224
const actorsInfo: (ActorInfo | null)[] = await Promise.all(
221225
actorIdsOrNames.map(async (actorIdOrName) => {
222-
let actorDefinition = actorDefinitionPrunedCache.get(actorIdOrName);
226+
// Always cache the full schema version under a stable key
227+
const cacheKey = actorIdOrName;
228+
let actorDefinition = actorDefinitionPrunedCache.get(cacheKey);
223229
if (!actorDefinition) {
224-
actorDefinition = await getActorDefinition(actorIdOrName, apifyToken, ACTOR_README_MAX_LENGTH, fullActorSchema);
230+
actorDefinition = await getActorDefinition(
231+
actorIdOrName,
232+
apifyToken,
233+
ACTOR_README_MAX_LENGTH,
234+
);
225235
}
226236

227237
if (!actorDefinition) {
228238
return null;
229239
}
230240

231-
// Cache the pruned Actor definition
232-
actorDefinitionPrunedCache.set(actorIdOrName, actorDefinition);
241+
// Cache canonical full schema without mutation
242+
actorDefinitionPrunedCache.set(cacheKey, actorDefinition);
243+
233244
return {
234245
actorDefinitionPruned: actorDefinition,
235246
webServerMcpPath: getActorMCPServerPath(actorDefinition),
@@ -244,7 +255,7 @@ export async function getActorsAsTools(
244255
const normalActorsInfo = clonedActors.filter((actorInfo) => actorInfo && !actorInfo.webServerMcpPath) as ActorInfo[];
245256

246257
const [normalTools, mcpServerTools] = await Promise.all([
247-
getNormalActorsAsTools(normalActorsInfo),
258+
getNormalActorsAsTools(normalActorsInfo, fullActorSchema),
248259
getMCPServersAsTools(actorMCPServersInfo, apifyToken),
249260
]);
250261

src/tools/build.ts

Lines changed: 3 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,8 @@ import zodToJsonSchema from 'zod-to-json-schema';
66
import log from '@apify/log';
77

88
import { ApifyClient } from '../apify-client.js';
9-
import { ACTOR_README_MAX_LENGTH, ADVANCED_INPUT_KEY, HelperTools } from '../const.js';
10-
import type {
11-
ActorDefinitionPruned,
12-
ActorDefinitionWithDesc,
13-
InternalTool,
14-
ISchemaProperties,
15-
ToolEntry,
16-
} from '../types.js';
9+
import { ACTOR_README_MAX_LENGTH, HelperTools } from '../const.js';
10+
import type { ActorDefinitionPruned, InternalTool, ISchemaProperties, ToolEntry } from '../types.js';
1711
import { filterSchemaProperties, shortenProperties } from './utils.js';
1812

1913
const ajv = new Ajv({ coerceTypes: 'array', strict: false });
@@ -31,7 +25,6 @@ export async function getActorDefinition(
3125
actorIdOrName: string,
3226
apifyToken: string,
3327
limit: number = ACTOR_README_MAX_LENGTH,
34-
fullActorSchema = true,
3528
): Promise<ActorDefinitionPruned | null> {
3629
const client = new ApifyClient({ token: apifyToken });
3730
const actorClient = client.actor(actorIdOrName);
@@ -47,7 +40,7 @@ export async function getActorDefinition(
4740
const buildDetails = await defaultBuildClient.get();
4841

4942
if (buildDetails?.actorDefinition) {
50-
return processActorDefinition(actor, buildDetails.actorDefinition, limit, fullActorSchema);
43+
return processActorDefinition(actor, buildDetails.actorDefinition, limit);
5144
}
5245
return null;
5346
} catch (error) {
@@ -60,7 +53,6 @@ export function processActorDefinition(
6053
actor: Actor,
6154
definition: ActorDefinition,
6255
limit: number,
63-
fullActorSchema: boolean,
6456
): ActorDefinitionPruned {
6557
let input;
6658
if (definition?.input && 'type' in definition.input && 'properties' in definition.input) {
@@ -69,9 +61,6 @@ export function processActorDefinition(
6961
type: definition.input.type as string,
7062
properties: definition.input.properties as Record<string, ISchemaProperties>,
7163
};
72-
if (!fullActorSchema) {
73-
input = separateAdvancedInputs(input);
74-
}
7564
}
7665
return {
7766
id: actor.id,
@@ -85,33 +74,6 @@ export function processActorDefinition(
8574
};
8675
}
8776

88-
function separateAdvancedInputs(input: ActorDefinitionWithDesc['input']): ActorDefinitionPruned['input'] {
89-
if (!input || !input.properties) {
90-
return input;
91-
}
92-
93-
const properties = Object.entries(input.properties);
94-
const firstSectionCaptionIndex = properties.findIndex(([_key, value]) => value.sectionCaption);
95-
if (firstSectionCaptionIndex === -1) {
96-
// No advanced inputs, return the input as is
97-
return input;
98-
}
99-
100-
// Separate advanced inputs from the main section
101-
const mainInputs = properties.slice(0, firstSectionCaptionIndex);
102-
const advancedInputs = properties.slice(firstSectionCaptionIndex);
103-
104-
const propObject = Object.fromEntries(mainInputs);
105-
propObject[ADVANCED_INPUT_KEY] = {
106-
type: 'object',
107-
title: 'Advanced Inputs',
108-
description: 'These inputs are considered advanced and are not required for basic functionality.',
109-
properties: Object.fromEntries(advancedInputs),
110-
};
111-
112-
return { ...input, properties: propObject };
113-
}
114-
11577
/** Prune Actor README if it is too long
11678
* If the README is too long
11779
* - We keep the README as it is up to the limit.

src/tools/utils.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -279,9 +279,19 @@ export function decodeDotPropertyNames(properties: Record<string, unknown>): Rec
279279
return decodedProperties;
280280
}
281281

282-
export function transformActorInputSchemaProperties(input: Readonly<IActorInputSchema>): ActorInputSchemaProperties {
282+
export function transformActorInputSchemaProperties(
283+
input: Readonly<IActorInputSchema>,
284+
options?: { separateAdvancedInputs?: boolean },
285+
): ActorInputSchemaProperties {
283286
// Deep clone input to avoid mutating the original object
284287
const inputClone: IActorInputSchema = structuredClone(input);
288+
289+
// Optionally separate advanced inputs section into a single ADVANCED_INPUT_KEY object
290+
if (options?.separateAdvancedInputs) {
291+
const separated = separateAdvancedInputsInSchema(inputClone);
292+
inputClone.properties = separated;
293+
}
294+
285295
let transformedProperties = markInputPropertiesAsRequired(inputClone);
286296
transformedProperties = buildApifySpecificProperties(transformedProperties);
287297
transformedProperties = filterSchemaProperties(transformedProperties);
@@ -291,3 +301,33 @@ export function transformActorInputSchemaProperties(input: Readonly<IActorInputS
291301
transformedProperties = encodeDotPropertyNames(transformedProperties);
292302
return transformedProperties;
293303
}
304+
305+
/**
306+
* Separates advanced inputs into a dedicated ADVANCED_INPUT_KEY object based on the first section-captioned property.
307+
* The properties from the first section-captioned property onwards are grouped under the advanced input object.
308+
*/
309+
export function separateAdvancedInputsInSchema(input: IActorInputSchema): Record<string, ISchemaProperties> {
310+
if (!input || !input.properties) {
311+
return input.properties ?? {};
312+
}
313+
314+
const propertiesEntries = Object.entries(input.properties);
315+
const firstSectionCaptionIndex = propertiesEntries.findIndex(([_key, value]) => value.sectionCaption);
316+
if (firstSectionCaptionIndex === -1) {
317+
return input.properties;
318+
}
319+
320+
const mainInputs = propertiesEntries.slice(0, firstSectionCaptionIndex);
321+
const advancedInputs = propertiesEntries.slice(firstSectionCaptionIndex);
322+
323+
const propObject: Record<string, ISchemaProperties> = Object.fromEntries(mainInputs);
324+
// Use a basic object container here; additional details (like title/description) are not required at this stage
325+
propObject.advancedInput = {
326+
type: 'object',
327+
title: 'Advanced Inputs',
328+
description: 'These inputs are considered advanced and are not required for basic functionality.',
329+
properties: Object.fromEntries(advancedInputs),
330+
} as unknown as ISchemaProperties;
331+
332+
return propObject;
333+
}

src/utils/tools-loader.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
*/
55

66
import { defaults } from '../const.js';
7-
import { addRemoveTools, getActorsAsTools, toolCategories } from '../tools/index.js';
87
import type { McpOptions } from '../input.js';
8+
import { addRemoveTools, getActorsAsTools, toolCategories } from '../tools/index.js';
99
import type { ToolCategory, ToolEntry } from '../types.js';
1010

1111
/**

0 commit comments

Comments
 (0)