Skip to content

Commit 6150f36

Browse files
committed
feat(dsp): Separate structured output example input fields with newlines and allow missing required fields during structured output validation in response processing.
1 parent ffc0fab commit 6150f36

File tree

4 files changed

+60
-10
lines changed

4 files changed

+60
-10
lines changed

src/ax/dsp/extract.ts

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -718,7 +718,8 @@ export function validateAndParseFieldValue(
718718
*/
719719
export function validateStructuredOutputValues(
720720
signature: Readonly<AxSignature>,
721-
values: Record<string, unknown>
721+
values: Record<string, unknown>,
722+
options?: { allowMissingRequired?: boolean }
722723
): void {
723724
const outputFields = signature.getOutputFields();
724725

@@ -727,7 +728,7 @@ export function validateStructuredOutputValues(
727728

728729
// Check required fields
729730
if (value === undefined || value === null) {
730-
if (!field.isOptional) {
731+
if (!field.isOptional && !options?.allowMissingRequired) {
731732
throw createRequiredFieldMissingError(field);
732733
}
733734
continue;
@@ -773,7 +774,11 @@ export function validateStructuredOutputValues(
773774
typeof value === 'object' &&
774775
!Array.isArray(value)
775776
) {
776-
validateNestedObjectFields(field, value as Record<string, unknown>);
777+
validateNestedObjectFields(
778+
field,
779+
value as Record<string, unknown>,
780+
options
781+
);
777782
}
778783

779784
// Validate array of objects
@@ -785,7 +790,11 @@ export function validateStructuredOutputValues(
785790
) {
786791
for (const item of value) {
787792
if (item && typeof item === 'object') {
788-
validateNestedObjectFields(field, item as Record<string, unknown>);
793+
validateNestedObjectFields(
794+
field,
795+
item as Record<string, unknown>,
796+
options
797+
);
789798
}
790799
}
791800
}
@@ -797,7 +806,8 @@ export function validateStructuredOutputValues(
797806
*/
798807
function validateNestedObjectFields(
799808
parentField: Readonly<AxField>,
800-
obj: Record<string, unknown>
809+
obj: Record<string, unknown>,
810+
options?: { allowMissingRequired?: boolean }
801811
): void {
802812
const fields = parentField.type?.fields;
803813
if (!fields || typeof fields !== 'object') return;
@@ -829,7 +839,7 @@ function validateNestedObjectFields(
829839

830840
// Check required fields
831841
if (value === undefined || value === null) {
832-
if (!nestedField.isOptional) {
842+
if (!nestedField.isOptional && !options?.allowMissingRequired) {
833843
throw createRequiredFieldMissingError(nestedField);
834844
}
835845
continue;
@@ -875,7 +885,11 @@ function validateNestedObjectFields(
875885
typeof value === 'object' &&
876886
!Array.isArray(value)
877887
) {
878-
validateNestedObjectFields(nestedField, value as Record<string, unknown>);
888+
validateNestedObjectFields(
889+
nestedField,
890+
value as Record<string, unknown>,
891+
options
892+
);
879893
}
880894

881895
// Validate array of objects
@@ -889,7 +903,8 @@ function validateNestedObjectFields(
889903
if (item && typeof item === 'object') {
890904
validateNestedObjectFields(
891905
nestedField,
892-
item as Record<string, unknown>
906+
item as Record<string, unknown>,
907+
options
893908
);
894909
}
895910
}

src/ax/dsp/processResponse.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,8 @@ async function* ProcessStreamingResponse<OUT extends AxGenOut>({
265265
// Validate structured output values against field constraints
266266
validateStructuredOutputValues(
267267
signature,
268-
delta as Record<string, unknown>
268+
delta as Record<string, unknown>,
269+
{ allowMissingRequired: true }
269270
);
270271

271272
// Update state values
@@ -441,7 +442,8 @@ export async function* finalizeStreamingResponse<OUT extends AxGenOut>({
441442
// Validate structured output values against field constraints
442443
validateStructuredOutputValues(
443444
signature,
444-
delta as Record<string, unknown>
445+
delta as Record<string, unknown>,
446+
{ allowMissingRequired: true }
445447
);
446448

447449
Object.assign(state.values, delta);

src/ax/dsp/prompt.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -502,6 +502,9 @@ export class AxPromptTemplate {
502502

503503
renderedItem.forEach((v) => {
504504
if (v) {
505+
if ('text' in v) {
506+
v.text = `${v.text}\n`;
507+
}
505508
list.push(v);
506509
}
507510
});

src/ax/dsp/structured_output_features.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,34 @@ describe('Structured Output Features', () => {
7575
'IMPORTANT: Provide the FULL JSON object for this field'
7676
);
7777
});
78+
79+
it('should separate input fields with newlines in examples when structured outputs are enabled', () => {
80+
const signature = f()
81+
.input('field1', f.string())
82+
.input('field2', f.string())
83+
.output('responseText', f.string())
84+
.useStructured()
85+
.build();
86+
87+
const template = new AxPromptTemplate(signature);
88+
const examples = [
89+
{
90+
field1: 'value1',
91+
field2: 'value2',
92+
responseText: 'result',
93+
},
94+
];
95+
96+
const rendered = template.render(
97+
{ field1: 'test', field2: 'test' },
98+
{ examples }
99+
);
100+
const systemMessage = rendered.find((m) => m.role === 'system');
101+
102+
// Check that fields are separated by newline
103+
// We expect "Field1: value1\nField2: value2"
104+
expect(systemMessage?.content).toContain(
105+
'Field 1: value1\nField 2: value2'
106+
);
107+
});
78108
});

0 commit comments

Comments
 (0)