charai 0.1.0

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.
@@ -0,0 +1,129 @@
1
+ module Charai
2
+ # ref: https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/widget/OverScroller.java
3
+ # Converted using ChatGPT 4o
4
+ #
5
+ # Usage:
6
+ # deceleration = SplineDeceleration.new(200)
7
+ # loop do
8
+ # delta = deceleration.calc
9
+ # sleep 0.016 # 60fps
10
+ # break if delta.zero?
11
+ # end
12
+
13
+ class SplineDeceleration
14
+ DECELERATION_RATE = Math.log(0.78) / Math.log(0.9)
15
+ INFLEXION = 0.35
16
+ START_TENSION = 0.5
17
+ END_TENSION = 1.0
18
+ P1 = START_TENSION * INFLEXION
19
+ P2 = 1.0 - END_TENSION * (1.0 - INFLEXION)
20
+ NB_SAMPLES = 100
21
+
22
+ attr_reader :current_velocity, :current_position
23
+
24
+ def initialize(initial_velocity)
25
+ @initial_velocity = initial_velocity
26
+ @current_velocity = initial_velocity
27
+ @physical_coeff = 1000
28
+ @elapsed_time = 0 # 累積時間を管理
29
+ @duration = calculate_duration
30
+ @distance = calculate_distance
31
+ @previous_position = 0 # 前回の位置を保持
32
+ @current_position = 0
33
+
34
+ # スプラインテーブルを構築
35
+ @spline_position = Array.new(NB_SAMPLES + 1)
36
+ @spline_time = Array.new(NB_SAMPLES + 1)
37
+ build_spline_tables
38
+ end
39
+
40
+ def calculate_distance
41
+ l = Math.log(INFLEXION * @initial_velocity.abs / @physical_coeff)
42
+ decel_minus_one = DECELERATION_RATE - 1.0
43
+ @physical_coeff * Math.exp(DECELERATION_RATE / decel_minus_one * l)
44
+ end
45
+
46
+ def calculate_duration
47
+ l = Math.log(INFLEXION * @initial_velocity.abs / @physical_coeff)
48
+ decel_minus_one = DECELERATION_RATE - 1.0
49
+ (1000.0 * Math.exp(l / decel_minus_one)).to_i
50
+ end
51
+
52
+ def calc
53
+ @elapsed_time += 16 # 毎回16ms経過
54
+
55
+ return 0 if @elapsed_time > @duration
56
+
57
+ t = @elapsed_time.to_f / @duration
58
+ distance_coef = interpolate_spline_position(t)
59
+ @current_velocity = interpolate_spline_velocity(t)
60
+
61
+ # 現在の位置を計算
62
+ @current_position = (distance_coef * @distance).to_i
63
+
64
+ # 16ms前の位置からのdeltaを計算
65
+ delta_position = @current_position - @previous_position
66
+
67
+ # 現在の位置を次回のために保存
68
+ @previous_position = @current_position
69
+
70
+ delta_position
71
+ end
72
+
73
+ private
74
+
75
+ def build_spline_tables
76
+ (0...NB_SAMPLES).each do |i|
77
+ alpha = i.to_f / NB_SAMPLES
78
+ # x 方向のスプライン補間
79
+ x_min = 0.0
80
+ x_max = 1.0
81
+ while true
82
+ x = (x_min + x_max) / 2.0
83
+ coef = 3.0 * x * (1.0 - x)
84
+ tx = coef * ((1.0 - x) * P1 + x * P2) + x**3
85
+ break if (tx - alpha).abs < 1e-5
86
+ tx > alpha ? x_max = x : x_min = x
87
+ end
88
+ @spline_position[i] = coef * ((1.0 - x) * START_TENSION + x) + x**3
89
+
90
+ # y 方向のスプライン補間
91
+ y_min = 0.0
92
+ y_max = 1.0
93
+ while true
94
+ y = (y_min + y_max) / 2.0
95
+ coef = 3.0 * y * (1.0 - y)
96
+ dy = coef * ((1.0 - y) * START_TENSION + y) + y**3
97
+ break if (dy - alpha).abs < 1e-5
98
+ dy > alpha ? y_max = y : y_min = y
99
+ end
100
+ @spline_time[i] = coef * ((1.0 - y) * P1 + y * P2) + y**3
101
+ end
102
+ @spline_position[NB_SAMPLES] = @spline_time[NB_SAMPLES] = 1.0
103
+ end
104
+
105
+ def interpolate_spline_position(t)
106
+ index = (NB_SAMPLES * t).to_i
107
+ return 1.0 if index >= NB_SAMPLES
108
+
109
+ t_inf = index.to_f / NB_SAMPLES
110
+ t_sup = (index + 1).to_f / NB_SAMPLES
111
+ d_inf = @spline_position[index]
112
+ d_sup = @spline_position[index + 1]
113
+ velocity_coef = (d_sup - d_inf) / (t_sup - t_inf)
114
+ d_inf + (t - t_inf) * velocity_coef
115
+ end
116
+
117
+ def interpolate_spline_velocity(t)
118
+ index = (NB_SAMPLES * t).to_i
119
+ return 0.0 if index >= NB_SAMPLES
120
+
121
+ t_inf = index.to_f / NB_SAMPLES
122
+ t_sup = (index + 1).to_f / NB_SAMPLES
123
+ d_inf = @spline_position[index]
124
+ d_sup = @spline_position[index + 1]
125
+ velocity_coef = (d_sup - d_inf) / (t_sup - t_inf)
126
+ velocity_coef * @distance / @duration * 1000.0
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,11 @@
1
+ module Charai
2
+ class Util
3
+ def self.macos?
4
+ RUBY_PLATFORM =~ /darwin/
5
+ end
6
+
7
+ def self.linux?
8
+ RUBY_PLATFORM =~ /linux/
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Charai
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,156 @@
1
+ require 'openssl'
2
+ require 'socket'
3
+ require 'websocket/driver'
4
+
5
+ module Charai
6
+ # ref: https://github.com/rails/rails/blob/master/actioncable/lib/action_cable/connection/client_socket.rb
7
+ # ref: https://github.com/cavalle/chrome_remote/blob/master/lib/chrome_remote/web_socket_client.rb
8
+ class WebSocket
9
+ class DriverImpl # providing #url, #write(string)
10
+ class SecureSocketFactory
11
+ def initialize(host, port)
12
+ @host = host
13
+ @port = port || 443
14
+ end
15
+
16
+ def create
17
+ tcp_socket = TCPSocket.new(@host, @port)
18
+ OpenSSL::SSL::SSLSocket.new(tcp_socket).tap(&:connect)
19
+ end
20
+ end
21
+
22
+ def initialize(url)
23
+ @url = url
24
+
25
+ endpoint = URI.parse(url)
26
+ @socket =
27
+ if endpoint.scheme == 'wss'
28
+ SecureSocketFactory.new(endpoint.host, endpoint.port).create
29
+ else
30
+ TCPSocket.new(endpoint.host, endpoint.port)
31
+ end
32
+ end
33
+
34
+ attr_reader :url
35
+
36
+ def write(data)
37
+ @socket.write(data)
38
+ rescue Errno::EPIPE
39
+ raise EOFError.new('already closed')
40
+ rescue Errno::ECONNRESET
41
+ raise EOFError.new('closed by remote')
42
+ end
43
+
44
+ def readpartial(maxlen = 1024)
45
+ @socket.readpartial(maxlen)
46
+ rescue Errno::ECONNRESET
47
+ raise EOFError.new('closed by remote')
48
+ end
49
+
50
+ def dispose
51
+ @socket.close
52
+ end
53
+ end
54
+
55
+ STATE_CONNECTING = 0
56
+ STATE_OPENED = 1
57
+ STATE_CLOSING = 2
58
+ STATE_CLOSED = 3
59
+
60
+ def initialize(url:, max_payload_size: 256 * 1024 * 1024)
61
+ @impl = DriverImpl.new(url)
62
+ @driver = ::WebSocket::Driver.client(@impl, max_length: max_payload_size)
63
+
64
+ setup
65
+ @driver.start
66
+
67
+ Thread.new do
68
+ wait_for_data until @ready_state >= STATE_CLOSING
69
+ rescue EOFError
70
+ # Browser was gone.
71
+ # We have nothing todo. Just finish polling.
72
+ if @ready_state < STATE_CLOSING
73
+ handle_on_close(reason: 'Going Away', code: 1001)
74
+ end
75
+ rescue IOError
76
+ # connection closed. Just ignore it.
77
+ raise if @ready_state < STATE_CLOSING
78
+ end
79
+ end
80
+
81
+ private def setup
82
+ @ready_state = STATE_CONNECTING
83
+ @driver.on(:open) do
84
+ @ready_state = STATE_OPENED
85
+ handle_on_open
86
+ end
87
+ @driver.on(:close) do |event|
88
+ @ready_state = STATE_CLOSED
89
+ handle_on_close(reason: event.reason, code: event.code)
90
+ end
91
+ @driver.on(:error) do |event|
92
+ unless handle_on_error(error_message: event.message)
93
+ raise event.message
94
+ end
95
+ end
96
+ @driver.on(:message) do |event|
97
+ handle_on_message(event.data)
98
+ end
99
+ end
100
+
101
+ private def wait_for_data
102
+ @driver.parse(@impl.readpartial)
103
+ end
104
+
105
+ # @param message [String]
106
+ def send_text(message)
107
+ return if @ready_state >= STATE_CLOSING
108
+ @driver.text(message)
109
+ end
110
+
111
+ def close(code: 1000, reason: "")
112
+ return if @ready_state >= STATE_CLOSING
113
+ @ready_state = STATE_CLOSING
114
+ @driver.close(reason, code)
115
+ @impl.dispose
116
+ end
117
+
118
+ def on_open(&block)
119
+ @on_open = block
120
+ end
121
+
122
+ # @param block [Proc(reason: String, code: Numeric)]
123
+ def on_close(&block)
124
+ @on_close = block
125
+ end
126
+
127
+ # @param block [Proc(error_message: String)]
128
+ def on_error(&block)
129
+ @on_error = block
130
+ end
131
+
132
+ def on_message(&block)
133
+ @on_message = block
134
+ end
135
+
136
+ private def handle_on_open
137
+ @on_open&.call
138
+ end
139
+
140
+ private def handle_on_close(reason:, code:)
141
+ @on_close&.call(reason, code)
142
+ end
143
+
144
+ private def handle_on_error(error_message:)
145
+ return false if @on_error.nil?
146
+
147
+ @on_error.call(error_message)
148
+ end
149
+
150
+ private def handle_on_message(data)
151
+ return if @ready_state != STATE_OPENED
152
+
153
+ @on_message&.call(data)
154
+ end
155
+ end
156
+ end
data/lib/charai.rb ADDED
@@ -0,0 +1,18 @@
1
+ require 'charai/version'
2
+
3
+ require 'capybara'
4
+ require 'concurrent'
5
+
6
+ require 'charai/action_queue'
7
+ require 'charai/agent'
8
+ require 'charai/browser'
9
+ require 'charai/browser_launcher'
10
+ require 'charai/browser_process'
11
+ require 'charai/browsing_context'
12
+ require 'charai/driver'
13
+ require 'charai/input_tool'
14
+ require 'charai/openai_chat'
15
+ require 'charai/openai_configuration'
16
+ require 'charai/spline_deceleration'
17
+ require 'charai/util'
18
+ require 'charai/web_socket'
metadata ADDED
@@ -0,0 +1,109 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: charai
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - YusukeIwaki
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2024-10-25 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: capybara
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: concurrent-ruby
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 1.1.6
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: 1.1.6
41
+ - !ruby/object:Gem::Dependency
42
+ name: websocket-driver
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: 0.6.1
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: 0.6.1
55
+ description: Prototype impl for Kaigi on Rails 2024 presentation.
56
+ email:
57
+ - q7w8e9w8q7w8e9@yahoo.co.jp
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - ".rspec"
63
+ - ".rubocop.yml"
64
+ - Gemfile
65
+ - LICENSE.txt
66
+ - README.md
67
+ - Rakefile
68
+ - bin/console
69
+ - charai.gemspec
70
+ - lib/charai.rb
71
+ - lib/charai/action_queue.rb
72
+ - lib/charai/agent.rb
73
+ - lib/charai/browser.rb
74
+ - lib/charai/browser_launcher.rb
75
+ - lib/charai/browser_process.rb
76
+ - lib/charai/browsing_context.rb
77
+ - lib/charai/driver.rb
78
+ - lib/charai/input_tool.rb
79
+ - lib/charai/openai_chat.rb
80
+ - lib/charai/openai_configuration.rb
81
+ - lib/charai/spline_deceleration.rb
82
+ - lib/charai/util.rb
83
+ - lib/charai/version.rb
84
+ - lib/charai/web_socket.rb
85
+ homepage: https://github.com/YusukeIwaki/charai
86
+ licenses:
87
+ - MIT
88
+ metadata:
89
+ homepage_uri: https://github.com/YusukeIwaki/charai
90
+ post_install_message:
91
+ rdoc_options: []
92
+ require_paths:
93
+ - lib
94
+ required_ruby_version: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - ">="
97
+ - !ruby/object:Gem::Version
98
+ version: 3.2.0
99
+ required_rubygems_version: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ requirements: []
105
+ rubygems_version: 3.5.3
106
+ signing_key:
107
+ specification_version: 4
108
+ summary: charai(Chat + Ruby + AI) Capybara driver
109
+ test_files: []