ruby_agent 0.2.2 → 0.2.3
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/Gemfile +1 -0
- data/README.md +14 -24
- data/Rakefile +13 -7
- data/examples/example1.rb +105 -0
- data/lib/ruby_agent/agent.rb +260 -0
- data/lib/ruby_agent/callback_support.rb +71 -0
- data/lib/ruby_agent/configuration.rb +12 -0
- data/lib/ruby_agent/event.rb +9 -0
- data/lib/ruby_agent/response.rb +24 -0
- data/lib/ruby_agent/version.rb +2 -2
- data/lib/ruby_agent.rb +13 -419
- metadata +7 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b1cc2c50708573c25ab812c6e10096d6be6aaea4580562a19951c90c35f8c387
|
|
4
|
+
data.tar.gz: 7857009a269597f17bdb800d06fe638626e704f4a57995a24594f5c3223183b6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3b39897b45ac0494ec5e559a402d1bb3deb49f6a48b218d5e483052c22143f96e2f28333cfa31763a7db3437feb7fee3170dc17953042dbe636ab2a7a6bad926
|
|
7
|
+
data.tar.gz: 97b30ea700b18198394bd02ba3d51aa3dcb127020a031506b42d5b4c23038c882c761622c8b9458538ccad1719743997f49a68ad66bf20643994111ed76b10ea
|
data/Gemfile
CHANGED
data/README.md
CHANGED
|
@@ -37,28 +37,13 @@ gem 'ruby_agent'
|
|
|
37
37
|
```ruby
|
|
38
38
|
require 'ruby_agent'
|
|
39
39
|
|
|
40
|
-
agent = RubyAgent.new
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
)
|
|
44
|
-
|
|
45
|
-
agent.on_assistant do |event, all_events|
|
|
46
|
-
if event.dig("message", "content", 0, "type") == "text"
|
|
47
|
-
text = event.dig("message", "content", 0, "text")
|
|
48
|
-
puts "Assistant: #{text}"
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
agent.on_result do |event, all_events|
|
|
53
|
-
puts "Result: #{event['subtype']}"
|
|
54
|
-
agent.exit if event["subtype"] == "success"
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
agent.connect do
|
|
58
|
-
agent.ask("What is 1+1?", sender_name: "User")
|
|
59
|
-
end
|
|
40
|
+
agent = RubyAgent.new
|
|
41
|
+
agent.on_result { |e, _| agent.exit if e["subtype"] == "success" }
|
|
42
|
+
agent.connect { agent.ask("What is 2+2?") }
|
|
60
43
|
```
|
|
61
44
|
|
|
45
|
+
That's it! Three lines to create an agent, ask Claude a question, and exit when done.
|
|
46
|
+
|
|
62
47
|
### Advanced Example with Callbacks
|
|
63
48
|
|
|
64
49
|
```ruby
|
|
@@ -219,9 +204,14 @@ end
|
|
|
219
204
|
rake ci
|
|
220
205
|
|
|
221
206
|
# Or run tasks individually:
|
|
222
|
-
rake ci:test
|
|
223
|
-
rake ci:lint
|
|
224
|
-
rake ci:
|
|
207
|
+
rake ci:test # Run test suite
|
|
208
|
+
rake ci:lint # Run RuboCop linter
|
|
209
|
+
rake ci:lint:fix # Auto-fix linting issues
|
|
210
|
+
rake ci:scan # Run security audit
|
|
211
|
+
|
|
212
|
+
# To run manual examples build locally:
|
|
213
|
+
rake build
|
|
214
|
+
rake install
|
|
225
215
|
```
|
|
226
216
|
|
|
227
217
|
5. Commit your changes: `git commit -am 'Add some feature'`
|
|
@@ -251,7 +241,7 @@ We use RuboCop for code linting:
|
|
|
251
241
|
rake ci:lint
|
|
252
242
|
|
|
253
243
|
# Auto-fix linting issues
|
|
254
|
-
|
|
244
|
+
rake ci:lint:fix
|
|
255
245
|
```
|
|
256
246
|
|
|
257
247
|
### Publishing
|
data/Rakefile
CHANGED
|
@@ -15,19 +15,25 @@ namespace :ci do
|
|
|
15
15
|
sh "bundle exec rake test"
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
namespace :lint do
|
|
19
|
+
desc "Run linter"
|
|
20
|
+
task :default do
|
|
21
|
+
sh "bundle exec rubocop"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
desc "Auto-fix linting issues"
|
|
25
|
+
task :fix do
|
|
26
|
+
sh "bundle exec rubocop -a"
|
|
27
|
+
end
|
|
23
28
|
end
|
|
24
29
|
|
|
25
30
|
desc "Run security scan"
|
|
26
31
|
task :scan do
|
|
27
32
|
sh "bundle exec bundler-audit check --update"
|
|
28
|
-
rescue StandardError
|
|
29
|
-
puts "Bundler-audit not installed, skipping..."
|
|
30
33
|
end
|
|
34
|
+
|
|
35
|
+
# alias ci:lint to ci:lint:default
|
|
36
|
+
task lint: "lint:default"
|
|
31
37
|
end
|
|
32
38
|
|
|
33
39
|
desc "Run all CI tasks"
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
require "dotenv/load"
|
|
2
|
+
require "reline"
|
|
3
|
+
|
|
4
|
+
# Register the mcp server
|
|
5
|
+
# claude mcp add --transport http headless-browser http://localhost:4567/mcp
|
|
6
|
+
# claude --dangerously-skip-permissions
|
|
7
|
+
|
|
8
|
+
# Install the headless_browser_tool gem
|
|
9
|
+
# gem install headless_browser_tool --source https://github.com/krschacht/headless-browser-tool.git
|
|
10
|
+
|
|
11
|
+
# Before running start the hbt server in a separate terminal:
|
|
12
|
+
# bundle exec hbt start --no-headless --be-human --single-session --session-id=amazon
|
|
13
|
+
|
|
14
|
+
# Load local development version instead of installed gem
|
|
15
|
+
$LOAD_PATH.unshift File.expand_path("../lib", __dir__)
|
|
16
|
+
require "ruby_agent"
|
|
17
|
+
|
|
18
|
+
class MyAgent < RubyAgent::Agent
|
|
19
|
+
# 1. General event handler - fires for ALL events
|
|
20
|
+
on_event :my_handler
|
|
21
|
+
|
|
22
|
+
def my_handler(event)
|
|
23
|
+
puts "Event triggered"
|
|
24
|
+
puts "Received event type: #{event['type']}"
|
|
25
|
+
puts "Received event: #{event.dig('message', 'id')}"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# 2. Event-specific handler - fires only for assistant messages
|
|
29
|
+
on_event_assistant do |_event|
|
|
30
|
+
puts "Assistant message received"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# 3. Another event-specific handler - fires only for content_block_delta events
|
|
34
|
+
on_event_content_block_delta :streaming_handler
|
|
35
|
+
|
|
36
|
+
def streaming_handler(event)
|
|
37
|
+
# Handle streaming text output for content_block_delta events only
|
|
38
|
+
return unless event.dig("delta", "text")
|
|
39
|
+
|
|
40
|
+
print event["delta"]["text"]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# TODO: Add this
|
|
44
|
+
# def on_event_result(event)
|
|
45
|
+
# puts "\nConversation complete!"
|
|
46
|
+
# end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
DONE = %w[done end eof exit].freeze
|
|
50
|
+
|
|
51
|
+
def prompt_for_message
|
|
52
|
+
puts "\n(multiline input; type 'end' on its own line when done. or 'exit' to exit)\n\n"
|
|
53
|
+
|
|
54
|
+
user_message = Reline.readmultiline("User message: ", true) do |multiline_input|
|
|
55
|
+
last = multiline_input.split.last
|
|
56
|
+
DONE.include?(last)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
return :noop unless user_message
|
|
60
|
+
|
|
61
|
+
lines = user_message.split("\n")
|
|
62
|
+
|
|
63
|
+
if lines.size > 1 && DONE.include?(lines.last)
|
|
64
|
+
# remove the "done" from the message
|
|
65
|
+
user_message = lines[0..-2].join("\n")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
return :exit if DONE.include?(user_message.downcase)
|
|
69
|
+
|
|
70
|
+
user_message
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
begin
|
|
74
|
+
RubyAgent.configure do |config|
|
|
75
|
+
config.anthropic_api_key = ENV.fetch("ANTHROPIC_API_KEY", nil) # Not strictly necessary with claude installed
|
|
76
|
+
config.system_prompt = "You are a helpful AI news assistant."
|
|
77
|
+
config.model = "claude-sonnet-4-5-20250929"
|
|
78
|
+
config.sandbox_dir = "./news_sandbox"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
agent = MyAgent.new(name: "News-Agent").connect(mcp_servers: { headless_browser: { type: :http,
|
|
82
|
+
url: "http://0.0.0.0:4567/mcp" } })
|
|
83
|
+
|
|
84
|
+
puts "Welcome to your Claude assistant!"
|
|
85
|
+
|
|
86
|
+
loop do
|
|
87
|
+
user_message = prompt_for_message
|
|
88
|
+
|
|
89
|
+
case user_message
|
|
90
|
+
when :noop
|
|
91
|
+
next
|
|
92
|
+
when :exit
|
|
93
|
+
break
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
puts "Asking Claude..."
|
|
97
|
+
response = agent.ask(user_message)
|
|
98
|
+
puts "\n\nFinal response:\n\n"
|
|
99
|
+
puts response.final_text
|
|
100
|
+
end
|
|
101
|
+
rescue Interrupt
|
|
102
|
+
puts "\nExiting..."
|
|
103
|
+
ensure
|
|
104
|
+
agent&.close
|
|
105
|
+
end
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
require_relative "callback_support"
|
|
2
|
+
require_relative "response"
|
|
3
|
+
|
|
4
|
+
module RubyAgent
|
|
5
|
+
class Agent
|
|
6
|
+
include CallbackSupport
|
|
7
|
+
|
|
8
|
+
class ConnectionError < StandardError; end
|
|
9
|
+
|
|
10
|
+
attr_reader :name, :sandbox_dir, :timezone, :skip_permissions, :verbose,
|
|
11
|
+
:system_prompt, :mcp_servers, :model, :session_key,
|
|
12
|
+
:context, :conversation_history
|
|
13
|
+
|
|
14
|
+
# Configure parameters for the Agent(s) like this or when initializing:
|
|
15
|
+
#
|
|
16
|
+
# RubyAgent.configure do |config|
|
|
17
|
+
# config.anthropic_api_key = ENV['ANTHROPIC_API_KEY'] # Not strictly necessary with Claude SDK
|
|
18
|
+
# config.system_prompt = "You are a helpful AI human resources assistant."
|
|
19
|
+
# config.model = "claude-sonnet-4-5-20250929"
|
|
20
|
+
# config.sandbox_dir = "./hr_sandbox"
|
|
21
|
+
# end
|
|
22
|
+
|
|
23
|
+
# Users can register callbacks in two ways:
|
|
24
|
+
#
|
|
25
|
+
# class MyAgent < RubyAgent::Agent
|
|
26
|
+
# # Using a method name
|
|
27
|
+
# on_event :my_handler # Fires for all events
|
|
28
|
+
#
|
|
29
|
+
# def my_handler(event)
|
|
30
|
+
# end
|
|
31
|
+
# end
|
|
32
|
+
#
|
|
33
|
+
# class MyAgent < RubyAgent::Agent
|
|
34
|
+
# # Using a block
|
|
35
|
+
# on_event do |event| # Fires for all events
|
|
36
|
+
# puts "Event received: #{event['type']}"
|
|
37
|
+
# end
|
|
38
|
+
# end
|
|
39
|
+
|
|
40
|
+
# You can register event-specific callbacks using the pattern
|
|
41
|
+
# on_event_<event_type>:
|
|
42
|
+
#
|
|
43
|
+
# on_event_content_block_delta :streaming_handler
|
|
44
|
+
# on_event_result :completion_handler
|
|
45
|
+
# on_event_assistant :assistant_handler
|
|
46
|
+
|
|
47
|
+
# Each callback fires only for its specific event type, while on_event
|
|
48
|
+
# callbacks fires for all events. This follows the Single
|
|
49
|
+
# Responsibility Principle and makes the code more maintainable.
|
|
50
|
+
|
|
51
|
+
def initialize(name: "MyName", system_prompt: nil, model: nil, sandbox_dir: nil)
|
|
52
|
+
@name = name
|
|
53
|
+
@system_prompt = system_prompt || config.system_prompt
|
|
54
|
+
@model = model || config.model
|
|
55
|
+
@sandbox_dir = sandbox_dir || config.sandbox_dir
|
|
56
|
+
@stdin = nil
|
|
57
|
+
@stdout = nil
|
|
58
|
+
@stderr = nil
|
|
59
|
+
@wait_thr = nil
|
|
60
|
+
@parsed_lines = []
|
|
61
|
+
@parsed_lines_mutex = Mutex.new
|
|
62
|
+
|
|
63
|
+
return unless @session_key.nil?
|
|
64
|
+
|
|
65
|
+
Time.now.utc.strftime("%Y%m%d%H%M%S")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def config
|
|
69
|
+
RubyAgent.configuration ||= RubyAgent::Configuration.new
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def connect(
|
|
73
|
+
timezone: "Eastern Time (US & Canada)",
|
|
74
|
+
skip_permissions: true,
|
|
75
|
+
verbose: true,
|
|
76
|
+
mcp_servers: nil,
|
|
77
|
+
session_key: nil,
|
|
78
|
+
resume_session: false,
|
|
79
|
+
**additional_context
|
|
80
|
+
)
|
|
81
|
+
@timezone = timezone
|
|
82
|
+
@skip_permissions = skip_permissions
|
|
83
|
+
@verbose = verbose
|
|
84
|
+
@mcp_servers = mcp_servers
|
|
85
|
+
@session_key = session_key
|
|
86
|
+
@resume_session = resume_session
|
|
87
|
+
@context = additional_context
|
|
88
|
+
@conversation_history = []
|
|
89
|
+
|
|
90
|
+
ensure_sandbox_exists
|
|
91
|
+
|
|
92
|
+
command = build_claude_command
|
|
93
|
+
|
|
94
|
+
@stdin, @stdout, @stderr, @wait_thr = spawn_process(command, @sandbox_dir)
|
|
95
|
+
|
|
96
|
+
sleep 0.5
|
|
97
|
+
unless @wait_thr.alive?
|
|
98
|
+
error_output = @stderr.read
|
|
99
|
+
raise ConnectionError, "Claude process failed to start. Error: #{error_output}"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
puts "Claude process started successfully (PID: #{@wait_thr.pid})"
|
|
103
|
+
self
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def ask(message)
|
|
107
|
+
return if message.nil? || message.strip.empty?
|
|
108
|
+
|
|
109
|
+
send_message(message)
|
|
110
|
+
read_response
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def close
|
|
114
|
+
return unless @stdin
|
|
115
|
+
|
|
116
|
+
@stdin.close unless @stdin.closed?
|
|
117
|
+
@stdout.close unless @stdout.closed?
|
|
118
|
+
@stderr.close unless @stderr.closed?
|
|
119
|
+
@wait_thr&.join
|
|
120
|
+
ensure
|
|
121
|
+
@stdin = nil
|
|
122
|
+
@stdout = nil
|
|
123
|
+
@stderr = nil
|
|
124
|
+
@wait_thr = nil
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
private
|
|
128
|
+
|
|
129
|
+
def ensure_sandbox_exists
|
|
130
|
+
return if File.directory?(@sandbox_dir)
|
|
131
|
+
|
|
132
|
+
puts "Creating sandbox directory: #{@sandbox_dir}"
|
|
133
|
+
FileUtils.mkdir_p(@sandbox_dir)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def build_claude_command
|
|
137
|
+
puts "Building Claude command..."
|
|
138
|
+
|
|
139
|
+
cmd = "claude -p --dangerously-skip-permissions --output-format=stream-json --input-format=stream-json"
|
|
140
|
+
cmd += " --verbose" if @verbose
|
|
141
|
+
cmd += " --system-prompt #{Shellwords.escape(@system_prompt)}"
|
|
142
|
+
cmd += " --model #{Shellwords.escape(@model)}"
|
|
143
|
+
|
|
144
|
+
if @mcp_servers
|
|
145
|
+
mcp_config_json = build_mcp_config(@mcp_servers).to_json
|
|
146
|
+
cmd += " --mcp-config #{Shellwords.escape(mcp_config_json)}"
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
cmd += ' --setting-sources ""'
|
|
150
|
+
cmd += " --resume #{Shellwords.escape(@session_key)}" if @resume_session && @session_key
|
|
151
|
+
cmd
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def build_mcp_config(mcp_servers)
|
|
155
|
+
servers = mcp_servers.transform_keys { |k| k.to_s.gsub("_", "-") }
|
|
156
|
+
{ mcpServers: servers }
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def spawn_process(command, sandbox_dir)
|
|
160
|
+
puts "Spawning process with command: #{command}"
|
|
161
|
+
|
|
162
|
+
command_to_run = if $stdout.tty? && File.exist?("./stream.rb")
|
|
163
|
+
"#{command} | tee >(ruby ./stream.rb >/dev/tty)"
|
|
164
|
+
else
|
|
165
|
+
command
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
stdin, stdout, stderr, wait_thr = Open3.popen3("bash", "-lc", command_to_run, chdir: sandbox_dir)
|
|
169
|
+
[stdin, stdout, stderr, wait_thr]
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def send_message(content, session_id = nil)
|
|
173
|
+
raise ConnectionError, "Not connected to Claude" unless @stdin
|
|
174
|
+
|
|
175
|
+
unless @wait_thr&.alive?
|
|
176
|
+
error_output = @stderr&.read || "Unknown error"
|
|
177
|
+
raise ConnectionError, "Claude process has died. Error: #{error_output}"
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
message_json = {
|
|
181
|
+
type: "user",
|
|
182
|
+
message: { role: "user", content: content },
|
|
183
|
+
session_id: session_id
|
|
184
|
+
}.compact
|
|
185
|
+
|
|
186
|
+
@stdin.puts JSON.generate(message_json)
|
|
187
|
+
@stdin.flush
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/BlockLength
|
|
191
|
+
def read_response
|
|
192
|
+
response = RubyAgent::Response.new
|
|
193
|
+
|
|
194
|
+
loop do
|
|
195
|
+
unless @wait_thr.alive?
|
|
196
|
+
error_output = @stderr.read
|
|
197
|
+
raise ConnectionError, "Claude process died while reading response. Error: #{error_output}"
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
ready = IO.select([@stdout, @stderr], nil, nil, 0.1)
|
|
201
|
+
|
|
202
|
+
next unless ready
|
|
203
|
+
|
|
204
|
+
if ready[0].include?(@stderr)
|
|
205
|
+
error_line = @stderr.gets
|
|
206
|
+
warn error_line if error_line
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
next unless ready[0].include?(@stdout)
|
|
210
|
+
|
|
211
|
+
line = @stdout.gets
|
|
212
|
+
break unless line
|
|
213
|
+
|
|
214
|
+
line = line.strip
|
|
215
|
+
next if line.empty?
|
|
216
|
+
|
|
217
|
+
begin
|
|
218
|
+
message = JSON.parse(line)
|
|
219
|
+
response.add_event(message)
|
|
220
|
+
|
|
221
|
+
case message["type"]
|
|
222
|
+
when "system"
|
|
223
|
+
next
|
|
224
|
+
when "assistant"
|
|
225
|
+
if message.dig("message", "content")
|
|
226
|
+
content = message["message"]["content"]
|
|
227
|
+
if content.is_a?(Array)
|
|
228
|
+
content.each do |block|
|
|
229
|
+
if block["type"] == "text" && block["text"]
|
|
230
|
+
text = block["text"]
|
|
231
|
+
response.append_text(text)
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
elsif content.is_a?(String)
|
|
235
|
+
response.append_text(content)
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
when "content_block_delta"
|
|
239
|
+
if message.dig("delta", "text")
|
|
240
|
+
text = message["delta"]["text"]
|
|
241
|
+
response.append_text(text)
|
|
242
|
+
end
|
|
243
|
+
when "result"
|
|
244
|
+
break
|
|
245
|
+
when "error"
|
|
246
|
+
puts "[ERROR] #{message['message']}"
|
|
247
|
+
break
|
|
248
|
+
end
|
|
249
|
+
run_callbacks(message)
|
|
250
|
+
rescue JSON::ParserError
|
|
251
|
+
warn "Failed to parse JSON: #{line[0..100]}"
|
|
252
|
+
next
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
response
|
|
257
|
+
end
|
|
258
|
+
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize, Metrics/BlockLength
|
|
259
|
+
end
|
|
260
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
module CallbackSupport
|
|
2
|
+
def self.included(base)
|
|
3
|
+
base.extend ClassMethods
|
|
4
|
+
end
|
|
5
|
+
|
|
6
|
+
module ClassMethods
|
|
7
|
+
def on_event(method_name = nil, &block)
|
|
8
|
+
@on_event_callbacks ||= []
|
|
9
|
+
@on_event_callbacks << (method_name || block)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def on_event_callbacks
|
|
13
|
+
callbacks = []
|
|
14
|
+
ancestors.each do |ancestor|
|
|
15
|
+
if ancestor.instance_variable_defined?(:@on_event_callbacks)
|
|
16
|
+
callbacks.concat(ancestor.instance_variable_get(:@on_event_callbacks))
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
callbacks
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def method_missing(method_name, *args, &block)
|
|
23
|
+
if method_name.to_s.start_with?("on_event_")
|
|
24
|
+
event_type = method_name.to_s.sub(/^on_event_/, "")
|
|
25
|
+
@specific_event_callbacks ||= {}
|
|
26
|
+
@specific_event_callbacks[event_type] ||= []
|
|
27
|
+
@specific_event_callbacks[event_type] << (args.first || block)
|
|
28
|
+
else
|
|
29
|
+
super
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def respond_to_missing?(method_name, include_private = false)
|
|
34
|
+
method_name.to_s.start_with?("on_event_") || super
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def specific_event_callbacks(event_type)
|
|
38
|
+
callbacks = []
|
|
39
|
+
ancestors.each do |ancestor|
|
|
40
|
+
if ancestor.instance_variable_defined?(:@specific_event_callbacks)
|
|
41
|
+
specific_callbacks = ancestor.instance_variable_get(:@specific_event_callbacks)
|
|
42
|
+
callbacks.concat(specific_callbacks[event_type]) if specific_callbacks[event_type]
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
callbacks
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def run_callbacks(event_data)
|
|
50
|
+
# Run general on_event callbacks
|
|
51
|
+
self.class.on_event_callbacks.each do |callback|
|
|
52
|
+
if callback.is_a?(Proc)
|
|
53
|
+
instance_exec(event_data, &callback)
|
|
54
|
+
else
|
|
55
|
+
send(callback, event_data)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Run event-specific callbacks
|
|
60
|
+
event_type = event_data["type"]
|
|
61
|
+
return unless event_type
|
|
62
|
+
|
|
63
|
+
self.class.specific_event_callbacks(event_type).each do |callback|
|
|
64
|
+
if callback.is_a?(Proc)
|
|
65
|
+
instance_exec(event_data, &callback)
|
|
66
|
+
else
|
|
67
|
+
send(callback, event_data)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
module RubyAgent
|
|
2
|
+
class Configuration
|
|
3
|
+
attr_accessor :anthropic_api_key, :system_prompt, :model, :sandbox_dir
|
|
4
|
+
|
|
5
|
+
def initialize
|
|
6
|
+
@anthropic_api_key = nil # Not necessarily required with Claude SDK
|
|
7
|
+
@system_prompt = "You are a helpful AI assistant."
|
|
8
|
+
@model = "claude-sonnet-4-5-20250929"
|
|
9
|
+
@sandbox_dir = "./sandbox"
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module RubyAgent
|
|
2
|
+
class Response
|
|
3
|
+
attr_reader :events, :text
|
|
4
|
+
|
|
5
|
+
def initialize(text: "", events: [])
|
|
6
|
+
@text = text
|
|
7
|
+
@events = events
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def final_text
|
|
11
|
+
@text
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def add_event(event)
|
|
15
|
+
@events << event
|
|
16
|
+
self
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def append_text(content)
|
|
20
|
+
@text += content
|
|
21
|
+
self
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
data/lib/ruby_agent/version.rb
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
VERSION = "0.2.
|
|
1
|
+
module RubyAgent
|
|
2
|
+
VERSION = "0.2.3".freeze
|
|
3
3
|
end
|
data/lib/ruby_agent.rb
CHANGED
|
@@ -1,428 +1,22 @@
|
|
|
1
|
-
|
|
1
|
+
require "dotenv/load"
|
|
2
2
|
require "shellwords"
|
|
3
3
|
require "open3"
|
|
4
|
-
require "erb"
|
|
5
4
|
require "json"
|
|
5
|
+
require "fileutils"
|
|
6
|
+
require "securerandom"
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
DEBUG = false
|
|
13
|
-
|
|
14
|
-
attr_reader :sandbox_dir, :timezone, :skip_permissions, :verbose, :system_prompt, :model, :mcp_servers
|
|
15
|
-
|
|
16
|
-
def initialize(
|
|
17
|
-
sandbox_dir: Dir.pwd,
|
|
18
|
-
timezone: "UTC",
|
|
19
|
-
skip_permissions: true,
|
|
20
|
-
verbose: false,
|
|
21
|
-
system_prompt: "You are a helpful assistant",
|
|
22
|
-
model: "claude-sonnet-4-5-20250929",
|
|
23
|
-
mcp_servers: nil,
|
|
24
|
-
session_key: nil,
|
|
25
|
-
**additional_context
|
|
26
|
-
)
|
|
27
|
-
@sandbox_dir = sandbox_dir
|
|
28
|
-
@timezone = timezone
|
|
29
|
-
@skip_permissions = skip_permissions
|
|
30
|
-
@verbose = verbose
|
|
31
|
-
@model = model
|
|
32
|
-
@mcp_servers = mcp_servers
|
|
33
|
-
@session_key = session_key
|
|
34
|
-
@system_prompt = parse_system_prompt(system_prompt, additional_context)
|
|
35
|
-
@on_message_callback = nil
|
|
36
|
-
@on_error_callback = nil
|
|
37
|
-
@dynamic_callbacks = {}
|
|
38
|
-
@custom_message_callbacks = {}
|
|
39
|
-
@stdin = nil
|
|
40
|
-
@stdout = nil
|
|
41
|
-
@stderr = nil
|
|
42
|
-
@wait_thr = nil
|
|
43
|
-
@parsed_lines = []
|
|
44
|
-
@parsed_lines_mutex = Mutex.new
|
|
45
|
-
@pending_ask_after_interrupt = nil
|
|
46
|
-
@pending_interrupt_request_id = nil
|
|
47
|
-
@deferred_exit = false
|
|
48
|
-
|
|
49
|
-
return if @session_key
|
|
50
|
-
|
|
51
|
-
inject_streaming_response({
|
|
52
|
-
type: "system",
|
|
53
|
-
subtype: "prompt",
|
|
54
|
-
system_prompt: @system_prompt,
|
|
55
|
-
timestamp: Time.now.utc.iso8601(6),
|
|
56
|
-
received_at: Time.now.utc.iso8601(6)
|
|
57
|
-
})
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def create_message_callback(name, &processor)
|
|
61
|
-
@custom_message_callbacks[name.to_s] = {
|
|
62
|
-
processor: processor,
|
|
63
|
-
callback: nil
|
|
64
|
-
}
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def on_message(&block)
|
|
68
|
-
@on_message_callback = block
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
alias on_event on_message
|
|
72
|
-
|
|
73
|
-
def on_error(&block)
|
|
74
|
-
@on_error_callback = block
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def method_missing(method_name, *args, &block)
|
|
78
|
-
if method_name.to_s.start_with?("on_") && block_given?
|
|
79
|
-
callback_name = method_name.to_s.sub(/^on_/, "")
|
|
80
|
-
|
|
81
|
-
if @custom_message_callbacks.key?(callback_name)
|
|
82
|
-
@custom_message_callbacks[callback_name][:callback] = block
|
|
83
|
-
else
|
|
84
|
-
@dynamic_callbacks[callback_name] = block
|
|
85
|
-
end
|
|
86
|
-
else
|
|
87
|
-
super
|
|
88
|
-
end
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
def respond_to_missing?(method_name, include_private = false)
|
|
92
|
-
method_name.to_s.start_with?("on_") || super
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
def connect(&block)
|
|
96
|
-
command = build_claude_command
|
|
97
|
-
|
|
98
|
-
spawn_process(command, @sandbox_dir) do |stdin, stdout, stderr, wait_thr|
|
|
99
|
-
@stdin = stdin
|
|
100
|
-
@stdout = stdout
|
|
101
|
-
@stderr = stderr
|
|
102
|
-
@wait_thr = wait_thr
|
|
103
|
-
|
|
104
|
-
begin
|
|
105
|
-
block.call if block_given?
|
|
106
|
-
receive_streaming_responses
|
|
107
|
-
ensure
|
|
108
|
-
@stdin = nil
|
|
109
|
-
@stdout = nil
|
|
110
|
-
@stderr = nil
|
|
111
|
-
@wait_thr = nil
|
|
112
|
-
end
|
|
113
|
-
end
|
|
114
|
-
rescue StandardError => e
|
|
115
|
-
trigger_error(e)
|
|
116
|
-
raise
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
def ask(text, sender_name: "User", additional: [])
|
|
120
|
-
formatted_text = if sender_name.downcase == "system"
|
|
121
|
-
<<~TEXT.strip
|
|
122
|
-
<system>
|
|
123
|
-
#{text}
|
|
124
|
-
</system>
|
|
125
|
-
TEXT
|
|
126
|
-
else
|
|
127
|
-
"#{sender_name}: #{text}"
|
|
128
|
-
end
|
|
129
|
-
formatted_text += extra_context(additional, sender_name:)
|
|
130
|
-
|
|
131
|
-
inject_streaming_response({
|
|
132
|
-
type: "user",
|
|
133
|
-
subtype: "new_message",
|
|
134
|
-
sender_name:,
|
|
135
|
-
text:,
|
|
136
|
-
formatted_text:,
|
|
137
|
-
timestamp: Time.now.utc.iso8601(6)
|
|
138
|
-
})
|
|
139
|
-
|
|
140
|
-
send_message(formatted_text)
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
def ask_after_interrupt(text, sender_name: "User", additional: [])
|
|
144
|
-
@pending_ask_after_interrupt = { text:, sender_name:, additional: }
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
def send_system_message(text)
|
|
148
|
-
ask(text, sender_name: "system")
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
def receive_streaming_responses
|
|
152
|
-
@stdout.each_line do |line|
|
|
153
|
-
next if line.strip.empty?
|
|
154
|
-
|
|
155
|
-
begin
|
|
156
|
-
json = JSON.parse(line)
|
|
157
|
-
|
|
158
|
-
all_lines = nil
|
|
159
|
-
@parsed_lines_mutex.synchronize do
|
|
160
|
-
@parsed_lines << json
|
|
161
|
-
all_lines = @parsed_lines.dup
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
trigger_message(json, all_lines)
|
|
165
|
-
trigger_dynamic_callbacks(json, all_lines)
|
|
166
|
-
trigger_custom_message_callbacks(json, all_lines)
|
|
167
|
-
rescue JSON::ParserError
|
|
168
|
-
warn "Failed to parse line: #{line}" if DEBUG
|
|
169
|
-
end
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
puts "→ stdout closed, waiting for process to exit..." if DEBUG
|
|
173
|
-
exit_status = @wait_thr.value
|
|
174
|
-
puts "→ Process exited with status: #{exit_status.success? ? 'success' : 'failure'}" if DEBUG
|
|
175
|
-
unless exit_status.success?
|
|
176
|
-
stderr_output = @stderr.read
|
|
177
|
-
raise ConnectionError, "Claude command failed: #{stderr_output}"
|
|
178
|
-
end
|
|
179
|
-
|
|
180
|
-
@parsed_lines
|
|
181
|
-
end
|
|
182
|
-
|
|
183
|
-
def inject_streaming_response(event_hash)
|
|
184
|
-
stringified_event = stringify_keys(event_hash)
|
|
185
|
-
all_lines = nil
|
|
186
|
-
@parsed_lines_mutex.synchronize do
|
|
187
|
-
@parsed_lines << stringified_event
|
|
188
|
-
all_lines = @parsed_lines.dup
|
|
189
|
-
end
|
|
190
|
-
|
|
191
|
-
trigger_message(stringified_event, all_lines)
|
|
192
|
-
trigger_dynamic_callbacks(stringified_event, all_lines)
|
|
193
|
-
trigger_custom_message_callbacks(stringified_event, all_lines)
|
|
194
|
-
end
|
|
195
|
-
|
|
196
|
-
def interrupt
|
|
197
|
-
raise ConnectionError, "Not connected to Claude" unless @stdin
|
|
198
|
-
raise ConnectionError, "Cannot interrupt - stdin is closed" if @stdin.closed?
|
|
199
|
-
|
|
200
|
-
@request_counter ||= 0
|
|
201
|
-
@request_counter += 1
|
|
202
|
-
request_id = "req_#{@request_counter}_#{SecureRandom.hex(4)}"
|
|
203
|
-
|
|
204
|
-
@pending_interrupt_request_id = request_id if @pending_ask_after_interrupt
|
|
205
|
-
if DEBUG
|
|
206
|
-
puts "→ Sending interrupt with request_id: #{request_id}, pending_ask: #{@pending_ask_after_interrupt ? true : false}"
|
|
207
|
-
end
|
|
208
|
-
|
|
209
|
-
control_request = {
|
|
210
|
-
type: "control_request",
|
|
211
|
-
request_id: request_id,
|
|
212
|
-
request: {
|
|
213
|
-
subtype: "interrupt"
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
inject_streaming_response({
|
|
218
|
-
type: "control",
|
|
219
|
-
subtype: "interrupt",
|
|
220
|
-
timestamp: Time.now.utc.iso8601(6)
|
|
221
|
-
})
|
|
222
|
-
|
|
223
|
-
@stdin.puts JSON.generate(control_request)
|
|
224
|
-
@stdin.flush
|
|
225
|
-
rescue StandardError => e
|
|
226
|
-
warn "Failed to send interrupt signal: #{e.message}"
|
|
227
|
-
raise
|
|
228
|
-
end
|
|
229
|
-
|
|
230
|
-
def exit
|
|
231
|
-
return unless @stdin
|
|
232
|
-
|
|
233
|
-
if @pending_interrupt_request_id
|
|
234
|
-
puts "→ Deferring exit - waiting for interrupt response (request_id: #{@pending_interrupt_request_id})" if DEBUG
|
|
235
|
-
@deferred_exit = true
|
|
236
|
-
return
|
|
237
|
-
end
|
|
238
|
-
|
|
239
|
-
puts "→ Exiting Claude (closing stdin)" if DEBUG
|
|
240
|
-
|
|
241
|
-
begin
|
|
242
|
-
@stdin.close unless @stdin.closed?
|
|
243
|
-
puts "→ stdin closed" if DEBUG
|
|
244
|
-
rescue StandardError => e
|
|
245
|
-
warn "Error closing stdin during exit: #{e.message}"
|
|
246
|
-
end
|
|
247
|
-
end
|
|
248
|
-
|
|
249
|
-
private
|
|
250
|
-
|
|
251
|
-
def spawn_process(command, sandbox_dir, &)
|
|
252
|
-
Open3.popen3("bash", "-lc", command, chdir: sandbox_dir, &)
|
|
253
|
-
end
|
|
254
|
-
|
|
255
|
-
def build_claude_command
|
|
256
|
-
cmd = "claude -p --dangerously-skip-permissions --output-format=stream-json --input-format=stream-json --verbose"
|
|
257
|
-
cmd += " --system-prompt #{Shellwords.escape(@system_prompt)}"
|
|
258
|
-
cmd += " --model #{Shellwords.escape(@model)}"
|
|
259
|
-
|
|
260
|
-
if @mcp_servers
|
|
261
|
-
mcp_config = build_mcp_config(@mcp_servers)
|
|
262
|
-
cmd += " --mcp-config #{Shellwords.escape(mcp_config.to_json)}"
|
|
263
|
-
end
|
|
264
|
-
|
|
265
|
-
cmd += " --setting-sources \"\""
|
|
266
|
-
cmd += " --resume #{Shellwords.escape(@session_key)}" if @session_key
|
|
267
|
-
cmd
|
|
268
|
-
end
|
|
269
|
-
|
|
270
|
-
def build_mcp_config(mcp_servers)
|
|
271
|
-
servers = mcp_servers.transform_keys { |k| k.to_s.gsub("_", "-") }
|
|
272
|
-
{ mcpServers: servers }
|
|
273
|
-
end
|
|
274
|
-
|
|
275
|
-
def parse_system_prompt(template_content, context_vars)
|
|
276
|
-
if Dir.exist?(@sandbox_dir)
|
|
277
|
-
Dir.chdir(@sandbox_dir) do
|
|
278
|
-
parse_system_prompt_in_context(template_content, context_vars)
|
|
279
|
-
end
|
|
280
|
-
else
|
|
281
|
-
parse_system_prompt_in_context(template_content, context_vars)
|
|
282
|
-
end
|
|
283
|
-
end
|
|
284
|
-
|
|
285
|
-
def parse_system_prompt_in_context(template_content, context_vars)
|
|
286
|
-
erb = ERB.new(template_content)
|
|
287
|
-
binding_context = create_binding_context(**context_vars)
|
|
288
|
-
result = erb.result(binding_context)
|
|
289
|
-
|
|
290
|
-
raise ParseError, "There was an error parsing the system prompt." if result.include?("<%=") || result.include?("%>")
|
|
291
|
-
|
|
292
|
-
result
|
|
293
|
-
end
|
|
294
|
-
|
|
295
|
-
def create_binding_context(**vars)
|
|
296
|
-
context = Object.new
|
|
297
|
-
vars.each do |key, value|
|
|
298
|
-
context.instance_variable_set("@#{key}", value)
|
|
299
|
-
context.define_singleton_method(key) { instance_variable_get("@#{key}") }
|
|
300
|
-
end
|
|
301
|
-
context.instance_eval { binding }
|
|
302
|
-
end
|
|
303
|
-
|
|
304
|
-
def extra_context(additional = [], sender_name:)
|
|
305
|
-
raise "additional is not an array" unless additional.is_a?(Array)
|
|
306
|
-
|
|
307
|
-
return "" if additional.empty?
|
|
308
|
-
|
|
309
|
-
<<~CONTEXT
|
|
310
|
-
|
|
311
|
-
<extra-context>
|
|
312
|
-
#{additional.join("\n\n")}
|
|
313
|
-
</extra-context>
|
|
314
|
-
CONTEXT
|
|
315
|
-
end
|
|
316
|
-
|
|
317
|
-
def send_message(content, session_id = nil)
|
|
318
|
-
raise ConnectionError, "Not connected to Claude" unless @stdin
|
|
319
|
-
|
|
320
|
-
message_json = {
|
|
321
|
-
type: "user",
|
|
322
|
-
message: { role: "user", content: content },
|
|
323
|
-
session_id: session_id
|
|
324
|
-
}.compact
|
|
325
|
-
|
|
326
|
-
@stdin.puts JSON.generate(message_json)
|
|
327
|
-
@stdin.flush
|
|
328
|
-
rescue StandardError => e
|
|
329
|
-
trigger_error(e)
|
|
330
|
-
raise
|
|
331
|
-
end
|
|
332
|
-
|
|
333
|
-
def trigger_message(message, all_messages)
|
|
334
|
-
@on_message_callback&.call(message, all_messages)
|
|
335
|
-
end
|
|
336
|
-
|
|
337
|
-
def trigger_dynamic_callbacks(message, all_messages)
|
|
338
|
-
type = message["type"]
|
|
339
|
-
subtype = message["subtype"]
|
|
340
|
-
|
|
341
|
-
return unless type
|
|
342
|
-
|
|
343
|
-
if type == "control_response"
|
|
344
|
-
puts "→ Received control_response: #{message.inspect}" if DEBUG || @pending_interrupt_request_id
|
|
345
|
-
if @pending_interrupt_request_id
|
|
346
|
-
response = message["response"]
|
|
347
|
-
if response&.dig("subtype") == "success" && response&.dig("request_id") == @pending_interrupt_request_id
|
|
348
|
-
puts "→ Interrupt confirmed, executing queued ask" if DEBUG
|
|
349
|
-
@pending_interrupt_request_id = nil
|
|
350
|
-
if @pending_ask_after_interrupt
|
|
351
|
-
pending = @pending_ask_after_interrupt
|
|
352
|
-
@pending_ask_after_interrupt = nil
|
|
353
|
-
begin
|
|
354
|
-
ask(pending[:text], sender_name: pending[:sender_name], additional: pending[:additional])
|
|
355
|
-
rescue IOError, Errno::EPIPE => e
|
|
356
|
-
warn "Failed to send queued ask after interrupt (stream closed): #{e.message}"
|
|
357
|
-
end
|
|
358
|
-
end
|
|
359
|
-
|
|
360
|
-
if @deferred_exit
|
|
361
|
-
puts "→ Executing deferred exit" if DEBUG
|
|
362
|
-
@deferred_exit = false
|
|
363
|
-
exit
|
|
364
|
-
end
|
|
365
|
-
elsif DEBUG
|
|
366
|
-
puts "→ Control response didn't match pending interrupt: #{response.inspect}"
|
|
367
|
-
end
|
|
368
|
-
end
|
|
369
|
-
end
|
|
370
|
-
|
|
371
|
-
if subtype
|
|
372
|
-
specific_callback_key = "#{type}_#{subtype}"
|
|
373
|
-
specific_callback = @dynamic_callbacks[specific_callback_key]
|
|
374
|
-
if specific_callback
|
|
375
|
-
puts "→ Triggering callback for: #{specific_callback_key}" if DEBUG
|
|
376
|
-
specific_callback.call(message, all_messages)
|
|
377
|
-
end
|
|
378
|
-
end
|
|
379
|
-
|
|
380
|
-
general_callback = @dynamic_callbacks[type]
|
|
381
|
-
if general_callback
|
|
382
|
-
puts "→ Triggering callback for: #{type}" if DEBUG
|
|
383
|
-
general_callback.call(message, all_messages)
|
|
384
|
-
end
|
|
385
|
-
|
|
386
|
-
check_nested_content_types(message, all_messages)
|
|
387
|
-
end
|
|
388
|
-
|
|
389
|
-
def check_nested_content_types(message, all_messages)
|
|
390
|
-
return unless message["message"].is_a?(Hash)
|
|
391
|
-
|
|
392
|
-
content = message.dig("message", "content")
|
|
393
|
-
return unless content.is_a?(Array)
|
|
394
|
-
|
|
395
|
-
content.each do |content_item|
|
|
396
|
-
next unless content_item.is_a?(Hash)
|
|
397
|
-
|
|
398
|
-
nested_type = content_item["type"]
|
|
399
|
-
next unless nested_type
|
|
400
|
-
|
|
401
|
-
callback = @dynamic_callbacks[nested_type]
|
|
402
|
-
if callback
|
|
403
|
-
puts "→ Triggering callback for nested type: #{nested_type}" if DEBUG
|
|
404
|
-
callback.call(message, all_messages)
|
|
405
|
-
end
|
|
406
|
-
end
|
|
407
|
-
end
|
|
408
|
-
|
|
409
|
-
def trigger_custom_message_callbacks(message, all_messages)
|
|
410
|
-
@custom_message_callbacks.each_value do |config|
|
|
411
|
-
processor = config[:processor]
|
|
412
|
-
callback = config[:callback]
|
|
413
|
-
|
|
414
|
-
next unless processor && callback
|
|
415
|
-
|
|
416
|
-
result = processor.call(message, all_messages)
|
|
417
|
-
callback.call(result) if result && !result.to_s.empty?
|
|
418
|
-
end
|
|
419
|
-
end
|
|
8
|
+
require_relative "ruby_agent/version"
|
|
9
|
+
require_relative "ruby_agent/configuration"
|
|
10
|
+
require_relative "ruby_agent/agent"
|
|
11
|
+
require_relative "ruby_agent/callback_support"
|
|
420
12
|
|
|
421
|
-
|
|
422
|
-
|
|
13
|
+
module RubyAgent
|
|
14
|
+
class << self
|
|
15
|
+
attr_accessor :configuration
|
|
423
16
|
end
|
|
424
17
|
|
|
425
|
-
def
|
|
426
|
-
|
|
18
|
+
def self.configure
|
|
19
|
+
self.configuration ||= Configuration.new
|
|
20
|
+
yield(configuration)
|
|
427
21
|
end
|
|
428
22
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ruby_agent
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Keith Schacht
|
|
@@ -22,7 +22,13 @@ files:
|
|
|
22
22
|
- LICENSE
|
|
23
23
|
- README.md
|
|
24
24
|
- Rakefile
|
|
25
|
+
- examples/example1.rb
|
|
25
26
|
- lib/ruby_agent.rb
|
|
27
|
+
- lib/ruby_agent/agent.rb
|
|
28
|
+
- lib/ruby_agent/callback_support.rb
|
|
29
|
+
- lib/ruby_agent/configuration.rb
|
|
30
|
+
- lib/ruby_agent/event.rb
|
|
31
|
+
- lib/ruby_agent/response.rb
|
|
26
32
|
- lib/ruby_agent/version.rb
|
|
27
33
|
homepage: https://github.com/AllYourBot/ruby-agent
|
|
28
34
|
licenses:
|