Skip to content

Commit 3d8b897

Browse files
authored
docs: add full example and update examples in README (#8)
* docs: add full example and update examples in README * feat: automatically send empty body for end request, also add support for ruby 4.0+
1 parent 0a9ad7e commit 3d8b897

6 files changed

Lines changed: 265 additions & 7 deletions

File tree

.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
BROWSERBASE_API_KEY=bb_live_your_api_key_here
2+
BROWSERBASE_PROJECT_ID=your-project-uuid-here
3+
MODEL_API_KEY=sk-proj-your-llm-api-key-here

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
.prism.log
55
.ruby-lsp/
66
.yardoc/
7+
.env
78
bin/tapioca
89
Brewfile.lock.json
910
doc/

README.md

Lines changed: 125 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,134 @@ gem "stagehand", "~> 0.6.0"
2828
require "bundler/setup"
2929
require "stagehand"
3030

31-
stagehand = Stagehand::Client.new(
32-
browserbase_api_key: ENV["BROWSERBASE_API_KEY"], # This is the default and can be omitted
33-
browserbase_project_id: ENV["BROWSERBASE_PROJECT_ID"], # This is the default and can be omitted
34-
model_api_key: ENV["MODEL_API_KEY"] # This is the default and can be omitted
31+
# Create a new Stagehand client with your credentials
32+
client = Stagehand::Client.new(
33+
browserbase_api_key: ENV["BROWSERBASE_API_KEY"], # defaults to ENV["BROWSERBASE_API_KEY"]
34+
browserbase_project_id: ENV["BROWSERBASE_PROJECT_ID"], # defaults to ENV["BROWSERBASE_PROJECT_ID"]
35+
model_api_key: ENV["MODEL_API_KEY"] # defaults to ENV["MODEL_API_KEY"]
36+
)
37+
38+
# Start a new browser session
39+
# x_language and x_sdk_version headers are required for the v3 API
40+
start_response = client.sessions.start(
41+
model_name: "openai/gpt-5-nano",
42+
x_language: :typescript,
43+
x_sdk_version: "3.0.6"
44+
)
45+
puts "Session started: #{start_response.data.session_id}"
46+
47+
session_id = start_response.data.session_id
48+
49+
# Navigate to a webpage
50+
# frame_id is required - use empty string for the main frame
51+
client.sessions.navigate(
52+
session_id,
53+
url: "https://news.ycombinator.com",
54+
frame_id: "",
55+
x_language: :typescript,
56+
x_sdk_version: "3.0.6"
57+
)
58+
puts "Navigated to Hacker News"
59+
60+
# Use Observe to find possible actions on the page
61+
observe_response = client.sessions.observe(
62+
session_id,
63+
instruction: "find the link to view comments for the top post",
64+
x_language: :typescript,
65+
x_sdk_version: "3.0.6"
66+
)
67+
68+
actions = observe_response.data.result
69+
puts "Found #{actions.length} possible actions"
70+
71+
# Take the first action returned by Observe
72+
action = actions.first
73+
puts "Acting on: #{action.description}"
74+
75+
# Pass the structured action to Act
76+
# Convert the observe result to a hash and ensure method is set to "click"
77+
act_response = client.sessions.act(
78+
session_id,
79+
input: action.to_h.merge(method: "click"),
80+
x_language: :typescript,
81+
x_sdk_version: "3.0.6"
82+
)
83+
puts "Act completed: #{act_response.data.result[:message]}"
84+
85+
# Extract data from the page
86+
# We're now on the comments page, so extract the top comment text
87+
extract_response = client.sessions.extract(
88+
session_id,
89+
instruction: "extract the text of the top comment on this page",
90+
schema: {
91+
type: "object",
92+
properties: {
93+
comment_text: {
94+
type: "string",
95+
description: "The text content of the top comment"
96+
},
97+
author: {
98+
type: "string",
99+
description: "The username of the comment author"
100+
}
101+
},
102+
required: ["comment_text"]
103+
},
104+
x_language: :typescript,
105+
x_sdk_version: "3.0.6"
106+
)
107+
puts "Extracted data: #{extract_response.data.result}"
108+
109+
# Get the author from the extracted data
110+
extracted_data = extract_response.data.result
111+
author = extracted_data[:author]
112+
puts "Looking up profile for author: #{author}"
113+
114+
# Use the Agent to find the author's profile
115+
# Execute runs an autonomous agent that can navigate and interact with pages
116+
execute_response = client.sessions.execute(
117+
session_id,
118+
execute_options: {
119+
instruction: "Find any personal website, GitHub, LinkedIn, or other best profile URL for the Hacker News user '#{author}'. " \
120+
"Click on their username to go to their profile page and look for any links they have shared.",
121+
max_steps: 15
122+
},
123+
agent_config: {
124+
model: Stagehand::ModelConfig::ModelConfigObject.new(
125+
model_name: "openai/gpt-5-nano",
126+
api_key: ENV["MODEL_API_KEY"]
127+
),
128+
cua: false
129+
},
130+
x_language: :typescript,
131+
x_sdk_version: "3.0.6"
35132
)
133+
puts "Agent completed: #{execute_response.data.result[:message]}"
134+
puts "Agent success: #{execute_response.data.result[:success]}"
135+
puts "Agent actions taken: #{execute_response.data.result[:actions]&.length || 0}"
136+
137+
# End the session to cleanup browser resources
138+
client.sessions.end_(
139+
session_id,
140+
x_language: :typescript,
141+
x_sdk_version: "3.0.6"
142+
)
143+
puts "Session ended"
144+
```
145+
146+
### Running the Example
147+
148+
Set the required environment variables and run the example script:
36149

37-
response = stagehand.sessions.act("00000000-your-session-id-000000000000", input: "click the first link on the page")
150+
```bash
151+
# Set your credentials
152+
export BROWSERBASE_API_KEY="your-browserbase-api-key"
153+
export BROWSERBASE_PROJECT_ID="your-browserbase-project-id"
154+
export MODEL_API_KEY="your-openai-api-key"
38155

39-
puts(response.data)
156+
# Install dependencies and run
157+
bundle install
158+
bundle exec ruby examples/basic.rb
40159
```
41160

42161
### Streaming

examples/basic.rb

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
#!/usr/bin/env ruby
2+
# frozen_string_literal: true
3+
4+
require "bundler/setup"
5+
require "stagehand"
6+
7+
# Set these environment variables before running this script:
8+
# BROWSERBASE_API_KEY - Your Browserbase API key
9+
# BROWSERBASE_PROJECT_ID - Your Browserbase project ID
10+
# MODEL_API_KEY - Your AI model API key (e.g., OpenAI)
11+
12+
# Create a new Stagehand client with your credentials
13+
client = Stagehand::Client.new(
14+
browserbase_api_key: ENV["BROWSERBASE_API_KEY"],
15+
browserbase_project_id: ENV["BROWSERBASE_PROJECT_ID"],
16+
model_api_key: ENV["MODEL_API_KEY"]
17+
)
18+
19+
# Start a new browser session
20+
# x_language and x_sdk_version headers are required for the v3 API
21+
start_response = client.sessions.start(
22+
model_name: "openai/gpt-5-nano",
23+
x_language: :typescript,
24+
x_sdk_version: "3.0.6"
25+
)
26+
puts "Session started: #{start_response.data.session_id}"
27+
28+
session_id = start_response.data.session_id
29+
30+
# Navigate to a webpage
31+
# frame_id is required - use empty string for the main frame
32+
client.sessions.navigate(
33+
session_id,
34+
url: "https://news.ycombinator.com",
35+
frame_id: "",
36+
x_language: :typescript,
37+
x_sdk_version: "3.0.6"
38+
)
39+
puts "Navigated to Hacker News"
40+
41+
# Use Observe to find possible actions on the page
42+
observe_response = client.sessions.observe(
43+
session_id,
44+
instruction: "find the link to view comments for the top post",
45+
x_language: :typescript,
46+
x_sdk_version: "3.0.6"
47+
)
48+
49+
actions = observe_response.data.result
50+
puts "Found #{actions.length} possible actions"
51+
52+
if actions.empty?
53+
puts "No actions found"
54+
exit 1
55+
end
56+
57+
# Take the first action returned by Observe
58+
action = actions.first
59+
puts "Acting on: #{action.description}"
60+
61+
# Pass the structured action to Act
62+
# Convert the observe result to a hash and ensure method is set to "click"
63+
act_response = client.sessions.act(
64+
session_id,
65+
input: action.to_h.merge(method: "click"),
66+
x_language: :typescript,
67+
x_sdk_version: "3.0.6"
68+
)
69+
puts "Act completed: #{act_response.data.result[:message]}"
70+
71+
# Extract data from the page
72+
# We're now on the comments page, so extract the top comment text
73+
extract_response = client.sessions.extract(
74+
session_id,
75+
instruction: "extract the text of the top comment on this page",
76+
schema: {
77+
type: "object",
78+
properties: {
79+
comment_text: {
80+
type: "string",
81+
description: "The text content of the top comment"
82+
},
83+
author: {
84+
type: "string",
85+
description: "The username of the comment author"
86+
}
87+
},
88+
required: ["comment_text"]
89+
},
90+
x_language: :typescript,
91+
x_sdk_version: "3.0.6"
92+
)
93+
puts "Extracted data: #{extract_response.data.result}"
94+
95+
# Get the author from the extracted data
96+
extracted_data = extract_response.data.result
97+
author = extracted_data[:author]
98+
puts "Looking up profile for author: #{author}"
99+
100+
# Use the Agent to find the author's profile
101+
# Execute runs an autonomous agent that can navigate and interact with pages
102+
execute_response = client.sessions.execute(
103+
session_id,
104+
execute_options: {
105+
instruction: "Find any personal website, GitHub, LinkedIn, or other best profile URL for the Hacker News user '#{author}'. " \
106+
"Click on their username to go to their profile page and look for any links they have shared.",
107+
max_steps: 15
108+
},
109+
agent_config: {
110+
model: Stagehand::ModelConfig::ModelConfigObject.new(
111+
model_name: "openai/gpt-5-nano",
112+
api_key: ENV["MODEL_API_KEY"]
113+
),
114+
cua: false
115+
},
116+
x_language: :typescript,
117+
x_sdk_version: "3.0.6"
118+
)
119+
puts "Agent completed: #{execute_response.data.result[:message]}"
120+
puts "Agent success: #{execute_response.data.result[:success]}"
121+
puts "Agent actions taken: #{execute_response.data.result[:actions]&.length || 0}"
122+
123+
# End the session to cleanup browser resources
124+
client.sessions.end_(
125+
session_id,
126+
x_language: :typescript,
127+
x_sdk_version: "3.0.6"
128+
)
129+
puts "Session ended"

lib/stagehand/internal/util.rb

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,12 @@ class << self
272272
#
273273
# @return [Hash{String=>Array<String>}]
274274
def decode_query(query)
275-
CGI.parse(query.to_s)
275+
return {} if query.nil? || query.empty?
276+
277+
# Use URI.decode_www_form for Ruby 3.2+ and 4.0+ compatibility
278+
URI.decode_www_form(query.to_s).each_with_object({}) do |(key, value), hash|
279+
(hash[key] ||= []) << value
280+
end
276281
end
277282

278283
# @api private

lib/stagehand/resources/sessions.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ def end_(id, params = {})
140140
x_sent_at: "x-sent-at",
141141
x_stream_response: "x-stream-response"
142142
),
143+
body: {},
143144
model: Stagehand::Models::SessionEndResponse,
144145
options: options
145146
)

0 commit comments

Comments
 (0)