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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 374e7d1da7f59eece14d7616f9e21cea9f99290a6ccf4eac4697576972f8e260
4
- data.tar.gz: a2892e99d70020084fe207dc58b042be5886fcd55f19dc371bf97fd1602fe169
3
+ metadata.gz: d497306b05c55fd6a5fc261dc5fe129c0f009246ae40c9c35ed43778fffa5e18
4
+ data.tar.gz: ac8ec9d9befe6cd78a2708bb10e0aa42d38777a5e93926ed2690c5dd024547b3
5
5
  SHA512:
6
- metadata.gz: 63248253961036a8734c03e1d4201fa259896cc8f015d40a8478518e7e3d1ed6831f5afa512de6d3cfaaebdfc9fa728177d796f360285189dc0f19d805696d57
7
- data.tar.gz: 1a75e4e82b4c9479e76e16df77fbe55f6ba96ec6bcf06a3146dd268a0aef1bba8e651e9277f8268c7a1998611670d415329d898328903ab7d8890d305b9be8bb
6
+ metadata.gz: 1c811483465d931daa25a388c1876f2eb9b67d636d09b7eb5f80023af1171649ae6c2cd9f27faa9133736a94b1f165ca957464498bd857d359980cbe95e924c9
7
+ data.tar.gz: 875681cd6a3fbecb28bf3ec5e06fdcb82bb91f4e09e8a6baa3f2f3a97efa6175d5b2d12bb72aad9a69c7c2e09dfc7d2da0211047ecaf3eb8ee491441cb3e57b4
@@ -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
- response = @client.chat(parameters: params)
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
- # Update token counts
53
- @last_prompt_tokens = response.usage.prompt_tokens
54
- @last_completion_tokens = response.usage.completion_tokens
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.choices.first.message
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)
@@ -1,3 +1,3 @@
1
1
  module RSmolagent
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
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.1.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-10 00:00:00.000000000 Z
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: []