Skip to content

Commit 19685ea

Browse files
committed
Add Structured Output Methods
1 parent 4112d58 commit 19685ea

File tree

9 files changed

+235
-1
lines changed

9 files changed

+235
-1
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ dist-deno
88
/*.tgz
99
.idea/
1010

11+
.env

.nvmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
v22.12.0

examples/demo.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
#!/usr/bin/env -S npm run tsn -T
2+
3+
import { BrowserUse } from 'browser-use-sdk';
4+
import { spinner } from './utils';
5+
import { TaskView } from 'browser-use-sdk/resources';
6+
7+
// gets API Key from environment variable BROWSER_USE_API_KEY
8+
const browseruse = new BrowserUse();
9+
10+
async function main() {
11+
let log = 'starting';
12+
const stop = spinner(() => log);
13+
14+
// Create Task
15+
const rsp = await browseruse.tasks.create({
16+
task: "What's the weather line in SF and what's the temperature?",
17+
});
18+
19+
poll: do {
20+
// Wait for Task to Finish
21+
const status = (await browseruse.tasks.retrieve(rsp.id, { statusOnly: false })) as TaskView;
22+
23+
switch (status.status) {
24+
case 'started':
25+
case 'paused':
26+
case 'stopped':
27+
log = `agent ${status.status} - live: ${status.sessionLiveUrl}`;
28+
29+
await new Promise((resolve) => setTimeout(resolve, 2000));
30+
break;
31+
32+
case 'finished':
33+
stop();
34+
35+
console.log(status.doneOutput);
36+
break poll;
37+
}
38+
} while (true);
39+
}
40+
41+
main().catch(console.error);

examples/structured-output.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
#!/usr/bin/env -S npm run tsn -T
2+
3+
import { BrowserUse } from 'browser-use-sdk';
4+
import { z } from 'zod';
5+
import { spinner } from './utils';
6+
7+
// gets API Key from environment variable BROWSER_USE_API_KEY
8+
const browseruse = new BrowserUse();
9+
10+
const HackerNewsResponse = z.object({
11+
title: z.string(),
12+
url: z.string(),
13+
score: z.number(),
14+
});
15+
16+
const TaskOutput = z.object({
17+
posts: z.array(HackerNewsResponse),
18+
});
19+
20+
async function main() {
21+
const rsp = await browseruse.tasks.createWithStructuredOutput({
22+
task: 'Extract top 10 Hacker News posts and return the title, url, and score',
23+
structuredOutputJson: TaskOutput,
24+
});
25+
26+
let latestStatusText = 'starting';
27+
28+
const stop = spinner(() => latestStatusText);
29+
30+
poll: do {
31+
const status = await browseruse.tasks.retrieveWithStructuredOutput(rsp.id, {
32+
structuredOutputJson: TaskOutput,
33+
});
34+
35+
switch (status.status) {
36+
case 'started':
37+
case 'paused':
38+
case 'stopped': {
39+
const stepsCount = status.steps ? status.steps.length : 0;
40+
const steps = `${stepsCount} steps`;
41+
const lastGoalDescription = stepsCount > 0 ? status.steps![stepsCount - 1]!.nextGoal : undefined;
42+
const lastGoal = lastGoalDescription ? `, last: ${lastGoalDescription}` : '';
43+
const liveUrl = status.sessionLiveUrl ? `, live: ${status.sessionLiveUrl}` : '';
44+
45+
latestStatusText = `agent ${status.status} (${steps}${lastGoal}${liveUrl}) `;
46+
47+
await new Promise((resolve) => setTimeout(resolve, 2000));
48+
}
49+
50+
case 'finished':
51+
stop();
52+
53+
console.log('TOP POSTS:');
54+
55+
for (const post of status.doneOutput!.posts) {
56+
console.log(` - ${post.title} (${post.score}) ${post.url}`);
57+
}
58+
59+
break poll;
60+
}
61+
} while (true);
62+
}
63+
64+
main().catch(console.error);

examples/utils.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
2+
3+
/**
4+
* Start a spinner that updates the text every 100ms.
5+
*
6+
* @param renderText - A function that returns the text to display.
7+
* @returns A function to stop the spinner.
8+
*/
9+
export function spinner(renderText: () => string): () => void {
10+
let frameIndex = 0;
11+
const interval = setInterval(() => {
12+
const frame = SPINNER_FRAMES[frameIndex++ % SPINNER_FRAMES.length];
13+
const text = `${frame} ${renderText()}`;
14+
if (typeof process.stdout.clearLine === 'function') {
15+
process.stdout.clearLine(0);
16+
process.stdout.cursorTo(0);
17+
}
18+
process.stdout.write(text);
19+
}, 100);
20+
21+
return () => {
22+
clearInterval(interval);
23+
if (typeof process.stdout.clearLine === 'function') {
24+
process.stdout.clearLine(0);
25+
process.stdout.cursorTo(0);
26+
}
27+
};
28+
}

