Skip to content

Commit 159f6d3

Browse files
authored
update the extract tool in v2 agent (#1593)
# why update extract handling # what changed # test plan <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Switch the v2 agent extract tool to accept JSON Schema and convert it to Zod internally. This removes unsafe schema string evaluation and adds format support (url, email, uuid) for clearer, safer extraction. - **Refactors** - Replaced Function-based Zod string evaluation with JSON Schema → Zod conversion. - Added format handling for url, email, and uuid; supports enum and arrays via items. - Wrapped non-object schemas into an object as { result: ... } for consistent output. - Removed logger dependency from createExtractTool and its registration. - **Migration** - Pass schema as a JSON object (JSON Schema), not a Zod string. - Use format: "url" for links; use enum for string lists; use items for arrays. - If you call createExtractTool directly, remove the logger parameter. <sup>Written for commit c1d8cf1. Summary will update on new commits. <a href="https://cubic.dev/pr/browserbase/stagehand/pull/1593">Review in cubic</a></sup> <!-- End of auto-generated description by cubic. -->
1 parent f2db0bb commit 159f6d3

File tree

3 files changed

+68
-45
lines changed

3 files changed

+68
-45
lines changed

.changeset/cold-needles-open.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@browserbasehq/stagehand": patch
3+
---
4+
5+
Update extract tool

lib/agent/tools/extract.ts

Lines changed: 62 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,72 +1,93 @@
11
import { tool } from "ai";
2-
import { z } from "zod/v3";
2+
import { z, ZodTypeAny } from "zod/v3";
33
import { Stagehand } from "../../index";
4-
import { LogLine } from "@/types/log";
54

6-
/**
7-
* Evaluates a Zod schema string and returns the actual Zod schema
8-
* Uses Function constructor to evaluate the schema string in a controlled way
9-
*/
10-
function evaluateZodSchema(
11-
schemaStr: string,
12-
logger?: (message: LogLine) => void,
13-
): z.ZodTypeAny {
14-
try {
15-
// Create a function that returns the evaluated schema
16-
// We pass z as a parameter to make it available in the evaluated context
17-
const schemaFunction = new Function("z", `return ${schemaStr}`);
18-
return schemaFunction(z);
19-
} catch (error) {
20-
logger?.({
21-
category: "extract",
22-
message: `Failed to evaluate schema string, using z.any(): ${error}`,
23-
level: 1,
24-
auxiliary: {
25-
error: {
26-
value: error,
27-
type: "string",
28-
},
29-
},
30-
});
31-
return z.any();
5+
interface JsonSchema {
6+
type?: string;
7+
properties?: Record<string, JsonSchema>;
8+
items?: JsonSchema;
9+
enum?: string[];
10+
format?: "url" | "email" | "uuid";
11+
}
12+
13+
function jsonSchemaToZod(schema: JsonSchema): ZodTypeAny {
14+
switch (schema.type) {
15+
case "object": {
16+
const shape: Record<string, ZodTypeAny> = {};
17+
if (schema.properties) {
18+
for (const [key, value] of Object.entries(schema.properties)) {
19+
shape[key] = jsonSchemaToZod(value);
20+
}
21+
}
22+
return z.object(shape);
23+
}
24+
case "array":
25+
return z.array(schema.items ? jsonSchemaToZod(schema.items) : z.any());
26+
case "string": {
27+
let s = z.string();
28+
if (schema.format === "url") s = s.url();
29+
if (schema.format === "email") s = s.email();
30+
if (schema.format === "uuid") s = s.uuid();
31+
if (schema.enum && schema.enum.length > 0)
32+
return z.enum(schema.enum as [string, ...string[]]);
33+
return s;
34+
}
35+
case "number":
36+
case "integer":
37+
return z.number();
38+
case "boolean":
39+
return z.boolean();
40+
case "null":
41+
return z.null();
42+
default:
43+
return z.any();
3244
}
3345
}
3446

3547
export const createExtractTool = (
3648
stagehand: Stagehand,
3749
executionModel?: string,
38-
logger?: (message: LogLine) => void,
3950
) =>
4051
tool({
4152
description: `Extract structured data from the current page based on a provided schema.
4253
4354
USAGE GUIDELINES:
4455
- Keep schemas MINIMAL - only include fields essential for the task
45-
- IMPORANT: only use this if explicitly asked for structured output. In most scenarios, you should use the aria tree tool over this.
46-
- If you need to extract a link, make sure the type defintion follows the format of z.string().url()
56+
- IMPORTANT: only use this if explicitly asked for structured output. In most scenarios, you should use the aria tree tool over this.
57+
- For URL fields, use format: "url"
58+
4759
EXAMPLES:
4860
1. Extract a single value:
4961
instruction: "extract the product price"
50-
schema: "z.object({ price: z.number()})"
62+
schema: { type: "object", properties: { price: { type: "number" } } }
5163
5264
2. Extract multiple fields:
5365
instruction: "extract product name and price"
54-
schema: "z.object({ name: z.string(), price: z.number() })"
66+
schema: { type: "object", properties: { name: { type: "string" }, price: { type: "number" } } }
5567
5668
3. Extract arrays:
5769
instruction: "extract all product names and prices"
58-
schema: "z.object({ products: z.array(z.object({ name: z.string(), price: z.number() })) })"`,
70+
schema: { type: "object", properties: { products: { type: "array", items: { type: "object", properties: { name: { type: "string" }, price: { type: "number" } } } } } }
71+
72+
4. Extract a URL:
73+
instruction: "extract the link"
74+
schema: { type: "object", properties: { url: { type: "string", format: "url" } } }`,
5975
parameters: z.object({
6076
instruction: z
6177
.string()
6278
.describe(
6379
"Clear instruction describing what data to extract from the page",
6480
),
6581
schema: z
66-
.string()
67-
.describe(
68-
'Zod schema as a string (e.g., "z.object({ price: z.number() })")',
69-
),
82+
.object({
83+
type: z.string().optional(),
84+
properties: z.record(z.string(), z.unknown()).optional(),
85+
items: z.unknown().optional(),
86+
enum: z.array(z.string()).optional(),
87+
format: z.enum(["url", "email", "uuid"]).optional(),
88+
})
89+
.passthrough()
90+
.describe("JSON Schema object describing the structure to extract"),
7091
}),
7192
execute: async ({ instruction, schema }) => {
7293
try {
@@ -79,23 +100,20 @@ export const createExtractTool = (
79100
value: instruction,
80101
type: "string",
81102
},
82-
// TODO: check if we want to log this
83103
schema: {
84-
value: schema,
104+
value: JSON.stringify(schema),
85105
type: "object",
86106
},
87107
},
88108
});
89-
// Evaluate the schema string to get the actual Zod schema
90-
const zodSchema = evaluateZodSchema(schema, logger);
91109

92-
// Ensure we have a ZodObject
110+
const zodSchema = jsonSchemaToZod(schema as JsonSchema);
111+
93112
const schemaObject =
94113
zodSchema instanceof z.ZodObject
95114
? zodSchema
96115
: z.object({ result: zodSchema });
97116

98-
// Extract with the schema - only pass modelName if executionModel is explicitly provided
99117
const result = await stagehand.page.extract({
100118
instruction,
101119
schema: schemaObject,

lib/agent/tools/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export function createAgentTools(
2626
act: createActTool(stagehand, executionModel),
2727
ariaTree: createAriaTreeTool(stagehand),
2828
close: createCloseTool(),
29-
extract: createExtractTool(stagehand, executionModel, options?.logger),
29+
extract: createExtractTool(stagehand, executionModel),
3030
fillForm: createFillFormTool(stagehand, executionModel),
3131
goto: createGotoTool(stagehand),
3232
navback: createNavBackTool(stagehand),

0 commit comments

Comments
 (0)