Skip to content

Commit ba3849a

Browse files
quanruclaudeyuyutaotao
authored
fix(core): handle ZodEffects and ZodUnion in schema parsing (#1359)
* fix(core): handle ZodEffects and ZodUnion in schema parsing - Add support for ZodEffects (transformations) in getTypeName and getDescription - Add support for ZodUnion types with proper type display (type1 | type2) - Fixes "failed to parse Zod type" warning on first execution with caching 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * test(core): add tests for descriptionForAction with ZodEffects and ZodUnion * chore(core): update test cases --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: yutao <yutao.tao@bytedance.com>
1 parent efbc2d3 commit ba3849a

File tree

2 files changed

+299
-0
lines changed

2 files changed

+299
-0
lines changed

packages/core/src/ai-model/prompt/llm-planning.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,14 @@ export const descriptionForAction = (
5757
return unwrapField(f._def.innerType);
5858
}
5959

60+
// Handle ZodEffects (transformations, refinements, preprocessors)
61+
if (typeName === 'ZodEffects') {
62+
// For ZodEffects, unwrap the schema field which contains the underlying type
63+
if (f._def.schema) {
64+
return unwrapField(f._def.schema);
65+
}
66+
}
67+
6068
return f;
6169
};
6270

@@ -82,6 +90,16 @@ export const descriptionForAction = (
8290

8391
return `enum(${values})`;
8492
}
93+
// Handle ZodUnion by taking the first option (for display purposes)
94+
if (fieldTypeName === 'ZodUnion') {
95+
const options = actualField._def?.options as any[] | undefined;
96+
if (options && options.length > 0) {
97+
// For unions, list all types
98+
const types = options.map((opt: any) => getTypeName(opt));
99+
return types.join(' | ');
100+
}
101+
return 'union';
102+
}
85103

86104
console.warn(
87105
'failed to parse Zod type. This may lead to wrong params from the LLM.\n',
@@ -107,6 +125,14 @@ export const descriptionForAction = (
107125
return unwrapField(f._def.innerType);
108126
}
109127

128+
// Handle ZodEffects (transformations, refinements, preprocessors)
129+
if (typeName === 'ZodEffects') {
130+
// For ZodEffects, unwrap the schema field which contains the underlying type
131+
if (f._def.schema) {
132+
return unwrapField(f._def.schema);
133+
}
134+
}
135+
110136
return f;
111137
};
112138

