Skip to content

Commit d9d9836

Browse files
committed
fix: refine field extraction logic and update test cases
- Enhanced the streamingExtractFinalValue function to accurately determine field boundaries, preventing content overlap between fields. - Updated the parseMissedFieldsFromFullContent function to process content line by line for improved field extraction precision. - Adjusted test cases in extract.test.ts to reflect changes in field handling and ensure correct functionality. - Added new words to the project dictionary for better spell checking support.
1 parent 71e8e63 commit d9d9836

File tree

4 files changed

+68
-45
lines changed

4 files changed

+68
-45
lines changed

.cspell/project-words.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,4 +142,5 @@ configtestagent
142142
openrouter
143143
anns
144144
annos
145-
mundo
145+
mundo
146+
fmap

src/ax/dsp/extract.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,8 +168,8 @@ Model Answer 2: false`;
168168
const content = `Plan: First, I will check the weather in San Francisco to determine if it's a nice day for outdoor seating. Then, I will search for restaurants in San Francisco that offer sushi, Chinese, or Indian cuisine, prioritizing outdoor seating if the weather is good. Finally, I will suggest a suitable restaurant.
169169
getCurrentWeather location: San Francisco
170170
findRestaurants location: San Francisco
171-
findRestaurants cuisine: sushi, chinese, indian
172171
findRestaurants outdoor: true
172+
findRestaurants cuisine: sushi, chinese, indian
173173
findRestaurants priceRange: $$-$$$
174174
`;
175175

src/ax/dsp/extract.ts

Lines changed: 49 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,25 @@ export const streamingExtractFinalValue = (
271271
const forceFinalize = options?.forceFinalize ?? false;
272272

273273
if (xstate.currField) {
274-
const val = content.substring(xstate.s).trim();
274+
let endIndex = content.length;
275+
276+
// Look for the next field boundary to avoid including content from other fields
277+
const outputFields = sig.getOutputFields();
278+
for (const otherField of outputFields) {
279+
if (otherField.name === xstate.currField.name) {
280+
continue;
281+
}
282+
283+
// Look for the next field title after current position
284+
const nextFieldPattern = `\n${otherField.title}:`;
285+
const nextFieldIndex = content.indexOf(nextFieldPattern, xstate.s);
286+
287+
if (nextFieldIndex !== -1 && nextFieldIndex < endIndex) {
288+
endIndex = nextFieldIndex;
289+
}
290+
}
291+
292+
const val = content.substring(xstate.s, endIndex).trim();
275293
const parsedValue = validateAndParseFieldValue(xstate.currField, val);
276294
if (parsedValue !== undefined) {
277295
values[xstate.currField.name] = parsedValue;
@@ -296,7 +314,7 @@ export const streamingExtractFinalValue = (
296314
}
297315

298316
// Check for optional fields that might have been missed by streaming parser
299-
parseOptionalFieldsFromFullContent(sig, values, content);
317+
parseMissedFieldsFromFullContent(sig, values, content);
300318

301319
// Check all previous required fields before processing current field
302320
// In streaming scenarios (non-strict), defer missing-required enforcement until end
@@ -321,57 +339,48 @@ export const streamingExtractFinalValue = (
321339
}
322340
};
323341

324-
// Helper function to parse optional fields from full content that streaming parser might have missed
325-
const parseOptionalFieldsFromFullContent = (
342+
// Helper function to parse missed fields from full content that streaming parser might have missed
343+
const parseMissedFieldsFromFullContent = (
326344
sig: Readonly<AxSignature>,
327345
values: Record<string, unknown>,
328346
content: string
329347
) => {
330348
const outputFields = sig.getOutputFields();
331349

350+
// Process content line by line for more precise field extraction
351+
const lines = content.split('\n');
352+
332353
for (const field of outputFields) {
333-
// Skip if field is not optional or already found
334-
if (!field.isOptional || field.name in values) {
354+
// Skip if field is already found
355+
if (field.name in values) {
335356
continue;
336357
}
337358

338-
// Look for field.title pattern in content
359+
// Look for field.title pattern in each line
339360
const prefix = `${field.title}:`;
340-
const fieldIndex = content.indexOf(prefix);
341-
342-
if (fieldIndex === -1) {
343-
continue;
344-
}
345361

346-
// Extract content after the field prefix
347-
const startIndex = fieldIndex + prefix.length;
348-
let endIndex = content.length;
349-
350-
// Find the end of this field's content by looking for the next field or end of content
351-
for (const otherField of outputFields) {
352-
if (otherField.name === field.name) {
353-
continue;
354-
}
355-
356-
const otherPrefix = `${otherField.title}:`;
357-
const otherFieldIndex = content.indexOf(otherPrefix, startIndex);
358-
359-
if (otherFieldIndex !== -1 && otherFieldIndex < endIndex) {
360-
endIndex = otherFieldIndex;
361-
}
362-
}
363-
364-
// Extract and validate the field value
365-
const fieldValue = content.substring(startIndex, endIndex).trim();
366-
367-
if (fieldValue) {
368-
try {
369-
const parsedValue = validateAndParseFieldValue(field, fieldValue);
370-
if (parsedValue !== undefined) {
371-
values[field.name] = parsedValue;
362+
for (const line of lines) {
363+
const trimmedLine = line.trim();
364+
if (trimmedLine.startsWith(prefix)) {
365+
// Extract the value after the colon
366+
const fieldValue = trimmedLine.substring(prefix.length).trim();
367+
368+
if (fieldValue) {
369+
try {
370+
const parsedValue = validateAndParseFieldValue(field, fieldValue);
371+
if (parsedValue !== undefined) {
372+
values[field.name] = parsedValue;
373+
break; // Found the field, stop looking
374+
}
375+
} catch (e) {
376+
// Only ignore validation errors for optional fields
377+
if (!field.isOptional) {
378+
throw e;
379+
}
380+
// Ignore validation errors for optional fields in this fallback parser
381+
}
372382
}
373-
} catch {
374-
// Ignore validation errors for optional fields in this fallback parser
383+
break; // Found the field marker, stop looking even if value was empty
375384
}
376385
}
377386
}

src/ax/dsp/functions.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ export class AxFunctionProcessor {
185185
} as const;
186186
};
187187

188-
public execute = async <MODEL>(
188+
public executeWithDetails = async <MODEL>(
189189
func: Readonly<AxChatResponseFunctionCall>,
190190
options?: Readonly<
191191
AxProgramForwardOptions<MODEL> & {
@@ -222,6 +222,19 @@ export class AxFunctionProcessor {
222222
throw e;
223223
}
224224
};
225+
226+
public execute = async <MODEL>(
227+
func: Readonly<AxChatResponseFunctionCall>,
228+
options?: Readonly<
229+
AxProgramForwardOptions<MODEL> & {
230+
traceId?: string;
231+
stopFunctionNames?: readonly string[];
232+
}
233+
>
234+
): Promise<string> => {
235+
const result = await this.executeWithDetails<MODEL>(func, options);
236+
return result.formatted;
237+
};
225238
}
226239

227240
export type AxInputFunctionType = (
@@ -328,7 +341,7 @@ export const processFunctions = async ({
328341

329342
if (!tracer) {
330343
return funcProc
331-
.execute(func, {
344+
.executeWithDetails(func, {
332345
sessionId,
333346
ai,
334347
functionResultFormatter,
@@ -427,7 +440,7 @@ export const processFunctions = async ({
427440
rawResult,
428441
parsedArgs,
429442
}: { formatted: string; rawResult: unknown; parsedArgs: unknown } =
430-
await funcProc.execute(func, {
443+
await funcProc.executeWithDetails(func, {
431444
sessionId,
432445
ai,
433446
functionResultFormatter,

0 commit comments

Comments
 (0)