Skip to content

Commit 930b1a4

Browse files
authored
Merge pull request #213 from PredicateSystems/multistep_form
fix: prioritize form-fill over extraction category to prevent false p…
2 parents 8b4600d + eb31650 commit 930b1a4

13 files changed

Lines changed: 2092 additions & 53 deletions

src/agents/planner-executor/category-pruner.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,21 @@ export function detectPruningCategory(
284284
return PruningTaskCategory.FORM_FILLING;
285285
}
286286

287+
// Form-fill keyword detection takes priority over extraction
288+
// because "Display name" or "email" in a form task are field labels, not extraction.
289+
if (
290+
normalizedGoal.includes('form') ||
291+
normalizedGoal.includes('fill') ||
292+
normalizedGoal.includes('fill out') ||
293+
normalizedGoal.includes('submit') ||
294+
normalizedGoal.includes('onboarding') ||
295+
normalizedGoal.includes('sign up') ||
296+
normalizedGoal.includes('signup') ||
297+
normalizedGoal.includes('register')
298+
) {
299+
return PruningTaskCategory.FORM_FILLING;
300+
}
301+
287302
// Extraction keyword detection takes priority over TRANSACTION/SHOPPING
288303
// because "find the title of X" or "extract Y" on an e-commerce site is
289304
// still an extraction task, not a shopping task.

src/agents/planner-executor/extraction-keywords.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,13 +177,37 @@ export const TEXT_EXTRACTION_KEYWORDS: readonly string[] = [
177177
* @param task - The task or step description to analyse
178178
* @returns true if this looks like a text extraction task
179179
*/
180+
const FORM_FILL_SIGNALS: readonly string[] = [
181+
'form',
182+
'fill',
183+
'submit',
184+
'onboarding',
185+
'sign up',
186+
'signup',
187+
'register',
188+
'checkbox',
189+
'dropdown',
190+
'radio button',
191+
'next button',
192+
'click the',
193+
'type ',
194+
'enter ',
195+
];
196+
180197
export function isTextExtractionTask(task: string): boolean {
181198
if (!task) {
182199
return false;
183200
}
184201

185202
const taskLower = task.toLowerCase();
186203

204+
// Form-fill negative signal: if the task clearly involves filling a form,
205+
// it's not extraction even if it contains extraction-like keywords
206+
// (e.g., "Display name", "email" are field labels, not extraction targets)
207+
if (FORM_FILL_SIGNALS.some(signal => taskLower.includes(signal))) {
208+
return false;
209+
}
210+
187211
// Tier 1: Strong extraction phrases (multi-word substring match)
188212
for (const phrase of EXTRACTION_PHRASES) {
189213
if (taskLower.includes(phrase)) {

src/agents/planner-executor/plan-models.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,8 @@ export interface ActionRecord {
175175
action: string;
176176
/** Element description or URL */
177177
target: string | null;
178+
/** Planner intent for this action, if provided */
179+
intent?: string | null;
178180
/** Outcome (success, failed) */
179181
result: string;
180182
/** URL after action completed */
@@ -213,6 +215,7 @@ export interface StepOutcome {
213215
urlBefore?: string;
214216
urlAfter?: string;
215217
extractedData?: unknown;
218+
pageContentPreview?: string;
216219
}
217220

218221
// ---------------------------------------------------------------------------

src/agents/planner-executor/plan-utils.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,14 +182,25 @@ function stripThinkingTags(content: string): string {
182182
function repairJson(text: string): string {
183183
let repaired = text;
184184

185+
// Fix backslash-escaped quotes that make JSON unparseable.
186+
// Some LLMs output: {"action":"CLICK",...,\"reasoning\":\"text\"}
187+
// where the outer quotes are real but inner key/value quotes are escaped.
188+
// Heuristic: if the text has unescaped quotes AND backslash-escaped quotes
189+
// mixed together, unescape the backslash-escaped ones.
190+
const hasUnescapedQuotes = /[^\\]"/.test(repaired);
191+
const hasEscapedQuotes = /\\"/.test(repaired);
192+
if (hasUnescapedQuotes && hasEscapedQuotes) {
193+
repaired = repaired.replace(/\\"/g, '"');
194+
}
195+
185196
// Add double quotes around unquoted object keys
186197
// Matches: word-characters followed by colon (not already inside a string)
187198
// Pattern: start of object `{` or comma `,`, optional whitespace, then unquoted key, then `:`
188199
repaired = repaired.replace(/([{,]\s*)([\w$]+)\s*:/g, '$1"$2":');
189200

190201
// Replace single-quoted strings with double-quoted strings
191202
// This is a simple heuristic — it won't handle escaped single quotes inside strings,
192-
// but it handles the common case of LLMs outputting `'text'` instead of `"text"`
203+
// but it handles the common case of LLMs outputting 'text' instead of "text"
193204
repaired = repaired.replace(/'([^']*)'/g, '"$1"');
194205

195206
// Remove trailing commas before } or ]
@@ -338,6 +349,8 @@ const ACTION_ALIASES: Record<string, string> = {
338349
CLICK_ELEMENT: 'CLICK',
339350
CLICK_BUTTON: 'CLICK',
340351
CLICK_LINK: 'CLICK',
352+
CLICK_XY: 'CLICK',
353+
TYPE_AT: 'TYPE',
341354
INPUT: 'TYPE_AND_SUBMIT',
342355
TYPE_TEXT: 'TYPE_AND_SUBMIT',
343356
ENTER_TEXT: 'TYPE_AND_SUBMIT',
@@ -347,6 +360,8 @@ const ACTION_ALIASES: Record<string, string> = {
347360
OPEN: 'NAVIGATE',
348361
SCROLL_DOWN: 'SCROLL',
349362
SCROLL_UP: 'SCROLL',
363+
SCROLL_TO: 'SCROLL',
364+
SCROLL_INTO_VIEW: 'SCROLL',
350365
};
351366

352367
/**
@@ -517,6 +532,20 @@ function normalizeStep(step: Record<string, unknown>): Record<string, unknown> {
517532
if ('target' in normalizedStep && normalizedStep.target === null) {
518533
delete normalizedStep.target;
519534
}
535+
if ('input' in normalizedStep && typeof normalizedStep.input !== 'string') {
536+
const inputValue = normalizedStep.input;
537+
if (inputValue === null || inputValue === undefined) {
538+
delete normalizedStep.input;
539+
} else if (
540+
typeof inputValue === 'number' ||
541+
typeof inputValue === 'boolean' ||
542+
typeof inputValue === 'bigint'
543+
) {
544+
normalizedStep.input = String(inputValue);
545+
} else {
546+
normalizedStep.input = JSON.stringify(inputValue);
547+
}
548+
}
520549

521550
if ('id' in normalizedStep && typeof normalizedStep.id === 'string') {
522551
const parsed = parseInt(normalizedStep.id, 10);

0 commit comments

Comments
 (0)