packages/core/tests/unit-test/llm-planning.test.ts

Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
getMidsceneLocationSchema,
55
} from '@/ai-model/common';
66
import { buildYamlFlowFromPlans } from '@/ai-model/common';
7+
import { descriptionForAction } from '@/ai-model/prompt/llm-planning';
78
import {
89
MIDSCENE_USE_DOUBAO_VISION,
910
OPENAI_API_KEY,
@@ -336,3 +337,275 @@ describe('llm planning - build yaml flow', () => {
336337
]);
337338
});
338339
});
340+
341+
describe('llm planning - descriptionForAction with ZodEffects and ZodUnion', () => {
342+
it('should handle ZodEffects (transform)', () => {
343+
const schema = z.object({
344+
value: z.string().transform((val) => val.toLowerCase()),
345+
});
346+
347+
const action = {
348+
name: 'TestAction',
349+
description: 'Test action with ZodEffects',
350+
paramSchema: schema,
351+
call: async () => {},
352+
};
353+
354+
const description = descriptionForAction(action, 'string');
355+
expect(description).toMatchInlineSnapshot(`
356+
"- TestAction, Test action with ZodEffects
357+
- type: "TestAction"
358+
- param:
359+
- value: string"
360+
`);
361+
});
362+
363+
it('should handle ZodEffects with refinement', () => {
364+
const schema = z.object({
365+
email: z.string().email(),
366+
});
367+
368+
const action = {
369+
name: 'ValidateEmail',
370+
description: 'Validate email action',
371+
paramSchema: schema,
372+
call: async () => {},
373+
};
374+
375+
const description = descriptionForAction(action, 'string');
376+
expect(description).toMatchInlineSnapshot(`
377+
"- ValidateEmail, Validate email action
378+
- type: "ValidateEmail"
379+
- param:
380+
- email: string"
381+
`);
382+
});
383+
384+
it('should handle ZodEffects with description', () => {
385+
const schema = z.object({
386+
count: z
387+
.number()
388+
.transform((val) => val * 2)
389+
.describe('Number to be doubled'),
390+
});
391+
392+
const action = {
393+
name: 'DoubleNumber',
394+
description: 'Double the number',
395+
paramSchema: schema,
396+
call: async () => {},
397+
};
398+
399+
const description = descriptionForAction(action, 'string');
400+
expect(description).toMatchInlineSnapshot(`
401+
"- DoubleNumber, Double the number
402+
- type: "DoubleNumber"
403+
- param:
404+
- count: number // Number to be doubled"
405+
`);
406+
});
407+
408+
it('should handle ZodUnion types', () => {
409+
const schema = z.object({
410+
value: z.union([z.string(), z.number()]),
411+
});
412+
413+
const action = {
414+
name: 'UnionTest',
415+
description: 'Test union types',
416+
paramSchema: schema,
417+
call: async () => {},
418+
};
419+
420+
const description = descriptionForAction(action, 'string');
421+
expect(description).toMatchInlineSnapshot(`
422+
"- UnionTest, Test union types
423+
- type: "UnionTest"
424+
- param:
425+
- value: string | number"
426+
`);
427+
});
428+
429+
it('should handle ZodUnion with multiple types', () => {
430+
const schema = z.object({
431+
status: z.union([z.string(), z.number(), z.boolean()]),
432+
});
433+
434+
const action = {
435+
name: 'MultiUnion',
436+
description: 'Multiple union types',
437+
paramSchema: schema,
438+
call: async () => {},
439+
};
440+
441+
const description = descriptionForAction(action, 'string');
442+
expect(description).toMatchInlineSnapshot(`
443+
"- MultiUnion, Multiple union types
444+
- type: "MultiUnion"
445+
- param:
446+
- status: string | number | boolean"
447+
`);
448+
});
449+
450+
it('should handle ZodUnion with description', () => {
451+
const schema = z.object({
452+
input: z
453+
.union([z.string(), z.number()])
454+
.describe('Either a string or number'),
455+
});
456+
457+
const action = {
458+
name: 'FlexibleInput',
459+
description: 'Accepts string or number',
460+
paramSchema: schema,
461+
call: async () => {},
462+
};
463+
464+
const description = descriptionForAction(action, 'string');
465+
expect(description).toMatchInlineSnapshot(`
466+
"- FlexibleInput, Accepts string or number
467+
- type: "FlexibleInput"
468+
- param:
469+
- input: string | number // Either a string or number"
470+
`);
471+
});
472+
473+
it('should handle optional ZodEffects', () => {
474+
const schema = z.object({
475+
optionalEmail: z.string().email().optional(),
476+
});
477+
478+
const action = {
479+
name: 'OptionalEmail',
480+
description: 'Optional email field',
481+
paramSchema: schema,
482+
call: async () => {},
483+
};
484+
485+
const description = descriptionForAction(action, 'string');
486+
expect(description).toMatchInlineSnapshot(`
487+
"- OptionalEmail, Optional email field
488+
- type: "OptionalEmail"
489+
- param:
490+
- optionalEmail?: string"
491+
`);
492+
});
493+
494+
it('should handle optional ZodUnion', () => {
495+
const schema = z.object({
496+
optionalValue: z.union([z.string(), z.number()]).optional(),
497+
});
498+
499+
const action = {
500+
name: 'OptionalUnion',
501+
description: 'Optional union field',
502+
paramSchema: schema,
503+
call: async () => {},
504+
};
505+
506+
const description = descriptionForAction(action, 'string');
507+
expect(description).toMatchInlineSnapshot(`
508+
"- OptionalUnion, Optional union field
509+
- type: "OptionalUnion"
510+
- param:
511+
- optionalValue?: string | number"
512+
`);
513+
});
514+
515+
it('should handle nullable ZodEffects', () => {
516+
const schema = z.object({
517+
nullableTransform: z
518+
.string()
519+
.transform((val) => val.toUpperCase())
520+
.nullable(),
521+
});
522+
523+
const action = {
524+
name: 'NullableTransform',
525+
description: 'Nullable transform field',
526+
paramSchema: schema,
527+
call: async () => {},
528+
};
529+
530+
const description = descriptionForAction(action, 'string');
531+
expect(description).toMatchInlineSnapshot(`
532+
"- NullableTransform, Nullable transform field
533+
- type: "NullableTransform"
534+
- param:
535+
- nullableTransform: string"
536+
`);
537+
});
538+
539+
it('should handle ZodEffects with ZodUnion', () => {
540+
const schema = z.object({
541+
complexField: z
542+
.union([z.string(), z.number()])
543+
.transform((val) => String(val)),
544+
});
545+
546+
const action = {
547+
name: 'ComplexField',
548+
description: 'Complex field with union and transform',
549+
paramSchema: schema,
550+
call: async () => {},
551+
};
552+
553+
const description = descriptionForAction(action, 'string');
554+
// The transform wraps the union, so we should get string | number from the inner union
555+
expect(description).toMatchInlineSnapshot(`
556+
"- ComplexField, Complex field with union and transform
557+
- type: "ComplexField"
558+
- param:
559+
- complexField: string | number"
560+
`);
561+
});
562+
563+
it('should handle ZodDefault with ZodEffects', () => {
564+
const schema = z.object({
565+
withDefault: z
566+
.string()
567+
.transform((val) => val.trim())
568+
.default('default'),
569+
});
570+
571+
const action = {
572+
name: 'DefaultTransform',
573+
description: 'Field with default and transform',
574+
paramSchema: schema,
575+
call: async () => {},
576+
};
577+
578+
const description = descriptionForAction(action, 'string');
579+
// Fields with .default() are optional
580+
expect(description).toMatchInlineSnapshot(`
581+
"- DefaultTransform, Field with default and transform
582+
- type: "DefaultTransform"
583+
- param:
584+
- withDefault?: string"
585+
`);
586+
});
587+
588+
it('should handle complex nested ZodUnion', () => {
589+
const schema = z.object({
590+
nested: z.union([
591+
z.string(),
592+
z.object({ type: z.string(), value: z.number() }),
593+
]),
594+
});
595+
596+
const action = {
597+
name: 'NestedUnion',
598+
description: 'Nested union type',
599+
paramSchema: schema,
600+
call: async () => {},
601+
};
602+
603+
const description = descriptionForAction(action, 'string');
604+
expect(description).toMatchInlineSnapshot(`
605+
"- NestedUnion, Nested union type
606+
- type: "NestedUnion"
607+
- param:
608+
- nested: string | object"
609+
`);
610+
});
611+
});

0 commit comments

Comments
 (0)