charai 0.1.0 → 0.2.0.beta2
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.
- checksums.yaml +4 -4
- data/README.md +3 -0
- data/lib/charai/agent.rb +1 -1
- data/lib/charai/browsing_context.rb +103 -6
- data/lib/charai/driver.rb +44 -9
- data/lib/charai/injectedScriptSource.js +34 -0
- data/lib/charai/injected_script.rb +40 -0
- data/lib/charai/input_tool.rb +61 -1
- data/lib/charai/openai_configuration.rb +20 -0
- data/lib/charai/version.rb +1 -1
- data/lib/charai.rb +1 -0
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7bfb144fd502f114e8749abd5cc23188a99014ad30e47452e81a5b2aa755028a
|
4
|
+
data.tar.gz: 5210f942e451c57aaae8bfa72cd803dc77ec8cffc5c517ac7b2da56498d70b9e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e900017b056d4f0d58e3895a4364a575b1925b10ca41d5fa50066d2394329f8f5de897deadafe6b5cd8767b506021f373bebe6bbea2f3df7d9c9b1a6cbe7c06f
|
7
|
+
data.tar.gz: d11259eb89c00fa35a76125ab8c339b697de67f538865a7d8df6d1a497cf1bb51770d0654097527e318e20d864f19a4412278577814d2e0134f800eb468371a8
|
data/README.md
CHANGED
data/lib/charai/agent.rb
CHANGED
@@ -65,7 +65,7 @@ module Charai
|
|
65
65
|
with_message_queuing do
|
66
66
|
with_aggregating_failures do
|
67
67
|
begin
|
68
|
-
answer.scan(/```[a-zA-Z]*\n(.*?)\n
|
68
|
+
answer.scan(/```[a-zA-Z]*\n(.*?)\n\s*```/m).map(&:first).each do |code|
|
69
69
|
if code.include?('`') # Avoid OS shell execution.
|
70
70
|
raise HandleMessageError, "It is not allowed to use backquote"
|
71
71
|
end
|
@@ -123,6 +123,16 @@ module Charai
|
|
123
123
|
}).value!
|
124
124
|
end
|
125
125
|
|
126
|
+
class Handle
|
127
|
+
def initialize(object_id)
|
128
|
+
@object_id = object_id
|
129
|
+
end
|
130
|
+
|
131
|
+
def as_serialized
|
132
|
+
{ handle: @object_id }
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
126
136
|
class Realm
|
127
137
|
def initialize(browsing_context:, id:, origin:, type: nil)
|
128
138
|
@browsing_context = browsing_context
|
@@ -133,44 +143,129 @@ module Charai
|
|
133
143
|
|
134
144
|
class ScriptEvaluationError < StandardError; end
|
135
145
|
|
136
|
-
def script_evaluate(expression)
|
146
|
+
def script_evaluate(expression, as_handle: false)
|
137
147
|
result = @browsing_context.send(:bidi_call_async, 'script.evaluate', {
|
138
148
|
expression: expression,
|
139
149
|
target: { realm: @id },
|
150
|
+
resultOwnership: as_handle ? 'root' : 'none',
|
151
|
+
awaitPromise: true,
|
152
|
+
userActivation: true,
|
153
|
+
}).value!
|
154
|
+
|
155
|
+
if result['type'] == 'exception'
|
156
|
+
raise ScriptEvaluationError, result['exceptionDetails']['text']
|
157
|
+
end
|
158
|
+
|
159
|
+
if as_handle
|
160
|
+
Handle.new(result['result']['handle'])
|
161
|
+
else
|
162
|
+
deserialize(result['result'])
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def script_call_function(function_declaration, arguments: [], as_handle: false)
|
167
|
+
args = arguments.map do |arg|
|
168
|
+
serialize(arg)
|
169
|
+
end
|
170
|
+
result = @browsing_context.send(:bidi_call_async, 'script.callFunction', {
|
171
|
+
functionDeclaration: function_declaration,
|
172
|
+
arguments: args,
|
173
|
+
target: { realm: @id },
|
174
|
+
resultOwnership: as_handle ? 'root' : 'none',
|
140
175
|
awaitPromise: true,
|
176
|
+
userActivation: true,
|
141
177
|
}).value!
|
142
178
|
|
143
179
|
if result['type'] == 'exception'
|
144
180
|
raise ScriptEvaluationError, result['exceptionDetails']['text']
|
145
181
|
end
|
146
182
|
|
147
|
-
|
183
|
+
if as_handle
|
184
|
+
Handle.new(result['result']['handle'])
|
185
|
+
else
|
186
|
+
deserialize(result['result'])
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
def _with_injected_script(&block)
|
191
|
+
raise ArgumentError, "block is required" unless block_given?
|
192
|
+
@injected_script ||= _new_injected_script
|
193
|
+
block.call(@injected_script)
|
148
194
|
end
|
149
195
|
|
150
196
|
attr_reader :type
|
151
197
|
|
152
198
|
private
|
153
199
|
|
200
|
+
def _new_injected_script
|
201
|
+
# Define module object before executing CommonJS code
|
202
|
+
source = File.read("#{__dir__}/injectedScriptSource.js")
|
203
|
+
options = {
|
204
|
+
isUnderTest: false,
|
205
|
+
sdkLanguage: 'ruby',
|
206
|
+
testIdAttributeName: 'data-testid',
|
207
|
+
stableRafCount: 1,
|
208
|
+
browserName: 'charai',
|
209
|
+
customEngines: [],
|
210
|
+
}
|
211
|
+
injected_script_handle = script_evaluate(<<~JAVASCRIPT, as_handle: true)
|
212
|
+
(() => {
|
213
|
+
const module = {};
|
214
|
+
#{source}
|
215
|
+
eval(module.exports.source)
|
216
|
+
return new (module.exports.InjectedScript())(globalThis, #{options.to_json});
|
217
|
+
})();
|
218
|
+
JAVASCRIPT
|
219
|
+
InjectedScript.new(self, injected_script_handle)
|
220
|
+
end
|
221
|
+
|
222
|
+
def serialize(value)
|
223
|
+
case value
|
224
|
+
when Handle
|
225
|
+
value.as_serialized
|
226
|
+
when Array
|
227
|
+
{ type: 'array', value: value.map { |v| serialize(v) } }
|
228
|
+
when Hash
|
229
|
+
{ type: 'object', value: value.map { |k, v| [serialize(k), serialize(v)] } }
|
230
|
+
when Regexp
|
231
|
+
{ type: 'regexp', value: { pattern: value.source, flags: (value.options & Regexp::MULTILINE != 0 ? 'm' : '') + (value.options & Regexp::IGNORECASE != 0 ? 'i' : '') } }
|
232
|
+
when Date, Time, DateTime
|
233
|
+
{ type: 'date', value: value.iso8601 }
|
234
|
+
when nil
|
235
|
+
{ type: 'undefined' }
|
236
|
+
when Numeric
|
237
|
+
{ type: 'number', value: value }
|
238
|
+
when String
|
239
|
+
{ type: 'string', value: value }
|
240
|
+
when Symbol
|
241
|
+
{ type: 'string', value: value.to_s }
|
242
|
+
when true, false
|
243
|
+
{ type: 'boolean', value: value }
|
244
|
+
else
|
245
|
+
raise ArgumentError, "Cannot serialize #{value.class}"
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
154
249
|
# ref: https://github.com/puppeteer/puppeteer/blob/puppeteer-v23.5.3/packages/puppeteer-core/src/bidi/Deserializer.ts#L21
|
155
250
|
# Converted using ChatGPT 4o
|
156
251
|
def deserialize(result)
|
157
252
|
case result["type"]
|
158
253
|
when 'array'
|
159
|
-
result['value']&.map { |value| deserialize(value) }
|
254
|
+
result['value']&.map { |value| deserialize(value) } || []
|
160
255
|
when 'set'
|
161
256
|
result['value']&.each_with_object(Set.new) do |value, acc|
|
162
257
|
acc.add(deserialize(value))
|
163
|
-
end
|
258
|
+
end || Set.new
|
164
259
|
when 'object'
|
165
260
|
result['value']&.each_with_object({}) do |tuple, acc|
|
166
261
|
key, value = tuple
|
167
262
|
acc[key] = deserialize(value)
|
168
|
-
end
|
263
|
+
end || {}
|
169
264
|
when 'map'
|
170
265
|
result['value']&.each_with_object({}) do |tuple, acc|
|
171
266
|
key, value = tuple
|
172
267
|
acc[key] = deserialize(value)
|
173
|
-
end
|
268
|
+
end || {}
|
174
269
|
when 'promise'
|
175
270
|
{}
|
176
271
|
when 'regexp'
|
@@ -192,6 +287,8 @@ module Charai
|
|
192
287
|
nil
|
193
288
|
when 'number', 'bigint', 'boolean', 'string'
|
194
289
|
result['value']
|
290
|
+
when 'function', 'node', 'window'
|
291
|
+
{}
|
195
292
|
else
|
196
293
|
raise ArgumentError, "Unknown type: #{result['type']}"
|
197
294
|
end
|
data/lib/charai/driver.rb
CHANGED
@@ -36,6 +36,16 @@ module Charai
|
|
36
36
|
@additional_instruction = nil
|
37
37
|
end
|
38
38
|
|
39
|
+
def save_screenshot(path = nil, **_options)
|
40
|
+
browsing_context.capture_screenshot(format: { type: 'png' }).tap do |binary|
|
41
|
+
if path
|
42
|
+
File.open(path, 'wb') do |fp|
|
43
|
+
fp.write(binary)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
39
49
|
def visit(path)
|
40
50
|
host = Capybara.app_host || Capybara.default_host
|
41
51
|
|
@@ -89,6 +99,7 @@ module Charai
|
|
89
99
|
* 画面の左上から (10ピクセル, 20ピクセル)の位置にマウスを置いてスクロール操作で下に向かってスクロールをしたい場合には `driver.scroll_down(x: 10, y: 20, velocity: 1500)`
|
90
100
|
* 同様に、上に向かってスクロールをしたい場合には `driver.scroll_up(x: 10, y: 20, velocity: 1500)`
|
91
101
|
* 画面が切り替わるまで2秒待ちたい場合には `driver.sleep_seconds(2)`
|
102
|
+
* 現在の画面のARIAスナップショットを取得したい場合には `driver.aria_snapshot(ref: true)`
|
92
103
|
* 現在の画面を一旦確認したい場合には `driver.capture_screenshot`
|
93
104
|
* DOM要素の位置を確認するために、JavaScriptの実行結果を取得したい場合は `driver.execute_script('JSON.stringify(document.querySelector("#some").getBoundingClientRect())')`
|
94
105
|
* テスト項目1がOKの場合には `driver.assertion_ok("テスト項目1")` 、テスト項目2がNGの場合には `driver.assertion_fail("テスト項目2")`
|
@@ -116,27 +127,51 @@ module Charai
|
|
116
127
|
driver.type_text("Passw0rd!")
|
117
128
|
driver.press_key("Enter")
|
118
129
|
driver.sleep_seconds(2)
|
119
|
-
driver.
|
130
|
+
driver.aria_snapshot(ref: true)
|
120
131
|
```
|
121
132
|
|
122
|
-
のような指示だけを出力してください。
|
133
|
+
のような指示だけを出力してください。 `driver.aria_snapshot(ref: true)` が実行されると、私が以下のようにARIAスナップショットの取得結果を返します。
|
123
134
|
|
124
135
|
```
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
136
|
+
- generic [active] [ref=e1]:
|
137
|
+
- heading "Login form" [level=1] [ref=e2]
|
138
|
+
- generic [ref=e3]:
|
139
|
+
- generic [ref=e4]:
|
140
|
+
- generic [ref=e5]: Email
|
141
|
+
- textbox "Email" [ref=e6]
|
142
|
+
- generic [ref=e7]:
|
143
|
+
- generic [ref=e8]: Password
|
144
|
+
- textbox "Password" [ref=e9]
|
145
|
+
- button "LOGIN" [ref=e10]
|
146
|
+
```
|
147
|
+
|
148
|
+
refの値を使用して、その要素に関連した関数の実行を行うことができます。たとえば、Email入力欄のDOMの領域を取得したい場合には
|
149
|
+
|
150
|
+
```
|
151
|
+
driver.execute_script_with_ref(:e6, "el => JSON.stringify(el.getBoundingClientRect())")
|
152
|
+
```
|
153
|
+
|
154
|
+
このように実行すれば、私が以下のように実行結果を返します。
|
155
|
+
|
130
156
|
```
|
157
|
+
{"top":396.25,"right":638.4140625,"bottom":422.25,"left":488.4140625,"width":150,"height":26,"x":488.4140625,"y":396.25}
|
158
|
+
```
|
159
|
+
|
160
|
+
これで、要素の真ん中をクリックしたい場合には `driver.click(x: 563, y: 409)` のように実行できます。
|
161
|
+
|
162
|
+
ARIAスナップショットではなく、色なども含めて画面の見た目を確認したい場合には、 `driver.capture_screenshot` を呼ぶと、その後、私が画像をアップロードします。
|
163
|
+
`driver.capture_screenshot` は画像を返しますが、とても大きなデータを必要としますので、本当に見た目を確認する必要がある場合にのみ使用してください。
|
164
|
+
多くの場合には、画面のスクリーンショットを取得するよりもARIAスナップショットを使用したほうが、効率的に画面の状態を確認できます。
|
131
165
|
|
132
166
|
### 注意点
|
133
167
|
* ログイン後のダッシュボード画面に遷移したと判断したら `driver.assertion_ok("ログイン後のダッシュボード画面に遷移すること")` のような指示だけ出力してください。5回やってもうまくいかない場合には `driver.assertion_fail("ログイン後のダッシュボード画面に遷移すること")` のような指示だけ出力してください。
|
134
|
-
*
|
168
|
+
* 必ず、ARIAスナップショットまたはスクリーンキャプチャ画像を見てクリックする場所がどこかを判断して `driver.click` を実行するようにしてください。場所がわからない場合には `driver.execute_script` を活用して、要素の場所を確認してください。 `driver.execute_script` を呼ぶと、私がJavaScriptの実行結果をアップロードします。現在のDOMの内容を確認したいときにも `driver.execute_script` は使用できます。例えば `driver.execute_script('document.body.innerHTML')` を実行すると現在のDOMのBodyのHTMLを取得することができます。
|
135
169
|
* 何も変化がない場合には、正しい場所をクリックできていない可能性が高いです。その場合には上記のgetBoundingClientRectを使用する手順で、クリックまたはスクロールする位置を必ず確かめてください。
|
136
170
|
* 画面外の要素はクリックできないので、getBoundingClientRectの結果、画面外にあることが判明したら、画面内に表示されるようにスクロールしてからクリックしてください。
|
137
171
|
* 一覧画面などでは、画面の一部だけがスクロールすることもあります。その場合には、スクロールする要素を特定して、その要素の位置を取得してからスクロール操作を行ってください。
|
172
|
+
* 実行すべきコードは必ず "```" を使用したコードブロックで囲んでください。コードブロックに囲まれていないコードは実行されません。
|
138
173
|
* `driver.execute_script` を複数実行した場合には、私は最後の結果だけをアップロードしますので、getBoundingClientRectを複数回使用する場合には、1回ずつ分けて指示してください。
|
139
|
-
* 最後に実行された内容が `driver.capture_screenshot` または `driver.execute_script` ではない場合には、会話が強制終了してしまいますので、操作を続ける必要がある場合には `driver.execute_script` または `driver.capture_screenshot` を最後に実行してください。
|
174
|
+
* 最後に実行された内容が `driver.capture_screenshot` または `driver.execute_script` または `driver.execute_script_with_ref` ではない場合には、会話が強制終了してしまいますので、操作を続ける必要がある場合には `driver.execute_script` または `driver.execute_script_with_ref` または `driver.capture_screenshot` を最後に実行してください。
|
140
175
|
|
141
176
|
#{@additional_instruction ? "### 補足説明\n#{@additional_instruction}" : ""}
|
142
177
|
|