From f1f4d1c3f72759ceac2238a17e21039b14112e42 Mon Sep 17 00:00:00 2001 From: Anirudh Kamath Date: Wed, 5 Feb 2025 16:28:20 -0800 Subject: [PATCH 1/2] custom-openai --- .env.example | 14 ++- app/api/agent/route.ts | 86 +++++++++------ custom_client.ts | 236 +++++++++++++++++++++++++++++++++++++++++ package.json | 6 +- pnpm-lock.yaml | 156 +++++++++++++++++++++++++-- stagehand.config.ts | 79 ++++++++++++++ utils.ts | 39 +++++++ 7 files changed, 568 insertions(+), 48 deletions(-) create mode 100644 custom_client.ts create mode 100644 stagehand.config.ts create mode 100644 utils.ts diff --git a/.env.example b/.env.example index 11c83e1..d9e8496 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,10 @@ -# OpenAI API Configuration -OPENAI_API_KEY=your_openai_api_key_here - # Browserbase Configuration -BROWSERBASE_API_KEY=your_browserbase_api_key_here -BROWSERBASE_PROJECT_ID=your_browserbase_project_id_here +BROWSERBASE_API_KEY="YOUR_BROWSERBASE_API_KEY" +BROWSERBASE_PROJECT_ID="YOUR_BROWSERBASE_PROJECT_ID" + +# Custom OpenAI Configuration +CUSTOM_OPENAI_MODEL_NAME="YOUR_CUSTOM_OPENAI_MODEL_NAME" + +# Brev Configuration +CUSTOM_OPENAI_API_KEY="YOUR_CUSTOM_OPENAI_API_KEY" +CUSTOM_OPENAI_BASE_URL="YOUR_CUSTOM_OPENAI_BASE_URL" \ No newline at end of file diff --git a/app/api/agent/route.ts b/app/api/agent/route.ts index 0cffea9..9e41821 100644 --- a/app/api/agent/route.ts +++ b/app/api/agent/route.ts @@ -1,8 +1,9 @@ -import { NextResponse } from 'next/server'; +import { NextResponse } from "next/server"; import { openai } from "@ai-sdk/openai"; import { CoreMessage, generateObject, UserContent } from "ai"; import { z } from "zod"; import { ObserveResult, Stagehand } from "@browserbasehq/stagehand"; +import StagehandConfig from "@/stagehand.config"; const LLMClient = openai("gpt-4o"); @@ -19,13 +20,20 @@ async function runStagehand({ instruction, }: { sessionID: string; - method: "GOTO" | "ACT" | "EXTRACT" | "CLOSE" | "SCREENSHOT" | "OBSERVE" | "WAIT" | "NAVBACK"; + method: + | "GOTO" + | "ACT" + | "EXTRACT" + | "CLOSE" + | "SCREENSHOT" + | "OBSERVE" + | "WAIT" + | "NAVBACK"; instruction?: string; }) { const stagehand = new Stagehand({ + ...StagehandConfig, browserbaseSessionID: sessionID, - env: "BROWSERBASE", - logger: () => {}, }); await stagehand.init(); @@ -96,21 +104,24 @@ async function sendPrompt({ try { const stagehand = new Stagehand({ + ...StagehandConfig, browserbaseSessionID: sessionID, - env: "BROWSERBASE" }); await stagehand.init(); currentUrl = await stagehand.page.url(); await stagehand.close(); } catch (error) { - console.error('Error getting page info:', error); + console.error("Error getting page info:", error); } const content: UserContent = [ { type: "text", - text: `Consider the following screenshot of a web page${currentUrl ? ` (URL: ${currentUrl})` : ''}, with the goal being "${goal}". -${previousSteps.length > 0 + text: `Consider the following screenshot of a web page${ + currentUrl ? ` (URL: ${currentUrl})` : "" + }, with the goal being "${goal}". +${ + previousSteps.length > 0 ? `Previous steps taken: ${previousSteps .map( @@ -141,7 +152,10 @@ If the goal has been achieved, return "close".`, ]; // Add screenshot if navigated to a page previously - if (previousSteps.length > 0 && previousSteps.some((step) => step.tool === "GOTO")) { + if ( + previousSteps.length > 0 && + previousSteps.some((step) => step.tool === "GOTO") + ) { content.push({ type: "image", image: (await runStagehand({ @@ -193,32 +207,34 @@ If the goal has been achieved, return "close".`, async function selectStartingUrl(goal: string) { const message: CoreMessage = { role: "user", - content: [{ - type: "text", - text: `Given the goal: "${goal}", determine the best URL to start from. + content: [ + { + type: "text", + text: `Given the goal: "${goal}", determine the best URL to start from. Choose from: 1. A relevant search engine (Google, Bing, etc.) 2. A direct URL if you're confident about the target website 3. Any other appropriate starting point -Return a URL that would be most effective for achieving this goal.` - }] +Return a URL that would be most effective for achieving this goal.`, + }, + ], }; const result = await generateObject({ model: LLMClient, schema: z.object({ url: z.string().url(), - reasoning: z.string() + reasoning: z.string(), }), - messages: [message] + messages: [message], }); return result.object; } export async function GET() { - return NextResponse.json({ message: 'Agent API endpoint ready' }); + return NextResponse.json({ message: "Agent API endpoint ready" }); } export async function POST(request: Request) { @@ -228,17 +244,17 @@ export async function POST(request: Request) { if (!sessionId) { return NextResponse.json( - { error: 'Missing sessionId in request body' }, + { error: "Missing sessionId in request body" }, { status: 400 } ); } // Handle different action types switch (action) { - case 'START': { + case "START": { if (!goal) { return NextResponse.json( - { error: 'Missing goal in request body' }, + { error: "Missing goal in request body" }, { status: 400 } ); } @@ -249,27 +265,27 @@ export async function POST(request: Request) { text: `Navigating to ${url}`, reasoning, tool: "GOTO" as const, - instruction: url + instruction: url, }; - + await runStagehand({ sessionID: sessionId, method: "GOTO", - instruction: url + instruction: url, }); - return NextResponse.json({ + return NextResponse.json({ success: true, result: firstStep, steps: [firstStep], - done: false + done: false, }); } - case 'GET_NEXT_STEP': { + case "GET_NEXT_STEP": { if (!goal) { return NextResponse.json( - { error: 'Missing goal in request body' }, + { error: "Missing goal in request body" }, { status: 400 } ); } @@ -285,15 +301,15 @@ export async function POST(request: Request) { success: true, result, steps: newPreviousSteps, - done: result.tool === "CLOSE" + done: result.tool === "CLOSE", }); } - case 'EXECUTE_STEP': { + case "EXECUTE_STEP": { const { step } = body; if (!step) { return NextResponse.json( - { error: 'Missing step in request body' }, + { error: "Missing step in request body" }, { status: 400 } ); } @@ -308,21 +324,21 @@ export async function POST(request: Request) { return NextResponse.json({ success: true, extraction, - done: step.tool === "CLOSE" + done: step.tool === "CLOSE", }); } default: return NextResponse.json( - { error: 'Invalid action type' }, + { error: "Invalid action type" }, { status: 400 } ); } } catch (error) { - console.error('Error in agent endpoint:', error); + console.error("Error in agent endpoint:", error); return NextResponse.json( - { success: false, error: 'Failed to process request' }, + { success: false, error: "Failed to process request" }, { status: 500 } ); } -} \ No newline at end of file +} diff --git a/custom_client.ts b/custom_client.ts new file mode 100644 index 0000000..3c12985 --- /dev/null +++ b/custom_client.ts @@ -0,0 +1,236 @@ +/** + * Welcome to the Stagehand custom-openai client! + * + * This is a client for the custom-openai API. It is a wrapper around the OpenAI API + * that allows you to create chat completions with custom-openai. + * + * To use this client, you need to have an custom-openai instance running. You can + * start an custom-openai instance by running the following command: + * + * ```bash + * custom-openai run deepseek-r1 + * ``` + */ + +import { + AvailableModel, + CreateChatCompletionOptions, + LLMClient, +} from "@browserbasehq/stagehand"; +import OpenAI, { type ClientOptions } from "openai"; +import { zodResponseFormat } from "openai/helpers/zod"; +import type { + ChatCompletion, + ChatCompletionAssistantMessageParam, + ChatCompletionContentPartImage, + ChatCompletionContentPartText, + ChatCompletionCreateParamsNonStreaming, + ChatCompletionMessageParam, + ChatCompletionSystemMessageParam, + ChatCompletionUserMessageParam, +} from "openai/resources/chat/completions"; +import { validateZodSchema } from "./utils"; + +export class CustomOpenAIClient extends LLMClient { + public type = "custom-openai" as const; + private client: OpenAI; + + constructor({ + modelName = process.env.MODEL_NAME!, + clientOptions, + enableCaching = false, + }: { + modelName?: string; + clientOptions?: ClientOptions; + enableCaching?: boolean; + }) { + if (enableCaching) { + console.warn( + "Caching is not supported yet. Setting enableCaching to true will have no effect." + ); + } + + super(modelName as AvailableModel); + this.client = new OpenAI({ + ...clientOptions, + baseURL: clientOptions?.baseURL || process.env.CUSTOM_OPENAI_BASE_URL!, + apiKey: clientOptions?.apiKey || process.env.CUSTOM_OPENAI_API_KEY!, + }); + this.modelName = modelName as AvailableModel; + } + + async createChatCompletion({ + options, + retries = 3, + logger, + }: CreateChatCompletionOptions): Promise { + const { requestId, ...optionsWithoutImageAndRequestId } = options; + + logger({ + category: "custom-openai", + message: "creating chat completion", + level: 1, + auxiliary: { + options: { + value: JSON.stringify({ + ...optionsWithoutImageAndRequestId, + requestId, + }), + type: "object", + }, + modelName: { + value: this.modelName, + type: "string", + }, + }, + }); + + if (options.image) { + console.warn("Image provided. Vision is not currently supported"); + } + + let responseFormat = undefined; + if (options.response_model) { + responseFormat = zodResponseFormat( + options.response_model.schema, + options.response_model.name + ); + } + + /* eslint-disable */ + // Remove unsupported options + const { response_model, ...customOpenAIOptions } = { + ...optionsWithoutImageAndRequestId, + model: this.modelName, + }; + + logger({ + category: "custom-openai", + message: "creating chat completion", + level: 1, + auxiliary: { + options: { + value: JSON.stringify(options), + type: "object", + }, + }, + }); + + const formattedMessages: ChatCompletionMessageParam[] = + options.messages.map((message) => { + if (Array.isArray(message.content)) { + const contentParts = message.content.map((content) => { + if ("image_url" in content) { + const imageContent: ChatCompletionContentPartImage = { + image_url: { + url: content.image_url.url, + }, + type: "image_url", + }; + return imageContent; + } else { + const textContent: ChatCompletionContentPartText = { + text: content.text, + type: "text", + }; + return textContent; + } + }); + + if (message.role === "system") { + const formattedMessage: ChatCompletionSystemMessageParam = { + ...message, + role: "system", + content: contentParts.filter( + (content): content is ChatCompletionContentPartText => + content.type === "text" + ), + }; + return formattedMessage; + } else if (message.role === "user") { + const formattedMessage: ChatCompletionUserMessageParam = { + ...message, + role: "user", + content: contentParts, + }; + return formattedMessage; + } else { + const formattedMessage: ChatCompletionAssistantMessageParam = { + ...message, + role: "assistant", + content: contentParts.filter( + (content): content is ChatCompletionContentPartText => + content.type === "text" + ), + }; + return formattedMessage; + } + } + + const formattedMessage: ChatCompletionUserMessageParam = { + role: "user", + content: message.content, + }; + + return formattedMessage; + }); + + const body: ChatCompletionCreateParamsNonStreaming = { + ...options, + model: this.modelName, + messages: formattedMessages, + response_format: responseFormat, + stream: false, + tools: options.tools?.map((tool) => ({ + function: { + name: tool.name, + description: tool.description, + parameters: tool.parameters, + }, + type: "function", + })), + }; + + const response = await this.client.chat.completions.create(body); + + logger({ + category: "custom-openai", + message: "response", + level: 1, + auxiliary: { + response: { + value: JSON.stringify(response), + type: "object", + }, + requestId: { + value: requestId, + type: "string", + }, + }, + }); + + if (options.response_model) { + const extractedData = response.choices[0].message.content; + if (!extractedData) { + throw new Error("No content in response"); + } + const parsedData = JSON.parse(extractedData); + + if (!validateZodSchema(options.response_model.schema, parsedData)) { + if (retries > 0) { + return this.createChatCompletion({ + options, + logger, + retries: retries - 1, + }); + } + + throw new Error("Invalid response schema"); + } + + return parsedData; + } + + return response as T; + } +} diff --git a/package.json b/package.json index 5d0a926..8e483fb 100644 --- a/package.json +++ b/package.json @@ -13,12 +13,16 @@ "@ai-sdk/provider": "^1.0.6", "@browserbasehq/sdk": "^2.0.0", "@browserbasehq/stagehand": "^1.10.1", + "@instructor-ai/instructor": "^1.7.0", "@vercel/analytics": "^1.4.1", "ai": "^4.1.2", + "boxen": "^8.0.1", + "dotenv": "^16.4.7", "framer-motion": "^11.0.3", "jotai": "^2.11.1", "motion": "^12.0.3", "next": "15.1.6", + "openai": "^4.83.0", "playwright": "^1.50.0", "playwright-core": "^1.50.0", "posthog-js": "^1.209.3", @@ -26,7 +30,7 @@ "react-dom": "^19.0.0", "sharp": "^0.33.5", "usehooks-ts": "^2.16.0", - "zod": "^3.22.4" + "zod": "^3.24.1" }, "devDependencies": { "@eslint/eslintrc": "^3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c75d71c..d985bc2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,13 +19,22 @@ importers: version: 2.0.0 '@browserbasehq/stagehand': specifier: ^1.10.1 - version: 1.10.1(@playwright/test@1.50.0)(deepmerge@4.3.1)(dotenv@16.4.7)(openai@4.80.0(ws@8.18.0)(zod@3.24.1))(zod@3.24.1) + version: 1.10.1(@playwright/test@1.50.0)(deepmerge@4.3.1)(dotenv@16.4.7)(openai@4.83.0(ws@8.18.0)(zod@3.24.1))(zod@3.24.1) + '@instructor-ai/instructor': + specifier: ^1.7.0 + version: 1.7.0(openai@4.83.0(ws@8.18.0)(zod@3.24.1))(zod@3.24.1) '@vercel/analytics': specifier: ^1.4.1 version: 1.4.1(next@15.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0) ai: specifier: ^4.1.2 version: 4.1.2(react@19.0.0)(zod@3.24.1) + boxen: + specifier: ^8.0.1 + version: 8.0.1 + dotenv: + specifier: ^16.4.7 + version: 16.4.7 framer-motion: specifier: ^11.0.3 version: 11.18.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -38,6 +47,9 @@ importers: next: specifier: 15.1.6 version: 15.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + openai: + specifier: ^4.83.0 + version: 4.83.0(ws@8.18.0)(zod@3.24.1) playwright: specifier: ^1.50.0 version: 1.50.0 @@ -60,7 +72,7 @@ importers: specifier: ^2.16.0 version: 2.16.0(react@19.0.0) zod: - specifier: ^3.22.4 + specifier: ^3.24.1 version: 3.24.1 devDependencies: '@eslint/eslintrc': @@ -314,6 +326,12 @@ packages: cpu: [x64] os: [win32] + '@instructor-ai/instructor@1.7.0': + resolution: {integrity: sha512-7ifu2ZzFBMSOtkY2X5dd2RCxQvfO6Bx+Ag/9V+FkamLGGGciazlTKsEyQR6KSn7BXADLWLIDV296S+O+CifE/A==} + peerDependencies: + openai: '>=4.58.0' + zod: '>=3.23.8' + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -566,6 +584,9 @@ packages: ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ansi-align@3.0.1: + resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -660,6 +681,10 @@ packages: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} + boxen@8.0.1: + resolution: {integrity: sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw==} + engines: {node: '>=18'} + brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} @@ -694,6 +719,10 @@ packages: resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} engines: {node: '>= 6'} + camelcase@8.0.0: + resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==} + engines: {node: '>=16'} + caniuse-lite@1.0.30001695: resolution: {integrity: sha512-vHyLade6wTgI2u1ec3WQBxv+2BrTERV28UXQu9LO6lZ9pYeMk34vjXFLOxo1A4UBA8XTL4njRQZdno/yYaSmWw==} @@ -709,6 +738,10 @@ packages: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} + cli-boxes@3.0.0: + resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==} + engines: {node: '>=10'} + client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} @@ -835,6 +868,9 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + emoji-regex@10.4.0: + resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==} + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -1112,6 +1148,10 @@ packages: functions-have-names@1.2.3: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + get-east-asian-width@1.3.0: + resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==} + engines: {node: '>=18'} + get-intrinsic@1.2.7: resolution: {integrity: sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==} engines: {node: '>= 0.4'} @@ -1560,8 +1600,8 @@ packages: resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} engines: {node: '>= 0.4'} - openai@4.80.0: - resolution: {integrity: sha512-5TqdNQgjOMxo3CkCvtjzuSwuznO/o3q5aak0MTy6IjRvPtvVA1wAFGJU3eZT1JHzhs2wFb/xtDG0o6Y/2KGCfw==} + openai@4.83.0: + resolution: {integrity: sha512-fmTsqud0uTtRKsPC7L8Lu55dkaTwYucqncDHzVvO64DKOpNTuiYwjbR/nVgpapXuYy8xSnhQQPUm+3jQaxICgw==} hasBin: true peerDependencies: ws: ^8.18.0 @@ -1704,6 +1744,9 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + ramda@0.29.1: + resolution: {integrity: sha512-OfxIeWzd4xdUNxlWhgFazxsA/nl3mS4/jGZI5n00uWOoSSFRhC1b6gl6xvmzUamgmqELraWp0J/qqVlXYPDPyA==} + react-dom@19.0.0: resolution: {integrity: sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==} peerDependencies: @@ -1769,6 +1812,11 @@ packages: scheduler@0.25.0: resolution: {integrity: sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==} + schema-stream@3.2.2: + resolution: {integrity: sha512-7hwI/4lbo8sOs7sRbKW9Bh+5qPHOj12tVpskHq1tYakVlWEpdLgeEKXXgzGLaAP1Bw+DcoB4DmdA7/nSxGI5OA==} + peerDependencies: + zod: ^3.23.3 + secure-json-parse@2.7.0: resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==} @@ -1847,6 +1895,10 @@ packages: resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} engines: {node: '>=12'} + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + string.prototype.includes@2.0.1: resolution: {integrity: sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==} engines: {node: '>= 0.4'} @@ -1963,6 +2015,10 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} + type-fest@4.33.0: + resolution: {integrity: sha512-s6zVrxuyKbbAsSAD5ZPTB77q4YIdRctkTbJ2/Dqlinwz+8ooH2gd+YA7VA6Pa93KML9GockVvoxjZ2vHP+mu8g==} + engines: {node: '>=16'} + typed-array-buffer@1.0.3: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} engines: {node: '>= 0.4'} @@ -2045,6 +2101,10 @@ packages: engines: {node: '>= 8'} hasBin: true + widest-line@5.0.0: + resolution: {integrity: sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==} + engines: {node: '>=18'} + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} @@ -2057,6 +2117,10 @@ packages: resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} engines: {node: '>=12'} + wrap-ansi@9.0.0: + resolution: {integrity: sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==} + engines: {node: '>=18'} + ws@8.18.0: resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} engines: {node: '>=10.0.0'} @@ -2078,11 +2142,23 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + zod-stream@3.0.0: + resolution: {integrity: sha512-BfW8KI+FCEMQOAIf8Qgc2q67iZ+LZ/zzk7alU45FWiZIrBSt0JMxNVGuc3o1yTpGFVk3zXiHt3TZvmUiad/9nw==} + peerDependencies: + openai: 4.47.1 + zod: ^3.23.3 + zod-to-json-schema@3.24.1: resolution: {integrity: sha512-3h08nf3Vw3Wl3PK+q3ow/lIil81IT2Oa7YpQyUUDsEWbXveMesdfK1xBd2RhCkynwZndAxixji/7SYJJowr62w==} peerDependencies: zod: ^3.24.1 + zod-validation-error@3.4.0: + resolution: {integrity: sha512-ZOPR9SVY6Pb2qqO5XHt+MkkTRxGXb4EVtnjc9JpXUOtUB1T9Ru7mZOT361AN3MsetVe7R0a1KZshJDZdgp9miQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + zod: ^3.18.0 + zod@3.24.1: resolution: {integrity: sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==} @@ -2151,14 +2227,14 @@ snapshots: transitivePeerDependencies: - encoding - '@browserbasehq/stagehand@1.10.1(@playwright/test@1.50.0)(deepmerge@4.3.1)(dotenv@16.4.7)(openai@4.80.0(ws@8.18.0)(zod@3.24.1))(zod@3.24.1)': + '@browserbasehq/stagehand@1.10.1(@playwright/test@1.50.0)(deepmerge@4.3.1)(dotenv@16.4.7)(openai@4.83.0(ws@8.18.0)(zod@3.24.1))(zod@3.24.1)': dependencies: '@anthropic-ai/sdk': 0.27.3 '@browserbasehq/sdk': 2.0.0 '@playwright/test': 1.50.0 deepmerge: 4.3.1 dotenv: 16.4.7 - openai: 4.80.0(ws@8.18.0)(zod@3.24.1) + openai: 4.83.0(ws@8.18.0)(zod@3.24.1) sharp: 0.33.5 ws: 8.18.0 zod: 3.24.1 @@ -2303,6 +2379,13 @@ snapshots: '@img/sharp-win32-x64@0.33.5': optional: true + '@instructor-ai/instructor@1.7.0(openai@4.83.0(ws@8.18.0)(zod@3.24.1))(zod@3.24.1)': + dependencies: + openai: 4.83.0(ws@8.18.0)(zod@3.24.1) + zod: 3.24.1 + zod-stream: 3.0.0(openai@4.83.0(ws@8.18.0)(zod@3.24.1))(zod@3.24.1) + zod-validation-error: 3.4.0(zod@3.24.1) + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -2536,6 +2619,10 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ansi-align@3.0.1: + dependencies: + string-width: 4.2.3 + ansi-regex@5.0.1: {} ansi-regex@6.1.0: {} @@ -2641,6 +2728,17 @@ snapshots: binary-extensions@2.3.0: {} + boxen@8.0.1: + dependencies: + ansi-align: 3.0.1 + camelcase: 8.0.0 + chalk: 5.4.1 + cli-boxes: 3.0.0 + string-width: 7.2.0 + type-fest: 4.33.0 + widest-line: 5.0.0 + wrap-ansi: 9.0.0 + brace-expansion@1.1.11: dependencies: balanced-match: 1.0.2 @@ -2679,6 +2777,8 @@ snapshots: camelcase-css@2.0.1: {} + camelcase@8.0.0: {} + caniuse-lite@1.0.30001695: {} chalk@4.1.2: @@ -2700,6 +2800,8 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + cli-boxes@3.0.0: {} + client-only@0.0.1: {} color-convert@2.0.1: @@ -2808,6 +2910,8 @@ snapshots: eastasianwidth@0.2.0: {} + emoji-regex@10.4.0: {} + emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} @@ -3225,6 +3329,8 @@ snapshots: functions-have-names@1.2.3: {} + get-east-asian-width@1.3.0: {} + get-intrinsic@1.2.7: dependencies: call-bind-apply-helpers: 1.0.1 @@ -3668,7 +3774,7 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 - openai@4.80.0(ws@8.18.0)(zod@3.24.1): + openai@4.83.0(ws@8.18.0)(zod@3.24.1): dependencies: '@types/node': 18.19.74 '@types/node-fetch': 2.6.12 @@ -3805,6 +3911,8 @@ snapshots: queue-microtask@1.2.3: {} + ramda@0.29.1: {} + react-dom@19.0.0(react@19.0.0): dependencies: react: 19.0.0 @@ -3885,6 +3993,11 @@ snapshots: scheduler@0.25.0: {} + schema-stream@3.2.2(zod@3.24.1): + dependencies: + ramda: 0.29.1 + zod: 3.24.1 + secure-json-parse@2.7.0: {} semver@6.3.1: {} @@ -3997,6 +4110,12 @@ snapshots: emoji-regex: 9.2.2 strip-ansi: 7.1.0 + string-width@7.2.0: + dependencies: + emoji-regex: 10.4.0 + get-east-asian-width: 1.3.0 + strip-ansi: 7.1.0 + string.prototype.includes@2.0.1: dependencies: call-bind: 1.0.8 @@ -4150,6 +4269,8 @@ snapshots: dependencies: prelude-ls: 1.2.1 + type-fest@4.33.0: {} + typed-array-buffer@1.0.3: dependencies: call-bound: 1.0.3 @@ -4266,6 +4387,10 @@ snapshots: dependencies: isexe: 2.0.0 + widest-line@5.0.0: + dependencies: + string-width: 7.2.0 + word-wrap@1.2.5: {} wrap-ansi@7.0.0: @@ -4280,14 +4405,31 @@ snapshots: string-width: 5.1.2 strip-ansi: 7.1.0 + wrap-ansi@9.0.0: + dependencies: + ansi-styles: 6.2.1 + string-width: 7.2.0 + strip-ansi: 7.1.0 + ws@8.18.0: {} yaml@2.7.0: {} yocto-queue@0.1.0: {} + zod-stream@3.0.0(openai@4.83.0(ws@8.18.0)(zod@3.24.1))(zod@3.24.1): + dependencies: + openai: 4.83.0(ws@8.18.0)(zod@3.24.1) + schema-stream: 3.2.2(zod@3.24.1) + zod: 3.24.1 + zod-to-json-schema: 3.24.1(zod@3.24.1) + zod-to-json-schema@3.24.1(zod@3.24.1): dependencies: zod: 3.24.1 + zod-validation-error@3.4.0(zod@3.24.1): + dependencies: + zod: 3.24.1 + zod@3.24.1: {} diff --git a/stagehand.config.ts b/stagehand.config.ts new file mode 100644 index 0000000..2055f06 --- /dev/null +++ b/stagehand.config.ts @@ -0,0 +1,79 @@ +import type { ConstructorParams, LogLine } from "@browserbasehq/stagehand"; +import dotenv from "dotenv"; +import { CustomOpenAIClient } from "./custom_client"; + +dotenv.config(); + +const StagehandConfig: ConstructorParams = { + env: + process.env.BROWSERBASE_API_KEY && process.env.BROWSERBASE_PROJECT_ID + ? "BROWSERBASE" + : "LOCAL", + apiKey: process.env.BROWSERBASE_API_KEY /* API key for authentication */, + projectId: process.env.BROWSERBASE_PROJECT_ID /* Project identifier */, + debugDom: true /* Enable DOM debugging features */, + headless: false /* Run browser in headless mode */, + logger: (message: LogLine) => + message.message === "creating chat completion" + ? console.log(JSON.stringify(message)) + : console.log(logLineToString(message)) /* Custom logging function */, + domSettleTimeoutMs: 30_000 /* Timeout for DOM to settle in milliseconds */, + llmClient: new CustomOpenAIClient({ + modelName: process.env.CUSTOM_OPENAI_MODEL_NAME!, + clientOptions: { + baseURL: process.env.CUSTOM_OPENAI_BASE_URL!, + apiKey: process.env.CUSTOM_OPENAI_API_KEY!, + }, + }), +}; +export default StagehandConfig; + +/** + * Custom logging function that you can use to filter logs. + * + * General pattern here is that `message` will always be unique with no params + * Any param you would put in a log is in `auxiliary`. + * + * For example, an error log looks like this: + * + * ``` + * { + * category: "error", + * message: "Some specific error occurred", + * auxiliary: { + * message: { value: "Error message", type: "string" }, + * trace: { value: "Error trace", type: "string" } + * } + * } + * ``` + * + * You can then use `logLineToString` to filter for a specific log pattern like + * + * ``` + * if (logLine.message === "Some specific error occurred") { + * console.log(logLineToString(logLine)); + * } + * ``` + */ +export function logLineToString(logLine: LogLine): string { + // If you want more detail, set this to false. However, this will make the logs + // more verbose and harder to read. + const HIDE_AUXILIARY = true; + + try { + const timestamp = logLine.timestamp || new Date().toISOString(); + if (logLine.auxiliary?.error) { + return `${timestamp}::[stagehand:${logLine.category}] ${logLine.message}\n ${logLine.auxiliary.error.value}\n ${logLine.auxiliary.trace.value}`; + } + + // If we want to hide auxiliary information, we don't add it to the log + return `${timestamp}::[stagehand:${logLine.category}] ${logLine.message} ${ + logLine.auxiliary && !HIDE_AUXILIARY + ? JSON.stringify(logLine.auxiliary) + : "" + }`; + } catch (error) { + console.error(`Error logging line:`, error); + return "error logging line"; + } +} diff --git a/utils.ts b/utils.ts new file mode 100644 index 0000000..3e88bdc --- /dev/null +++ b/utils.ts @@ -0,0 +1,39 @@ +import boxen from "boxen"; +import { z } from "zod"; +export function announce(message: string, title?: string) { + console.log( + boxen(message, { + padding: 1, + margin: 3, + title: title || "Stagehand", + }) + ); +} + +/** + * Get an environment variable and throw an error if it's not found + * @param name - The name of the environment variable + * @returns The value of the environment variable + */ +export function getEnvVar(name: string, required = true): string | undefined { + const value = process.env[name]; + if (!value && required) { + throw new Error(`${name} not found in environment variables`); + } + return value; +} + +/** + * Validate a Zod schema against some data + * @param schema - The Zod schema to validate against + * @param data - The data to validate + * @returns Whether the data is valid against the schema + */ +export function validateZodSchema(schema: z.ZodTypeAny, data: unknown) { + try { + schema.parse(data); + return true; + } catch { + return false; + } +} From fda8f7e21795b7394574142124bebf44cc3a3538 Mon Sep 17 00:00:00 2001 From: Anirudh Kamath Date: Wed, 5 Feb 2025 16:31:15 -0800 Subject: [PATCH 2/2] comment --- .env.example | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index d9e8496..ce8ce26 100644 --- a/.env.example +++ b/.env.example @@ -5,6 +5,6 @@ BROWSERBASE_PROJECT_ID="YOUR_BROWSERBASE_PROJECT_ID" # Custom OpenAI Configuration CUSTOM_OPENAI_MODEL_NAME="YOUR_CUSTOM_OPENAI_MODEL_NAME" -# Brev Configuration +# Custom OpenAI Configuration CUSTOM_OPENAI_API_KEY="YOUR_CUSTOM_OPENAI_API_KEY" -CUSTOM_OPENAI_BASE_URL="YOUR_CUSTOM_OPENAI_BASE_URL" \ No newline at end of file +CUSTOM_OPENAI_BASE_URL="YOUR_CUSTOM_OPENAI_BASE_URL"