charai 0.1.0 → 0.2.0.beta1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a2bb9857e550fece14adf8d4b0ff13d3f1bd5006be2cad4a047e03a23e6192ef
4
- data.tar.gz: bed2120f92dc260ea88d6f38bcef45f533705f2b027239e4e21068d17ed1e59d
3
+ metadata.gz: 0da70ce8b749b76af884e10bc9a801965e49e80defde5b118cbecaae71277ac5
4
+ data.tar.gz: bafaf0e4d31d0a9e0bc7e2bfd381c1671cf8b9776d13e40b23f50f4ab1671c3b
5
5
  SHA512:
6
- metadata.gz: 295c7c1f728df8c6daa02814c0a00a21c02c9b4182e361688a38a4e011b90368b98f78efcce5eb0baf996862ee4081c2c1a5b6d2b7359e948dad0d71ba1fadc0
7
- data.tar.gz: 85660fa979d2f8476022a625eed86c2b870f1d6983c194a0f846bafbfa3a9c27fefd558bbde1bcf522f4bc1cf183782bd09e4ba67797198691e9cbdac4c13bea
6
+ metadata.gz: 1d20a04f1c1653c8e4482bd0f454a567ba08ff571f7ff0d6aa1f634e9a0146e6b8f2e17684bd3f7441f92ba3428e06cc32541a8b701e54253cbc4e5181a68525
7
+ data.tar.gz: d27e90d862d238ee218f998eca39774afbe795c4427a3e837efa2f8f5879f6e2802ff8b07b22f5d9ea8ea36154b5103a9e9e45396f06411bb80d8e21d15d7a19
data/README.md CHANGED
@@ -1,3 +1,6 @@
1
+ [![Gem Version](https://badge.fury.io/rb/charai.svg)](https://badge.fury.io/rb/charai)
2
+
3
+
1
4
  # Charai
2
5
 
3
6
  Chat + Ruby + AI = Charai
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```/m).map(&:first).each do |code|
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
- deserialize(result['result'])
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.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.capture_screenshot
130
+ driver.aria_snapshot(ref: true)
120
131
  ```
121
132
 
122
- のような指示だけを出力してください。 `driver.capture_screenshot` を呼ぶと、その後、私が画像をアップロードします。その画像を見て、ログイン画面のままであれば、再度上記のようなログイン手順を、ログインを完了できるように指示だけ出力してください。
133
+ のような指示だけを出力してください。 `driver.aria_snapshot(ref: true)` が実行されると、私が以下のようにARIAスナップショットの取得結果を返します。
123
134
 
124
135
  ```
125
- driver.click(x: 100, y: 320)
126
- driver.type_text("Passw0rd!")
127
- driver.press_key("Enter")
128
- driver.sleep_seconds(2)
129
- driver.capture_screenshot
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
- * 必ず、画像を見てクリックする場所がどこかを判断して `driver.click` を実行するようにしてください。場所がわからない場合には `driver.execute_script` を活用して、要素の場所を確認してください。 `driver.execute_script` を呼ぶと、私がJavaScriptの実行結果をアップロードします。現在のDOMの内容を確認したいときにも `driver.execute_script` は使用できます。例えば `driver.execute_script('document.body.innerHTML')` を実行すると現在のDOMのBodyのHTMLを取得することができます。
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