diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 3549461..c6a6955 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "3.5.1" + ".": "3.5.2" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index 9738f87..a0da57a 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 8 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-8fbb3fa8f3a37c1c7408de427fe125aadec49f705e8e30d191601a9b69c4cc41.yml -openapi_spec_hash: 48b4dfac35a842d7fb0d228caf87544e +openapi_spec_hash: 8a36f79075102c63234ed06107deb8c9 config_hash: 7386d24e2f03a3b2a89b3f6881446348 diff --git a/CHANGELOG.md b/CHANGELOG.md index c0ee849..f511dfb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 3.5.2 (2026-02-06) + +Full Changelog: [v3.5.1...v3.5.2](https://github.com/browserbase/stagehand-ruby/compare/v3.5.1...v3.5.2) + +### Chores + +* **docs:** remove www prefix ([8498bdd](https://github.com/browserbase/stagehand-ruby/commit/8498bdd34e0288b8a1c28e789d35dcecf91cd8b7)) + ## 3.5.1 (2026-02-03) Full Changelog: [v3.5.0...v3.5.1](https://github.com/browserbase/stagehand-ruby/compare/v3.5.0...v3.5.1) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 96554b4..c2d5f73 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -43,13 +43,13 @@ If you’d like to use the repository from source, you can either install from g To install via git in your `Gemfile`: ```ruby -gem "stagehand", git: "https://www.github.com/browserbase/stagehand-ruby" +gem "stagehand", git: "https://github.com/browserbase/stagehand-ruby" ``` Alternatively, reference local copy of the repo: ```bash -$ git clone -- 'https://www.github.com/browserbase/stagehand-ruby' '' +$ git clone -- 'https://github.com/browserbase/stagehand-ruby' '' ``` ```ruby diff --git a/Gemfile.lock b/Gemfile.lock index c29ea56..d78caf4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -11,7 +11,7 @@ GIT PATH remote: . specs: - stagehand (3.5.1) + stagehand (3.5.2) cgi connection_pool diff --git a/README.md b/README.md index 2216409..4089a35 100644 --- a/README.md +++ b/README.md @@ -149,7 +149,7 @@ export MODEL_API_KEY="your-openai-api-key" bundle exec ruby examples/local_browser_example.rb ``` -Playwright local example: +Playwright local example (SSE streaming): ```bash gem install playwright-ruby-client @@ -157,6 +157,8 @@ npm install playwright ./node_modules/.bin/playwright install chromium export MODEL_API_KEY="your-openai-api-key" bundle exec ruby examples/local_playwright_example.rb + +bundle exec ruby examples/local_browser_playwright_example.rb ``` Playwright remote example: @@ -168,7 +170,7 @@ npm install playwright export BROWSERBASE_API_KEY="your-browserbase-api-key" export BROWSERBASE_PROJECT_ID="your-browserbase-project-id" export MODEL_API_KEY="your-openai-api-key" -bundle exec ruby examples/remote_playwright_example.rb +bundle exec ruby examples/remote_browser_playwright_example.rb ``` Watir local example: diff --git a/examples/local_browser_playwright_example.rb b/examples/local_browser_playwright_example.rb new file mode 100755 index 0000000..587a8bf --- /dev/null +++ b/examples/local_browser_playwright_example.rb @@ -0,0 +1,207 @@ +#!/usr/bin/env ruby +# typed: ignore +# frozen_string_literal: true + +require "bundler/setup" +require "stagehand" + +# Example: Using Playwright with Stagehand local mode (local browser). +# +# Prerequisites: +# - Set MODEL_API_KEY or OPENAI_API_KEY environment variable +# - Set BROWSERBASE_API_KEY and BROWSERBASE_PROJECT_ID (can be any value in local mode) +# - Install Playwright (outside this gem): +# gem install playwright-ruby-client +# npm install playwright +# ./node_modules/.bin/playwright install chromium +# +# Run: +# bundle exec ruby examples/local_browser_playwright_example.rb + +begin + require("playwright") +rescue LoadError + warn("Playwright is not installed. Run: gem install playwright-ruby-client") + exit(1) +end + +model_key = ENV["MODEL_API_KEY"] || ENV["OPENAI_API_KEY"] +browserbase_api_key = ENV["BROWSERBASE_API_KEY"].to_s +browserbase_project_id = ENV["BROWSERBASE_PROJECT_ID"].to_s + +missing = [] +missing << "MODEL_API_KEY" if model_key.to_s.empty? +missing << "BROWSERBASE_API_KEY" if browserbase_api_key.empty? +missing << "BROWSERBASE_PROJECT_ID" if browserbase_project_id.empty? + +unless missing.empty? + warn("Set #{missing.join(', ')} to run the local Playwright example.") + exit(1) +end + +def print_stream_event(label, event) + case event.type + when :log + puts("[#{label}] log: #{event.data.message}") + when :system + status = event.data.status + if event.data.respond_to?(:error) && event.data.error + puts("[#{label}] system #{status}: #{event.data.error}") + elsif event.data.respond_to?(:result) && !event.data.result.nil? + puts("[#{label}] system #{status}: #{event.data.result}") + else + puts("[#{label}] system #{status}") + end + else + puts("[#{label}] event: #{event.inspect}") + end +end + +def stream_with_result(label, stream) + puts("#{label} stream:") + result = nil + stream.each do |event| + print_stream_event(label, event) + if event.type == :system && event.data.respond_to?(:result) && !event.data.result.nil? + result = event.data.result + end + end + result +end + +client = Stagehand::Client.new( + browserbase_api_key: browserbase_api_key, + browserbase_project_id: browserbase_project_id, + model_api_key: model_key, + server: "local" +) + +session_id = nil + +begin + # rubocop:disable Metrics/BlockLength + Playwright.create(playwright_cli_executable_path: "./node_modules/.bin/playwright") do |playwright| + browser_server = playwright.chromium.launch_server(headless: true) + cdp_url = browser_server.ws_endpoint + + start_response = client.sessions.start( + model_name: "openai/gpt-5-nano", + browser: { + type: :local, + launch_options: { + cdp_url: cdp_url + } + } + ) + session_id = start_response.data.session_id + + puts("Session started: #{session_id}") + puts("Connecting Playwright over CDP...") + + browser = playwright.chromium.connect_over_cdp(cdp_url) + begin + context = browser.contexts.first || browser.new_context + page = context.pages.first || context.new_page + page.goto("https://example.com") + page.wait_for_load_state(state: "domcontentloaded") + + observe_stream = client.sessions.observe_streaming( + session_id, + instruction: "Find all clickable links on this page" + ) + observe_result = stream_with_result("Observe", observe_stream) + if observe_result.nil? + observe_response = client.sessions.observe( + session_id, + instruction: "Find all clickable links on this page" + ) + observe_result = observe_response.data.result + end + puts("Found #{observe_result.length} possible actions") + + act_input = "Click the 'Learn more' link" + act_stream = client.sessions.act_streaming( + session_id, + input: act_input + ) + act_result = stream_with_result("Act", act_stream) + if act_result.nil? + act_response = client.sessions.act( + session_id, + input: act_input + ) + act_result = act_response.data.result + end + act_message = act_result.is_a?(Hash) ? (act_result[:message] || act_result["message"]) : act_result + puts("Act completed: #{act_message}") + + extract_stream = client.sessions.extract_streaming( + session_id, + instruction: "Extract the main heading and any links on this page", + schema: { + type: "object", + properties: { + heading: {type: "string"}, + links: {type: "array", items: {type: "string"}} + } + } + ) + extract_result = stream_with_result("Extract", extract_stream) + if extract_result.nil? + extract_response = client.sessions.extract( + session_id, + instruction: "Extract the main heading and any links on this page", + schema: { + type: "object", + properties: { + heading: {type: "string"}, + links: {type: "array", items: {type: "string"}} + } + } + ) + extract_result = extract_response.data.result + end + puts("Extracted: #{extract_result}") + + execute_stream = client.sessions.execute_streaming( + session_id, + execute_options: { + instruction: "Click the 'Learn more' link if available and summarize the destination.", + max_steps: 5 + }, + agent_config: { + model: { + model_name: "openai/gpt-5-nano", + api_key: model_key + }, + cua: false + } + ) + execute_result = stream_with_result("Execute", execute_stream) + if execute_result.nil? + execute_response = client.sessions.execute( + session_id, + execute_options: { + instruction: "Click the 'Learn more' link if available and summarize the destination.", + max_steps: 5 + }, + agent_config: { + model: { + model_name: "openai/gpt-5-nano", + api_key: model_key + }, + cua: false + } + ) + execute_result = execute_response.data.result + end + puts("Execute result: #{execute_result}") + ensure + browser.close + browser_server.close + end + end + # rubocop:enable Metrics/BlockLength +ensure + client.sessions.end(session_id) if session_id +end diff --git a/examples/remote_playwright_example.rb b/examples/remote_browser_playwright_example.rb similarity index 53% rename from examples/remote_playwright_example.rb rename to examples/remote_browser_playwright_example.rb index 9b4375a..89c2fda 100755 --- a/examples/remote_playwright_example.rb +++ b/examples/remote_browser_playwright_example.rb @@ -16,7 +16,7 @@ # ./node_modules/.bin/playwright install chromium # # Run: -# bundle exec ruby examples/remote_playwright_example.rb +# bundle exec ruby examples/remote_browser_playwright_example.rb begin require("playwright") @@ -62,6 +62,36 @@ def resolve_page_target_id(cdp_session, page_url) target && target["targetId"] end +def print_stream_event(label, event) + case event.type + when :log + puts("[#{label}] log: #{event.data.message}") + when :system + status = event.data.status + if event.data.respond_to?(:error) && event.data.error + puts("[#{label}] system #{status}: #{event.data.error}") + elsif event.data.respond_to?(:result) && !event.data.result.nil? + puts("[#{label}] system #{status}: #{event.data.result}") + else + puts("[#{label}] system #{status}") + end + else + puts("[#{label}] event: #{event.inspect}") + end +end + +def stream_with_result(label, stream) + puts("#{label} stream:") + result = nil + stream.each do |event| + print_stream_event(label, event) + if event.type == :system && event.data.respond_to?(:result) && !event.data.result.nil? + result = event.data.result + end + end + result +end + client = Stagehand::Client.new( browserbase_api_key: browserbase_api_key, browserbase_project_id: browserbase_project_id, @@ -102,23 +132,43 @@ def resolve_page_target_id(cdp_session, page_url) raise "Page target id not found for page target" end - observe_response = client.sessions.observe( + observe_stream = client.sessions.observe_streaming( session_id, frame_id: page_target_id, instruction: "Find all clickable links on this page" ) - puts("Found #{observe_response.data.result.length} possible actions") + observe_result = stream_with_result("Observe", observe_stream) + if observe_result.nil? + observe_response = client.sessions.observe( + session_id, + frame_id: page_target_id, + instruction: "Find all clickable links on this page" + ) + observe_result = observe_response.data.result + end + puts("Found #{observe_result.length} possible actions") - action = observe_response.data.result.first + action = observe_result.first act_input = action ? action.to_h.merge(method: "click") : "Click the 'Learn more' link" - act_response = client.sessions.act( + + act_stream = client.sessions.act_streaming( session_id, frame_id: page_target_id, input: act_input ) - puts("Act completed: #{act_response.data.result[:message]}") + act_result = stream_with_result("Act", act_stream) + if act_result.nil? + act_response = client.sessions.act( + session_id, + frame_id: page_target_id, + input: act_input + ) + act_result = act_response.data.result + end + act_message = act_result.is_a?(Hash) ? (act_result[:message] || act_result["message"]) : act_result + puts("Act completed: #{act_message}") - extract_response = client.sessions.extract( + extract_stream = client.sessions.extract_streaming( session_id, frame_id: page_target_id, instruction: "Extract the main heading and any links on this page", @@ -130,9 +180,25 @@ def resolve_page_target_id(cdp_session, page_url) } } ) - puts("Extracted: #{extract_response.data.result}") + extract_result = stream_with_result("Extract", extract_stream) + if extract_result.nil? + extract_response = client.sessions.extract( + session_id, + frame_id: page_target_id, + instruction: "Extract the main heading and any links on this page", + schema: { + type: "object", + properties: { + heading: {type: "string"}, + links: {type: "array", items: {type: "string"}} + } + } + ) + extract_result = extract_response.data.result + end + puts("Extracted: #{extract_result}") - execute_response = client.sessions.execute( + execute_stream = client.sessions.execute_streaming( session_id, frame_id: page_target_id, execute_options: { @@ -147,12 +213,34 @@ def resolve_page_target_id(cdp_session, page_url) cua: false } ) - puts("Agent completed: #{execute_response.data.result[:message]}") - puts("Agent success: #{execute_response.data.result[:success]}") + execute_result = stream_with_result("Agent", execute_stream) + if execute_result.nil? + execute_response = client.sessions.execute( + session_id, + frame_id: page_target_id, + execute_options: { + instruction: "Click on the 'Learn more' link if available", + max_steps: 3 + }, + agent_config: { + model: Stagehand::ModelConfig.new( + model_name: "openai/gpt-5-nano", + api_key: model_key + ), + cua: false + } + ) + execute_result = execute_response.data.result + end + agent_message = + execute_result.is_a?(Hash) ? (execute_result[:message] || execute_result["message"]) : execute_result + agent_success = execute_result.is_a?(Hash) ? (execute_result[:success] || execute_result["success"]) : nil + puts("Agent completed: #{agent_message}") + puts("Agent success: #{agent_success}") page.wait_for_load_state(state: "domcontentloaded") - page.screenshot(path: "screenshot_remote_playwright.png", fullPage: true) - puts("Screenshot saved to: screenshot_remote_playwright.png") + page.screenshot(path: "screenshot_remote_browser_playwright.png", fullPage: true) + puts("Screenshot saved to: screenshot_remote_browser_playwright.png") ensure browser.close end diff --git a/lib/stagehand/version.rb b/lib/stagehand/version.rb index 23c52d4..6872959 100644 --- a/lib/stagehand/version.rb +++ b/lib/stagehand/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Stagehand - VERSION = "3.5.1" + VERSION = "3.5.2" end