rsmolagent 0.1.0 → 0.2.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.
- checksums.yaml +4 -4
- data/lib/rsmolagent/agent.rb +12 -0
- data/lib/rsmolagent/llm_provider.rb +227 -5
- data/lib/rsmolagent/version.rb +1 -1
- metadata +18 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d497306b05c55fd6a5fc261dc5fe129c0f009246ae40c9c35ed43778fffa5e18
|
4
|
+
data.tar.gz: ac8ec9d9befe6cd78a2708bb10e0aa42d38777a5e93926ed2690c5dd024547b3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1c811483465d931daa25a388c1876f2eb9b67d636d09b7eb5f80023af1171649ae6c2cd9f27faa9133736a94b1f165ca957464498bd857d359980cbe95e924c9
|
7
|
+
data.tar.gz: 875681cd6a3fbecb28bf3ec5e06fdcb82bb91f4e09e8a6baa3f2f3a97efa6175d5b2d12bb72aad9a69c7c2e09dfc7d2da0211047ecaf3eb8ee491441cb3e57b4
|
data/lib/rsmolagent/agent.rb
CHANGED
@@ -91,6 +91,18 @@ module RSmolagent
|
|
91
91
|
answer = arguments["answer"] || arguments[:answer] || ""
|
92
92
|
@memory.add_final_answer(answer)
|
93
93
|
return { final_answer: true, answer: answer }
|
94
|
+
elsif tool_name == "answer"
|
95
|
+
# Handle answer tool as final answer too
|
96
|
+
result = arguments["result"] || arguments[:result] || ""
|
97
|
+
explanation = arguments["explanation"] || arguments[:explanation] || ""
|
98
|
+
|
99
|
+
final_answer = result
|
100
|
+
if explanation && !explanation.empty?
|
101
|
+
final_answer += " (#{explanation})"
|
102
|
+
end
|
103
|
+
|
104
|
+
@memory.add_final_answer(final_answer)
|
105
|
+
return { final_answer: true, answer: final_answer }
|
94
106
|
else
|
95
107
|
# Execute the tool
|
96
108
|
result = execute_tool(tool_name, arguments)
|
@@ -32,6 +32,8 @@ module RSmolagent
|
|
32
32
|
def initialize(model_id:, client:, **options)
|
33
33
|
super(model_id: model_id, **options)
|
34
34
|
@client = client
|
35
|
+
@max_retries = options[:max_retries] || 3
|
36
|
+
@initial_backoff = options[:initial_backoff] || 1
|
35
37
|
end
|
36
38
|
|
37
39
|
def chat(messages, tools: nil, tool_choice: nil)
|
@@ -47,13 +49,233 @@ module RSmolagent
|
|
47
49
|
params[:tool_choice] = tool_choice || "auto"
|
48
50
|
end
|
49
51
|
|
50
|
-
|
52
|
+
retries = 0
|
53
|
+
begin
|
54
|
+
response = @client.chat(parameters: params)
|
55
|
+
|
56
|
+
# Update token counts
|
57
|
+
@last_prompt_tokens = response.usage.prompt_tokens
|
58
|
+
@last_completion_tokens = response.usage.completion_tokens
|
59
|
+
|
60
|
+
response.choices.first.message
|
61
|
+
rescue Faraday::TooManyRequestsError => e
|
62
|
+
retries += 1
|
63
|
+
if retries <= @max_retries
|
64
|
+
# Exponential backoff with jitter
|
65
|
+
sleep_time = @initial_backoff * (2 ** (retries - 1)) * (0.5 + rand)
|
66
|
+
puts "Rate limited (429). Retrying in #{sleep_time.round(1)} seconds... (#{retries}/#{@max_retries})"
|
67
|
+
sleep(sleep_time)
|
68
|
+
retry
|
69
|
+
else
|
70
|
+
puts "Max retries (#{@max_retries}) exceeded. Rate limit error persists."
|
71
|
+
raise e
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def extract_tool_calls(response)
|
77
|
+
return [] unless response.tool_calls
|
78
|
+
|
79
|
+
response.tool_calls.map do |tool_call|
|
80
|
+
{
|
81
|
+
name: tool_call.function.name,
|
82
|
+
arguments: parse_json_if_needed(tool_call.function.arguments)
|
83
|
+
}
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
class ClaudeProvider < LLMProvider
|
89
|
+
def initialize(model_id:, client:, **options)
|
90
|
+
super(model_id: model_id, **options)
|
91
|
+
@client = client
|
92
|
+
@max_retries = options[:max_retries] || 3
|
93
|
+
@initial_backoff = options[:initial_backoff] || 1
|
94
|
+
end
|
95
|
+
|
96
|
+
def chat(messages, tools: nil, tool_choice: nil)
|
97
|
+
# Prepare the system message
|
98
|
+
system_message = nil
|
99
|
+
filtered_messages = messages.reject do |message|
|
100
|
+
if message[:role] == 'system'
|
101
|
+
system_message = message[:content]
|
102
|
+
true
|
103
|
+
else
|
104
|
+
false
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# Convert messages format for Claude
|
109
|
+
claude_messages = filtered_messages.map do |msg|
|
110
|
+
role = msg[:role] == 'assistant' ? 'assistant' : 'user'
|
111
|
+
|
112
|
+
# Process assistant messages to help guide Claude's behavior
|
113
|
+
content = msg[:content]
|
114
|
+
if role == 'assistant' && content.start_with?("I'll use the calculator tool")
|
115
|
+
content = "I'll use the calculator tool to solve this problem."
|
116
|
+
end
|
117
|
+
|
118
|
+
{ role: role, content: content }
|
119
|
+
end
|
120
|
+
|
121
|
+
# Check current state of conversation to guide the model
|
122
|
+
calculator_used = false
|
123
|
+
calculator_result = nil
|
124
|
+
|
125
|
+
# Analyze the conversation to detect patterns
|
126
|
+
claude_messages.each_with_index do |msg, i|
|
127
|
+
# Check for calculator result
|
128
|
+
if msg[:role] == 'user' && msg[:content].match?(/^\d+$/) && i > 0
|
129
|
+
calculator_used = true
|
130
|
+
calculator_result = msg[:content]
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
# Prepare parameters
|
135
|
+
params = {
|
136
|
+
model: @model_id,
|
137
|
+
messages: claude_messages,
|
138
|
+
temperature: @options[:temperature] || 0.7,
|
139
|
+
max_tokens: @options[:max_tokens] || 4096
|
140
|
+
}
|
141
|
+
|
142
|
+
# Add system message with appropriate guidance based on conversation state
|
143
|
+
if system_message
|
144
|
+
enhanced_system = system_message.dup
|
145
|
+
if calculator_used && calculator_result
|
146
|
+
enhanced_system += " IMPORTANT: You have already calculated the result (#{calculator_result}). Use the answer tool now to provide your final answer."
|
147
|
+
end
|
148
|
+
params[:system] = enhanced_system
|
149
|
+
end
|
150
|
+
|
151
|
+
# Add tools if provided - Format specifically for Claude's API
|
152
|
+
if tools && !tools.empty?
|
153
|
+
claude_tools = tools.map do |tool|
|
154
|
+
schema = tool.to_json_schema
|
155
|
+
{
|
156
|
+
name: schema["name"],
|
157
|
+
description: schema["description"],
|
158
|
+
input_schema: {
|
159
|
+
type: "object",
|
160
|
+
properties: schema["parameters"]["properties"],
|
161
|
+
required: schema["parameters"]["required"]
|
162
|
+
}
|
163
|
+
}
|
164
|
+
end
|
165
|
+
params[:tools] = claude_tools
|
166
|
+
end
|
167
|
+
|
168
|
+
# Debug information
|
169
|
+
puts "CLAUDE REQUEST PARAMS:"
|
170
|
+
puts "Model: #{params[:model]}"
|
171
|
+
puts "Messages: #{params[:messages].inspect}"
|
172
|
+
puts "System: #{params[:system]}" if params[:system]
|
173
|
+
puts "Tools: #{params[:tools].inspect}" if params[:tools]
|
174
|
+
|
175
|
+
retries = 0
|
176
|
+
begin
|
177
|
+
response = @client.messages(parameters: params)
|
178
|
+
|
179
|
+
# Update token counts if available
|
180
|
+
if response.respond_to?(:usage)
|
181
|
+
@last_prompt_tokens = response.usage.input_tokens if response.usage.respond_to?(:input_tokens)
|
182
|
+
@last_completion_tokens = response.usage.output_tokens if response.usage.respond_to?(:output_tokens)
|
183
|
+
end
|
184
|
+
|
185
|
+
# Create a response object that matches the structure expected by Agent
|
186
|
+
content = nil
|
187
|
+
|
188
|
+
# Handle hash response
|
189
|
+
if response.is_a?(Hash) && response["content"].is_a?(Array)
|
190
|
+
text_item = response["content"].find { |item| item["type"] == "text" }
|
191
|
+
content = text_item["text"] if text_item
|
192
|
+
# Handle object response
|
193
|
+
elsif response.respond_to?(:content) && response.content.is_a?(Array) && !response.content.empty?
|
194
|
+
content_item = response.content.find { |item| item.type == 'text' }
|
195
|
+
content = content_item ? content_item.text : nil
|
196
|
+
end
|
197
|
+
|
198
|
+
OpenStruct.new(
|
199
|
+
content: content,
|
200
|
+
tool_calls: extract_tool_calls_from_response(response)
|
201
|
+
)
|
202
|
+
rescue => e
|
203
|
+
if e.is_a?(Faraday::TooManyRequestsError) || e.message.include?('429')
|
204
|
+
retries += 1
|
205
|
+
if retries <= @max_retries
|
206
|
+
# Exponential backoff with jitter
|
207
|
+
sleep_time = @initial_backoff * (2 ** (retries - 1)) * (0.5 + rand)
|
208
|
+
puts "Rate limited (429). Retrying in #{sleep_time.round(1)} seconds... (#{retries}/#{@max_retries})"
|
209
|
+
sleep(sleep_time)
|
210
|
+
retry
|
211
|
+
else
|
212
|
+
puts "Max retries (#{@max_retries}) exceeded. Rate limit error persists."
|
213
|
+
raise e
|
214
|
+
end
|
215
|
+
elsif e.is_a?(Faraday::BadRequestError) || e.message.include?('400')
|
216
|
+
puts "Bad request error (400). Request parameters may be invalid:"
|
217
|
+
puts "Error details: #{e.message}"
|
218
|
+
puts "Response body: #{e.response[:body] if e.respond_to?(:response) && e.response.is_a?(Hash)}"
|
219
|
+
raise e
|
220
|
+
else
|
221
|
+
puts "Unknown error occurred: #{e.class} - #{e.message}"
|
222
|
+
raise e
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
def extract_tool_calls_from_response(response)
|
228
|
+
# Debug information about the response
|
229
|
+
puts "Response type: #{response.class}"
|
230
|
+
puts "Response content: #{response.inspect}"
|
51
231
|
|
52
|
-
#
|
53
|
-
|
54
|
-
|
232
|
+
# Check if response is a hash (the anthropic gem might return raw hash)
|
233
|
+
if response.is_a?(Hash)
|
234
|
+
puts "Response is a Hash, extracting from hash"
|
235
|
+
content_array = response["content"] rescue nil
|
236
|
+
return nil unless content_array && content_array.is_a?(Array)
|
237
|
+
|
238
|
+
tool_calls_content = content_array.find { |c| c["type"] == "tool_use" }
|
239
|
+
return nil unless tool_calls_content
|
240
|
+
|
241
|
+
tool_call = tool_calls_content["id"] ? {
|
242
|
+
"name" => tool_calls_content["name"],
|
243
|
+
"input" => tool_calls_content["input"]
|
244
|
+
} : nil
|
245
|
+
|
246
|
+
return nil unless tool_call
|
247
|
+
|
248
|
+
return [
|
249
|
+
OpenStruct.new(
|
250
|
+
function: OpenStruct.new(
|
251
|
+
name: tool_call["name"],
|
252
|
+
arguments: tool_call["input"].to_json
|
253
|
+
)
|
254
|
+
)
|
255
|
+
]
|
256
|
+
end
|
55
257
|
|
56
|
-
response
|
258
|
+
# Handle object-style response (if anthropic gem parses to objects)
|
259
|
+
begin
|
260
|
+
return nil unless response.respond_to?(:content) &&
|
261
|
+
response.content.is_a?(Array) &&
|
262
|
+
!response.content.empty?
|
263
|
+
|
264
|
+
tool_use_blocks = response.content.select { |c| c.type == "tool_use" }
|
265
|
+
return nil if tool_use_blocks.empty?
|
266
|
+
|
267
|
+
return tool_use_blocks.map do |tool_block|
|
268
|
+
OpenStruct.new(
|
269
|
+
function: OpenStruct.new(
|
270
|
+
name: tool_block.name,
|
271
|
+
arguments: tool_block.input.to_json
|
272
|
+
)
|
273
|
+
)
|
274
|
+
end
|
275
|
+
rescue => e
|
276
|
+
puts "Error extracting tool calls: #{e.message}"
|
277
|
+
nil
|
278
|
+
end
|
57
279
|
end
|
58
280
|
|
59
281
|
def extract_tool_calls(response)
|
data/lib/rsmolagent/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rsmolagent
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- gkosmo
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-03-
|
11
|
+
date: 2025-03-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: json
|
@@ -38,6 +38,20 @@ dependencies:
|
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '6.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: anthropic
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 0.3.0
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 0.3.0
|
41
55
|
- !ruby/object:Gem::Dependency
|
42
56
|
name: rspec
|
43
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -53,7 +67,8 @@ dependencies:
|
|
53
67
|
- !ruby/object:Gem::Version
|
54
68
|
version: '3.0'
|
55
69
|
description: RSmolagent is a Ruby library for building AI agents that can use tools
|
56
|
-
to solve tasks
|
70
|
+
to solve tasks. Supports both OpenAI and Claude models with automatic retries and
|
71
|
+
rate limiting capabilities.
|
57
72
|
email:
|
58
73
|
- gkosmo1@hotmail.com
|
59
74
|
executables: []
|