package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,11 @@
4747
"tsc-multi": "https://github.com/stainless-api/tsc-multi/releases/download/v1.1.8/tsc-multi.tgz",
4848
"tsconfig-paths": "^4.0.0",
4949
"typescript": "5.8.3",
50-
"typescript-eslint": "8.31.1"
50+
"typescript-eslint": "8.31.1",
51+
"zod": "^4.0.17"
52+
},
53+
"peerDependencies": {
54+
"zod": "^4.0.17"
5155
},
5256
"exports": {
5357
".": {

src/lib/parse.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import z from 'zod';
2+
import type { TaskCreateParams, TaskRetrieveParams, TaskView } from '../resources/tasks';
3+
4+
// RUN
5+
6+
export type RunTaskCreateParamsWithStructuredOutput<T extends z.ZodTypeAny> = Omit<
7+
TaskCreateParams,
8+
'structuredOutputJson'
9+
> & {
10+
structuredOutputJson: T;
11+
};
12+
13+
export function stringifyStructuredOutput<T extends z.ZodTypeAny>(
14+
req: RunTaskCreateParamsWithStructuredOutput<T>,
15+
): TaskCreateParams {
16+
return {
17+
...req,
18+
structuredOutputJson: JSON.stringify(z.toJSONSchema(req.structuredOutputJson)),
19+
};
20+
}
21+
22+
// RETRIEVE
23+
24+
export type GetTaskStatusParamsWithStructuredOutput<T extends z.ZodTypeAny> = Omit<
25+
TaskRetrieveParams,
26+
'statusOnly'
27+
> & {
28+
statusOnly?: false;
29+
structuredOutputJson: T;
30+
};
31+
32+
export type TaskViewWithStructuredOutput<T extends z.ZodTypeAny> = Omit<TaskView, 'doneOutput'> & {
33+
doneOutput: z.output<T> | null;
34+
};
35+
36+
export function parseStructuredTaskOutput<T extends z.ZodTypeAny>(
37+
res: TaskView,
38+
body: GetTaskStatusParamsWithStructuredOutput<T>,
39+
): TaskViewWithStructuredOutput<T> {
40+
try {
41+
const parsed = JSON.parse(res.doneOutput);
42+
43+
const response = body.structuredOutputJson.safeParse(parsed);
44+
if (!response.success) {
45+
throw new Error(`Invalid structured output: ${response.error.message}`);
46+
}
47+
48+
return { ...res, doneOutput: response.data };
49+
} catch (e) {
50+
if (e instanceof SyntaxError) {
51+
return {
52+
...res,
53+
doneOutput: null,
54+
};
55+
}
56+
throw e;
57+
}
58+
}

src/resources/tasks.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
11
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
22

3+
import z from 'zod';
4+
35
import { APIResource } from '../core/resource';
46
import * as TasksAPI from './tasks';
57
import { APIPromise } from '../core/api-promise';
68
import { RequestOptions } from '../internal/request-options';
79
import { path } from '../internal/utils/path';
10+
import {
11+
parseStructuredTaskOutput,
12+
stringifyStructuredOutput,
13+
TaskViewWithStructuredOutput,
14+
type GetTaskStatusParamsWithStructuredOutput,
15+
type RunTaskCreateParamsWithStructuredOutput,
16+
} from '../lib/parse';
817

918
export class Tasks extends APIResource {
1019
/**
@@ -14,6 +23,15 @@ export class Tasks extends APIResource {
1423
return this._client.post('/tasks', { body, ...options });
1524
}
1625

26+
createWithStructuredOutput<T extends z.ZodTypeAny>(
27+
body: RunTaskCreateParamsWithStructuredOutput<T>,
28+
options?: RequestOptions,
29+
): APIPromise<TaskViewWithStructuredOutput<T>> {
30+
return this.create(stringifyStructuredOutput(body), options)._thenUnwrap((rsp) =>
31+
parseStructuredTaskOutput(rsp as TaskView, body),
32+
);
33+
}
34+
1735
/**
1836
* Get Task
1937
*/
@@ -25,6 +43,20 @@ export class Tasks extends APIResource {
2543
return this._client.get(path`/tasks/${taskID}`, { query, ...options });
2644
}
2745

46+
retrieveWithStructuredOutput<T extends z.ZodTypeAny>(
47+
taskID: string,
48+
query: GetTaskStatusParamsWithStructuredOutput<T>,
49+
options?: RequestOptions,
50+
): APIPromise<TaskViewWithStructuredOutput<T>> {
51+
// NOTE: We manually remove structuredOutputJson from the query object because
52+
// it's not a valid Browser Use Cloud parameter.
53+
const { structuredOutputJson, ...rest } = query;
54+
55+
return this.retrieve(taskID, rest, options)._thenUnwrap((rsp) =>
56+
parseStructuredTaskOutput(rsp as TaskView, query),
57+
);
58+
}
59+
2860
/**
2961
* Update Task
3062
*/

yarn.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3498,3 +3498,8 @@ yocto-queue@^0.1.0:
34983498
version "0.1.0"
34993499
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
35003500
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
3501+
3502+
zod@^4.0.17:
3503+
version "4.0.17"
3504+
resolved "https://registry.yarnpkg.com/zod/-/zod-4.0.17.tgz#95931170715f73f7426c385c237b7477750d6c8d"
3505+
integrity sha512-1PHjlYRevNxxdy2JZ8JcNAw7rX8V9P1AKkP+x/xZfxB0K5FYfuV+Ug6P/6NVSR2jHQ+FzDDoDHS04nYUsOIyLQ==

0 commit comments

Comments
 (0)