@browserbasehq/stagehand 1.4.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/cache/ActionCache.ts +158 -0
- package/lib/cache/BaseCache.ts +553 -0
- package/lib/cache/LLMCache.ts +48 -0
- package/lib/cache.ts +99 -0
- package/lib/dom/build/index.js +626 -0
- package/lib/dom/build/scriptContent.ts +1 -0
- package/lib/dom/debug.ts +147 -0
- package/lib/dom/genDomScripts.ts +29 -0
- package/lib/dom/global.d.ts +25 -0
- package/lib/dom/index.ts +3 -0
- package/lib/dom/process.ts +441 -0
- package/lib/dom/utils.ts +17 -0
- package/lib/dom/xpathUtils.ts +246 -0
- package/lib/handlers/actHandler.ts +1421 -0
- package/lib/handlers/extractHandler.ts +179 -0
- package/lib/handlers/observeHandler.ts +170 -0
- package/lib/index.ts +900 -0
- package/lib/inference.ts +324 -0
- package/lib/llm/AnthropicClient.ts +314 -0
- package/lib/llm/LLMClient.ts +66 -0
- package/lib/llm/LLMProvider.ts +81 -0
- package/lib/llm/OpenAIClient.ts +206 -0
- package/lib/prompt.ts +341 -0
- package/lib/utils.ts +16 -0
- package/lib/vision.ts +299 -0
- package/package.json +3 -3
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { OpenAIClient } from "./OpenAIClient";
|
|
2
|
+
import { AnthropicClient } from "./AnthropicClient";
|
|
3
|
+
import { LLMClient } from "./LLMClient";
|
|
4
|
+
import { LLMCache } from "../cache/LLMCache";
|
|
5
|
+
import { LogLine } from "../../types/log";
|
|
6
|
+
import {
|
|
7
|
+
AvailableModel,
|
|
8
|
+
ModelProvider,
|
|
9
|
+
ClientOptions,
|
|
10
|
+
} from "../../types/model";
|
|
11
|
+
|
|
12
|
+
export class LLMProvider {
|
|
13
|
+
private modelToProviderMap: { [key in AvailableModel]: ModelProvider } = {
|
|
14
|
+
"gpt-4o": "openai",
|
|
15
|
+
"gpt-4o-mini": "openai",
|
|
16
|
+
"gpt-4o-2024-08-06": "openai",
|
|
17
|
+
"claude-3-5-sonnet-latest": "anthropic",
|
|
18
|
+
"claude-3-5-sonnet-20240620": "anthropic",
|
|
19
|
+
"claude-3-5-sonnet-20241022": "anthropic",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
private logger: (message: LogLine) => void;
|
|
23
|
+
private enableCaching: boolean;
|
|
24
|
+
private cache: LLMCache | undefined;
|
|
25
|
+
|
|
26
|
+
constructor(logger: (message: LogLine) => void, enableCaching: boolean) {
|
|
27
|
+
this.logger = logger;
|
|
28
|
+
this.enableCaching = enableCaching;
|
|
29
|
+
this.cache = enableCaching ? new LLMCache(logger) : undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
cleanRequestCache(requestId: string): void {
|
|
33
|
+
if (!this.enableCaching) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
this.logger({
|
|
38
|
+
category: "llm_cache",
|
|
39
|
+
message: "cleaning up cache",
|
|
40
|
+
level: 1,
|
|
41
|
+
auxiliary: {
|
|
42
|
+
requestId: {
|
|
43
|
+
value: requestId,
|
|
44
|
+
type: "string",
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
this.cache.deleteCacheForRequestId(requestId);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
getClient(
|
|
52
|
+
modelName: AvailableModel,
|
|
53
|
+
clientOptions?: ClientOptions,
|
|
54
|
+
): LLMClient {
|
|
55
|
+
const provider = this.modelToProviderMap[modelName];
|
|
56
|
+
if (!provider) {
|
|
57
|
+
throw new Error(`Unsupported model: ${modelName}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
switch (provider) {
|
|
61
|
+
case "openai":
|
|
62
|
+
return new OpenAIClient(
|
|
63
|
+
this.logger,
|
|
64
|
+
this.enableCaching,
|
|
65
|
+
this.cache,
|
|
66
|
+
modelName,
|
|
67
|
+
clientOptions,
|
|
68
|
+
);
|
|
69
|
+
case "anthropic":
|
|
70
|
+
return new AnthropicClient(
|
|
71
|
+
this.logger,
|
|
72
|
+
this.enableCaching,
|
|
73
|
+
this.cache,
|
|
74
|
+
modelName,
|
|
75
|
+
clientOptions,
|
|
76
|
+
);
|
|
77
|
+
default:
|
|
78
|
+
throw new Error(`Unsupported provider: ${provider}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import OpenAI, { ClientOptions } from "openai";
|
|
2
|
+
import { zodResponseFormat } from "openai/helpers/zod";
|
|
3
|
+
import { ChatCompletionCreateParamsNonStreaming } from "openai/resources/chat";
|
|
4
|
+
import { LogLine } from "../../types/log";
|
|
5
|
+
import { AvailableModel } from "../../types/model";
|
|
6
|
+
import { LLMCache } from "../cache/LLMCache";
|
|
7
|
+
import { ChatCompletionOptions, ChatMessage, LLMClient } from "./LLMClient";
|
|
8
|
+
|
|
9
|
+
export class OpenAIClient extends LLMClient {
|
|
10
|
+
private client: OpenAI;
|
|
11
|
+
private cache: LLMCache | undefined;
|
|
12
|
+
public logger: (message: LogLine) => void;
|
|
13
|
+
private enableCaching: boolean;
|
|
14
|
+
private clientOptions: ClientOptions;
|
|
15
|
+
|
|
16
|
+
constructor(
|
|
17
|
+
logger: (message: LogLine) => void,
|
|
18
|
+
enableCaching = false,
|
|
19
|
+
cache: LLMCache | undefined,
|
|
20
|
+
modelName: AvailableModel,
|
|
21
|
+
clientOptions?: ClientOptions,
|
|
22
|
+
) {
|
|
23
|
+
super(modelName);
|
|
24
|
+
this.client = new OpenAI(clientOptions);
|
|
25
|
+
this.logger = logger;
|
|
26
|
+
this.cache = cache;
|
|
27
|
+
this.enableCaching = enableCaching;
|
|
28
|
+
this.modelName = modelName;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async createChatCompletion(options: ChatCompletionOptions) {
|
|
32
|
+
const { image: _, ...optionsWithoutImage } = options;
|
|
33
|
+
this.logger({
|
|
34
|
+
category: "openai",
|
|
35
|
+
message: "creating chat completion",
|
|
36
|
+
level: 1,
|
|
37
|
+
auxiliary: {
|
|
38
|
+
options: {
|
|
39
|
+
value: JSON.stringify(optionsWithoutImage),
|
|
40
|
+
type: "object",
|
|
41
|
+
},
|
|
42
|
+
modelName: {
|
|
43
|
+
value: this.modelName,
|
|
44
|
+
type: "string",
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
const cacheOptions = {
|
|
49
|
+
model: this.modelName,
|
|
50
|
+
messages: options.messages,
|
|
51
|
+
temperature: options.temperature,
|
|
52
|
+
top_p: options.top_p,
|
|
53
|
+
frequency_penalty: options.frequency_penalty,
|
|
54
|
+
presence_penalty: options.presence_penalty,
|
|
55
|
+
image: options.image,
|
|
56
|
+
response_model: options.response_model,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
if (this.enableCaching) {
|
|
60
|
+
const cachedResponse = await this.cache.get(
|
|
61
|
+
cacheOptions,
|
|
62
|
+
options.requestId,
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
if (cachedResponse) {
|
|
66
|
+
this.logger({
|
|
67
|
+
category: "llm_cache",
|
|
68
|
+
message: "LLM cache hit - returning cached response",
|
|
69
|
+
level: 1,
|
|
70
|
+
auxiliary: {
|
|
71
|
+
requestId: {
|
|
72
|
+
value: options.requestId,
|
|
73
|
+
type: "string",
|
|
74
|
+
},
|
|
75
|
+
cachedResponse: {
|
|
76
|
+
value: JSON.stringify(cachedResponse),
|
|
77
|
+
type: "object",
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
return cachedResponse;
|
|
82
|
+
} else {
|
|
83
|
+
this.logger({
|
|
84
|
+
category: "llm_cache",
|
|
85
|
+
message: "LLM cache miss - no cached response found",
|
|
86
|
+
level: 1,
|
|
87
|
+
auxiliary: {
|
|
88
|
+
requestId: {
|
|
89
|
+
value: options.requestId,
|
|
90
|
+
type: "string",
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (options.image) {
|
|
98
|
+
const screenshotMessage: ChatMessage = {
|
|
99
|
+
role: "user",
|
|
100
|
+
content: [
|
|
101
|
+
{
|
|
102
|
+
type: "image_url",
|
|
103
|
+
image_url: {
|
|
104
|
+
url: `data:image/jpeg;base64,${options.image.buffer.toString("base64")}`,
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
...(options.image.description
|
|
108
|
+
? [{ type: "text", text: options.image.description }]
|
|
109
|
+
: []),
|
|
110
|
+
],
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
options.messages = [...options.messages, screenshotMessage];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const { image, response_model, requestId, ...openAiOptions } = {
|
|
117
|
+
...options,
|
|
118
|
+
model: this.modelName,
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
let responseFormat = undefined;
|
|
122
|
+
if (options.response_model) {
|
|
123
|
+
responseFormat = zodResponseFormat(
|
|
124
|
+
options.response_model.schema,
|
|
125
|
+
options.response_model.name,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
this.logger({
|
|
130
|
+
category: "openai",
|
|
131
|
+
message: "creating chat completion",
|
|
132
|
+
level: 1,
|
|
133
|
+
auxiliary: {
|
|
134
|
+
openAiOptions: {
|
|
135
|
+
value: JSON.stringify(openAiOptions),
|
|
136
|
+
type: "object",
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const response = await this.client.chat.completions.create({
|
|
142
|
+
...openAiOptions,
|
|
143
|
+
response_format: responseFormat,
|
|
144
|
+
} as unknown as ChatCompletionCreateParamsNonStreaming); // TODO (kamath): remove this forced typecast
|
|
145
|
+
|
|
146
|
+
this.logger({
|
|
147
|
+
category: "openai",
|
|
148
|
+
message: "response",
|
|
149
|
+
level: 1,
|
|
150
|
+
auxiliary: {
|
|
151
|
+
response: {
|
|
152
|
+
value: JSON.stringify(response),
|
|
153
|
+
type: "object",
|
|
154
|
+
},
|
|
155
|
+
requestId: {
|
|
156
|
+
value: options.requestId,
|
|
157
|
+
type: "string",
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
if (response_model) {
|
|
163
|
+
const extractedData = response.choices[0].message.content;
|
|
164
|
+
const parsedData = JSON.parse(extractedData);
|
|
165
|
+
|
|
166
|
+
if (this.enableCaching) {
|
|
167
|
+
this.cache.set(
|
|
168
|
+
cacheOptions,
|
|
169
|
+
{
|
|
170
|
+
...parsedData,
|
|
171
|
+
},
|
|
172
|
+
options.requestId,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
...parsedData,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (this.enableCaching) {
|
|
182
|
+
this.logger({
|
|
183
|
+
category: "llm_cache",
|
|
184
|
+
message: "caching response",
|
|
185
|
+
level: 1,
|
|
186
|
+
auxiliary: {
|
|
187
|
+
requestId: {
|
|
188
|
+
value: options.requestId,
|
|
189
|
+
type: "string",
|
|
190
|
+
},
|
|
191
|
+
cacheOptions: {
|
|
192
|
+
value: JSON.stringify(cacheOptions),
|
|
193
|
+
type: "object",
|
|
194
|
+
},
|
|
195
|
+
response: {
|
|
196
|
+
value: JSON.stringify(response),
|
|
197
|
+
type: "object",
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
this.cache.set(cacheOptions, response, options.requestId);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return response;
|
|
205
|
+
}
|
|
206
|
+
}
|
package/lib/prompt.ts
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import OpenAI from "openai";
|
|
2
|
+
import { ChatMessage } from "./llm/LLMClient";
|
|
3
|
+
|
|
4
|
+
// act
|
|
5
|
+
const actSystemPrompt = `
|
|
6
|
+
# Instructions
|
|
7
|
+
You are a browser automation assistant. Your job is to accomplish the user's goal across multiple model calls by running playwright commands.
|
|
8
|
+
|
|
9
|
+
## Input
|
|
10
|
+
You will receive:
|
|
11
|
+
1. the user's overall goal
|
|
12
|
+
2. the steps that you've taken so far
|
|
13
|
+
3. a list of active DOM elements in this chunk to consider to get closer to the goal.
|
|
14
|
+
4. Optionally, a list of variable names that the user has provided that you may use to accomplish the goal. To use the variables, you must use the special <|VARIABLE_NAME|> syntax.
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
## Your Goal / Specification
|
|
18
|
+
You have 2 tools that you can call: doAction, and skipSection. Do action only performs Playwright actions. Do exactly what the user's goal is. Do not perform any other actions or exceed the scope of the goal.
|
|
19
|
+
If the user's goal will be accomplished after running the playwright action, set completed to true. Better to have completed set to true if your are not sure.
|
|
20
|
+
|
|
21
|
+
Note: If there is a popup on the page for cookies or advertising that has nothing to do with the goal, try to close it first before proceeding. As this can block the goal from being completed.
|
|
22
|
+
|
|
23
|
+
Again, if the user's goal will be accomplished after running the playwright action, set completed to true.
|
|
24
|
+
`;
|
|
25
|
+
|
|
26
|
+
const verifyActCompletionSystemPrompt = `
|
|
27
|
+
You are a browser automation assistant. The job has given you a goal and a list of steps that have been taken so far. Your job is to determine if the user's goal has been completed based on the provided information.
|
|
28
|
+
|
|
29
|
+
# Input
|
|
30
|
+
You will receive:
|
|
31
|
+
1. The user's goal: A clear description of what the user wants to achieve.
|
|
32
|
+
2. Steps taken so far: A list of actions that have been performed up to this point.
|
|
33
|
+
3. An image of the current page
|
|
34
|
+
|
|
35
|
+
# Your Task
|
|
36
|
+
Analyze the provided information to determine if the user's goal has been fully completed.
|
|
37
|
+
|
|
38
|
+
# Output
|
|
39
|
+
Return a boolean value:
|
|
40
|
+
- true: If the goal has been definitively completed based on the steps taken and the current page.
|
|
41
|
+
- false: If the goal has not been completed or if there's any uncertainty about its completion.
|
|
42
|
+
|
|
43
|
+
# Important Considerations
|
|
44
|
+
- False positives are okay. False negatives are not okay.
|
|
45
|
+
- Look for evidence of errors on the page or something having gone wrong in completing the goal. If one does not exist, return true.
|
|
46
|
+
`;
|
|
47
|
+
|
|
48
|
+
// ## Examples for completion check
|
|
49
|
+
// ### Example 1
|
|
50
|
+
// 1. User's goal: "input data scientist into role"
|
|
51
|
+
// 2. Steps you've taken so far: "The role input field was filled with 'data scientist'."
|
|
52
|
+
// 3. Active DOM elements: ["<input id="c9" class="VfPpkd-fmcmS-wGMbrd " aria-expanded="false" data-axe="mdc-autocomplete">data scientist</input>", "<button class="VfPpkd-LgbsSe VfPpkd-LgbsSe-OWXEXe-INsAgc lJ9FBc nDgy9d" type="submit">Search</button>"]
|
|
53
|
+
|
|
54
|
+
// Output: Will need to have completed set to true. Nothing else matters.
|
|
55
|
+
// Reasoning: The goal the user set has already been accomplished. We should not take any extra actions outside of the scope of the goal (for example, clicking on the search button is an invalid action - ie: not acceptable).
|
|
56
|
+
|
|
57
|
+
// ### Example 2
|
|
58
|
+
// 1. User's goal: "Sign up for the newsletter"
|
|
59
|
+
// 2. Steps you've taken so far: ["The email input field was filled with 'test@test.com'."]
|
|
60
|
+
// 3. Active DOM elements: ["<input type='email' id='newsletter-email' placeholder='Enter your email'></input>", "<button id='subscribe-button'>Subscribe</button>"]
|
|
61
|
+
|
|
62
|
+
// Output: Will need to have click on the subscribe button as action. And completed set to false.
|
|
63
|
+
// Reasoning: There might be an error when trying to submit the form and you need to make sure the goal is accomplished properly. So you set completed to false.
|
|
64
|
+
|
|
65
|
+
export function buildVerifyActCompletionSystemPrompt(): ChatMessage {
|
|
66
|
+
return {
|
|
67
|
+
role: "system",
|
|
68
|
+
content: verifyActCompletionSystemPrompt,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function buildVerifyActCompletionUserPrompt(
|
|
73
|
+
goal: string,
|
|
74
|
+
steps = "None",
|
|
75
|
+
domElements: string | undefined,
|
|
76
|
+
): ChatMessage {
|
|
77
|
+
let actUserPrompt = `
|
|
78
|
+
# My Goal
|
|
79
|
+
${goal}
|
|
80
|
+
|
|
81
|
+
# Steps You've Taken So Far
|
|
82
|
+
${steps}
|
|
83
|
+
`;
|
|
84
|
+
|
|
85
|
+
if (domElements) {
|
|
86
|
+
actUserPrompt += `
|
|
87
|
+
# Active DOM Elements on the current page
|
|
88
|
+
${domElements}
|
|
89
|
+
`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
role: "user",
|
|
94
|
+
content: actUserPrompt,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function buildActSystemPrompt(): ChatMessage {
|
|
99
|
+
return {
|
|
100
|
+
role: "system",
|
|
101
|
+
content: actSystemPrompt,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function buildActUserPrompt(
|
|
106
|
+
action: string,
|
|
107
|
+
steps = "None",
|
|
108
|
+
domElements: string,
|
|
109
|
+
variables?: Record<string, string>,
|
|
110
|
+
): ChatMessage {
|
|
111
|
+
let actUserPrompt = `
|
|
112
|
+
# My Goal
|
|
113
|
+
${action}
|
|
114
|
+
|
|
115
|
+
# Steps You've Taken So Far
|
|
116
|
+
${steps}
|
|
117
|
+
|
|
118
|
+
# Current Active Dom Elements
|
|
119
|
+
${domElements}
|
|
120
|
+
`;
|
|
121
|
+
|
|
122
|
+
if (variables && Object.keys(variables).length > 0) {
|
|
123
|
+
actUserPrompt += `
|
|
124
|
+
# Variables
|
|
125
|
+
${Object.entries(variables)
|
|
126
|
+
.map(([key, value]) => `<|${key.toUpperCase()}|>`)
|
|
127
|
+
.join("\n")}
|
|
128
|
+
`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
role: "user",
|
|
133
|
+
content: actUserPrompt,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export const actTools: Array<OpenAI.ChatCompletionTool> = [
|
|
138
|
+
{
|
|
139
|
+
type: "function",
|
|
140
|
+
function: {
|
|
141
|
+
name: "doAction",
|
|
142
|
+
description:
|
|
143
|
+
"execute the next playwright step that directly accomplishes the goal",
|
|
144
|
+
parameters: {
|
|
145
|
+
type: "object",
|
|
146
|
+
required: ["method", "element", "args", "step", "completed"],
|
|
147
|
+
properties: {
|
|
148
|
+
method: {
|
|
149
|
+
type: "string",
|
|
150
|
+
description: "The playwright function to call.",
|
|
151
|
+
},
|
|
152
|
+
element: {
|
|
153
|
+
type: "number",
|
|
154
|
+
description: "The element number to act on",
|
|
155
|
+
},
|
|
156
|
+
args: {
|
|
157
|
+
type: "array",
|
|
158
|
+
description: "The required arguments",
|
|
159
|
+
items: {
|
|
160
|
+
type: "string",
|
|
161
|
+
description: "The argument to pass to the function",
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
step: {
|
|
165
|
+
type: "string",
|
|
166
|
+
description:
|
|
167
|
+
"human readable description of the step that is taken in the past tense. Please be very detailed.",
|
|
168
|
+
},
|
|
169
|
+
why: {
|
|
170
|
+
type: "string",
|
|
171
|
+
description:
|
|
172
|
+
"why is this step taken? how does it advance the goal?",
|
|
173
|
+
},
|
|
174
|
+
completed: {
|
|
175
|
+
type: "boolean",
|
|
176
|
+
description:
|
|
177
|
+
"true if the goal should be accomplished after this step",
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
type: "function",
|
|
185
|
+
function: {
|
|
186
|
+
name: "skipSection",
|
|
187
|
+
description:
|
|
188
|
+
"skips this area of the webpage because the current goal cannot be accomplished here",
|
|
189
|
+
parameters: {
|
|
190
|
+
type: "object",
|
|
191
|
+
properties: {
|
|
192
|
+
reason: {
|
|
193
|
+
type: "string",
|
|
194
|
+
description: "reason that no action is taken",
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
];
|
|
201
|
+
|
|
202
|
+
// extract
|
|
203
|
+
const extractSystemPrompt = `You are extracting content on behalf of a user. You will be given:
|
|
204
|
+
1. An instruction
|
|
205
|
+
2. A list of DOM elements to extract from
|
|
206
|
+
|
|
207
|
+
Print the exact text from the DOM elements with all symbols, characters, and endlines as is.
|
|
208
|
+
Print null or an empty string if no new information is found.
|
|
209
|
+
|
|
210
|
+
ONLY print the content using the print_extracted_data tool provided.
|
|
211
|
+
ONLY print the content using the print_extracted_data tool provided.
|
|
212
|
+
`;
|
|
213
|
+
|
|
214
|
+
export function buildExtractSystemPrompt(): ChatMessage {
|
|
215
|
+
const content = extractSystemPrompt.replace(/\s+/g, " ");
|
|
216
|
+
return {
|
|
217
|
+
role: "system",
|
|
218
|
+
content,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function buildExtractUserPrompt(
|
|
223
|
+
instruction: string,
|
|
224
|
+
domElements: string,
|
|
225
|
+
): ChatMessage {
|
|
226
|
+
return {
|
|
227
|
+
role: "user",
|
|
228
|
+
content: `Instruction: ${instruction}
|
|
229
|
+
DOM: ${domElements}
|
|
230
|
+
|
|
231
|
+
ONLY print the content using the print_extracted_data tool provided.
|
|
232
|
+
ONLY print the content using the print_extracted_data tool provided.`,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const refineSystemPrompt = `You are tasked with refining and filtering information for the final output based on newly extracted and previously extracted content. Your responsibilities are:
|
|
237
|
+
1. Remove exact duplicates for elements in arrays and objects.
|
|
238
|
+
2. For text fields, append or update relevant text if the new content is an extension, replacement, or continuation.
|
|
239
|
+
3. For non-text fields (e.g., numbers, booleans), update with new values if they differ.
|
|
240
|
+
4. Add any completely new fields or objects.
|
|
241
|
+
|
|
242
|
+
Return the updated content that includes both the previous content and the new, non-duplicate, or extended information.`;
|
|
243
|
+
|
|
244
|
+
export function buildRefineSystemPrompt(): ChatMessage {
|
|
245
|
+
return {
|
|
246
|
+
role: "system",
|
|
247
|
+
content: refineSystemPrompt,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function buildRefineUserPrompt(
|
|
252
|
+
instruction: string,
|
|
253
|
+
previouslyExtractedContent: object,
|
|
254
|
+
newlyExtractedContent: object,
|
|
255
|
+
): ChatMessage {
|
|
256
|
+
return {
|
|
257
|
+
role: "user",
|
|
258
|
+
content: `Instruction: ${instruction}
|
|
259
|
+
Previously extracted content: ${JSON.stringify(previouslyExtractedContent, null, 2)}
|
|
260
|
+
Newly extracted content: ${JSON.stringify(newlyExtractedContent, null, 2)}
|
|
261
|
+
Refined content:`,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const metadataSystemPrompt = `You are an AI assistant tasked with evaluating the progress and completion status of an extraction task.
|
|
266
|
+
Analyze the extraction response and determine if the task is completed or if more information is needed.
|
|
267
|
+
|
|
268
|
+
Strictly abide by the following criteria:
|
|
269
|
+
1. Once the instruction has been satisfied by the current extraction response, ALWAYS set completion status to true and stop processing, regardless of remaining chunks.
|
|
270
|
+
2. Only set completion status to false if BOTH of these conditions are true:
|
|
271
|
+
- The instruction has not been satisfied yet
|
|
272
|
+
- There are still chunks left to process (chunksTotal > chunksSeen)`;
|
|
273
|
+
|
|
274
|
+
export function buildMetadataSystemPrompt(): ChatMessage {
|
|
275
|
+
return {
|
|
276
|
+
role: "system",
|
|
277
|
+
content: metadataSystemPrompt,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function buildMetadataPrompt(
|
|
282
|
+
instruction: string,
|
|
283
|
+
extractionResponse: object,
|
|
284
|
+
chunksSeen: number,
|
|
285
|
+
chunksTotal: number,
|
|
286
|
+
): ChatMessage {
|
|
287
|
+
return {
|
|
288
|
+
role: "user",
|
|
289
|
+
content: `Instruction: ${instruction}
|
|
290
|
+
Extracted content: ${JSON.stringify(extractionResponse, null, 2)}
|
|
291
|
+
chunksSeen: ${chunksSeen}
|
|
292
|
+
chunksTotal: ${chunksTotal}`,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// observe
|
|
297
|
+
const observeSystemPrompt = `
|
|
298
|
+
You are helping the user automate the browser by finding elements based on what the user wants to observe in the page.
|
|
299
|
+
You will be given:
|
|
300
|
+
1. a instruction of elements to observe
|
|
301
|
+
2. a numbered list of possible elements or an annotated image of the page
|
|
302
|
+
|
|
303
|
+
Return an array of elements that match the instruction.
|
|
304
|
+
`;
|
|
305
|
+
export function buildObserveSystemPrompt(): ChatMessage {
|
|
306
|
+
const content = observeSystemPrompt.replace(/\s+/g, " ");
|
|
307
|
+
|
|
308
|
+
return {
|
|
309
|
+
role: "system",
|
|
310
|
+
content,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export function buildObserveUserMessage(
|
|
315
|
+
instruction: string,
|
|
316
|
+
domElements: string,
|
|
317
|
+
): ChatMessage {
|
|
318
|
+
return {
|
|
319
|
+
role: "user",
|
|
320
|
+
content: `instruction: ${instruction}
|
|
321
|
+
DOM: ${domElements}`,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// ask
|
|
326
|
+
const askSystemPrompt = `
|
|
327
|
+
you are a simple question answering assistent given the user's question. respond with only the answer.
|
|
328
|
+
`;
|
|
329
|
+
export function buildAskSystemPrompt(): ChatMessage {
|
|
330
|
+
return {
|
|
331
|
+
role: "system",
|
|
332
|
+
content: askSystemPrompt,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export function buildAskUserPrompt(question: string): ChatMessage {
|
|
337
|
+
return {
|
|
338
|
+
role: "user",
|
|
339
|
+
content: `question: ${question}`,
|
|
340
|
+
};
|
|
341
|
+
}
|
package/lib/utils.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import crypto from "crypto";
|
|
2
|
+
import { LogLine } from "../types/log";
|
|
3
|
+
|
|
4
|
+
export function generateId(operation: string) {
|
|
5
|
+
return crypto.createHash("sha256").update(operation).digest("hex");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function logLineToString(logLine: LogLine): string {
|
|
9
|
+
const timestamp = logLine.timestamp || new Date().toISOString();
|
|
10
|
+
if (logLine.auxiliary?.error) {
|
|
11
|
+
return `${timestamp}::[stagehand:${logLine.category}] ${logLine.message}\n ${logLine.auxiliary.error.value}\n ${logLine.auxiliary.trace.value}`;
|
|
12
|
+
}
|
|
13
|
+
return `${timestamp}::[stagehand:${logLine.category}] ${logLine.message} ${
|
|
14
|
+
logLine.auxiliary ? JSON.stringify(logLine.auxiliary) : ""
|
|
15
|
+
}`;
|
|
16
|
+
}
|