rsmolagent 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 374e7d1da7f59eece14d7616f9e21cea9f99290a6ccf4eac4697576972f8e260
4
+ data.tar.gz: a2892e99d70020084fe207dc58b042be5886fcd55f19dc371bf97fd1602fe169
5
+ SHA512:
6
+ metadata.gz: 63248253961036a8734c03e1d4201fa259896cc8f015d40a8478518e7e3d1ed6831f5afa512de6d3cfaaebdfc9fa728177d796f360285189dc0f19d805696d57
7
+ data.tar.gz: 1a75e4e82b4c9479e76e16df77fbe55f6ba96ec6bcf06a3146dd268a0aef1bba8e651e9277f8268c7a1998611670d415329d898328903ab7d8890d305b9be8bb
data/README.md ADDED
@@ -0,0 +1,254 @@
1
+ # RSmolagent
2
+
3
+ A lightweight Ruby library for creating AI agents that can use tools to solve tasks. Inspired by Python's smolagents library, RSmolagent provides a simple way to build agents that can interact with the world through tools.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'rsmolagent'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ ```bash
16
+ $ bundle install
17
+ ```
18
+
19
+ Or install it yourself as:
20
+
21
+ ```bash
22
+ $ gem install rsmolagent
23
+ ```
24
+
25
+ ## Usage
26
+
27
+ RSmolagent is designed to be simple to use. Here's a basic example:
28
+
29
+ ```ruby
30
+ require 'rsmolagent'
31
+ require 'ruby/openai'
32
+
33
+ # Create a custom tool
34
+ class CalculatorTool < RSmolagent::Tool
35
+ def initialize
36
+ super(
37
+ name: "calculator",
38
+ description: "Perform mathematical calculations",
39
+ input_schema: {
40
+ expression: {
41
+ type: "string",
42
+ description: "The mathematical expression to evaluate"
43
+ }
44
+ }
45
+ )
46
+ end
47
+
48
+ def execute(args)
49
+ expression = args[:expression]
50
+ eval(expression).to_s
51
+ rescue => e
52
+ "Error: #{e.message}"
53
+ end
54
+ end
55
+
56
+ # Initialize OpenAI client
57
+ client = OpenAI::Client.new(access_token: ENV["OPENAI_API_KEY"])
58
+
59
+ # Create LLM provider
60
+ llm = RSmolagent::OpenAIProvider.new(
61
+ model_id: "gpt-3.5-turbo",
62
+ client: client
63
+ )
64
+
65
+ # Create agent with tools
66
+ calculator = CalculatorTool.new
67
+ agent = RSmolagent::Agent.new(
68
+ llm_provider: llm,
69
+ tools: [calculator]
70
+ )
71
+
72
+ # Run the agent
73
+ result = agent.run("What is 123 * 456?")
74
+ puts result # Outputs the calculated result
75
+ ```
76
+
77
+ ## Features
78
+
79
+ - Simple interface for creating AI agents
80
+ - Support for custom tools
81
+ - Built-in memory for tracking conversation history
82
+ - Works with OpenAI API (easily extensible to other LLM providers)
83
+ - Automatic handling of tool calls and responses
84
+ - Built-in tools including web search
85
+ - Dynamic code execution and custom tool creation
86
+ - Ability to create tools from Ruby code at runtime
87
+
88
+ ## Built-in Tools
89
+
90
+ RSmolagent comes with several built-in tools:
91
+
92
+ ### WebSearchTool
93
+
94
+ Searches the web using DuckDuckGo's API:
95
+
96
+ ```ruby
97
+ # Create a web search tool
98
+ web_search = RSmolagent::Tools::WebSearchTool.new(max_results: 3)
99
+
100
+ # Add it to your agent
101
+ agent = RSmolagent::Agent.new(
102
+ llm_provider: llm,
103
+ tools: [web_search]
104
+ )
105
+
106
+ # The agent can now search the web
107
+ result = agent.run("What are the latest developments in AI?")
108
+ ```
109
+
110
+ ### Ruby Code Execution Tools
111
+
112
+ RSmolagent provides tools for executing Ruby code, allowing you to create custom tools dynamically:
113
+
114
+ #### 1. RubyExecutorTool
115
+
116
+ Executes arbitrary Ruby code in a controlled environment:
117
+
118
+ ```ruby
119
+ # Create a Ruby executor tool
120
+ ruby_executor = RSmolagent::Tools::RubyExecutorTool.new
121
+
122
+ # Add it to your agent
123
+ agent = RSmolagent::Agent.new(
124
+ llm_provider: llm,
125
+ tools: [ruby_executor]
126
+ )
127
+
128
+ # The agent can now execute Ruby code
129
+ result = agent.run("Calculate the factorial of 5")
130
+ ```
131
+
132
+ #### 2. CustomClassExecutorTool
133
+
134
+ Creates and executes custom tool classes with defined structure:
135
+
136
+ ```ruby
137
+ # Create a custom tool executor
138
+ custom_executor = RSmolagent::Tools::CustomClassExecutorTool.new
139
+
140
+ # Add it to your agent
141
+ agent = RSmolagent::Agent.new(
142
+ llm_provider: llm,
143
+ tools: [custom_executor]
144
+ )
145
+
146
+ # The agent can now create and use custom tools
147
+ result = agent.run("Create a tool to get the current date and then use it")
148
+ ```
149
+
150
+ #### 3. Creating Custom Tools from Code
151
+
152
+ You can also create tools from code directly using the CustomToolFactory:
153
+
154
+ ```ruby
155
+ # Define a custom tool class
156
+ tool_code = <<~RUBY
157
+ class MyCustomTool < CustomToolBase
158
+ def initialize
159
+ super(
160
+ "my_tool",
161
+ "Description of what my tool does",
162
+ {
163
+ param1: {
164
+ type: "string",
165
+ description: "Parameter description"
166
+ }
167
+ }
168
+ )
169
+ end
170
+
171
+ def run(args)
172
+ # Implement tool logic here
173
+ "Result: " + args[:param1].upcase
174
+ end
175
+ end
176
+ RUBY
177
+
178
+ # Create a tool from the code
179
+ my_tool = RSmolagent::Tools::CustomToolFactory.create_from_code(tool_code)
180
+
181
+ # Use the tool
182
+ my_tool.call(param1: "hello world") # => "Result: HELLO WORLD"
183
+ ```
184
+
185
+ ## Creating Custom Tools
186
+
187
+ To create a custom tool, simply inherit from `RSmolagent::Tool` and implement the `execute` method:
188
+
189
+ ```ruby
190
+ class MyTool < RSmolagent::Tool
191
+ def initialize
192
+ super(
193
+ name: "my_tool",
194
+ description: "Description of what the tool does",
195
+ input_schema: {
196
+ param1: {
197
+ type: "string",
198
+ description: "Description of parameter 1"
199
+ },
200
+ param2: {
201
+ type: "number",
202
+ description: "Description of parameter 2"
203
+ }
204
+ }
205
+ )
206
+ end
207
+
208
+ def execute(args)
209
+ # Implement your tool logic here
210
+ # args will contain the parameters passed by the agent
211
+ "Result of the tool execution"
212
+ end
213
+ end
214
+ ```
215
+
216
+ ## Supporting Other LLM Providers
217
+
218
+ To add support for other LLM providers, inherit from `RSmolagent::LLMProvider` and implement the required methods:
219
+
220
+ ```ruby
221
+ class MyLLMProvider < RSmolagent::LLMProvider
222
+ def initialize(model_id:, **options)
223
+ super(model_id: model_id, **options)
224
+ # Initialize your LLM client
225
+ end
226
+
227
+ def chat(messages, tools: nil, tool_choice: nil)
228
+ # Call your LLM provider's API
229
+ # Return the response in a standardized format
230
+ end
231
+
232
+ def extract_tool_calls(response)
233
+ # Extract tool calls from the response
234
+ # Return an array of tool calls in the format:
235
+ # [{ name: "tool_name", arguments: { param1: "value1", ... } }, ...]
236
+ end
237
+ end
238
+ ```
239
+
240
+ ## Testing
241
+
242
+ RSmolagent includes a comprehensive test suite using RSpec. To run the tests:
243
+
244
+ ```bash
245
+ # Install development dependencies
246
+ $ bundle install
247
+
248
+ # Run the tests
249
+ $ bundle exec rake spec
250
+ ```
251
+
252
+ ## License
253
+
254
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,164 @@
1
+ module RSmolagent
2
+ class Agent
3
+ attr_reader :memory, :tools, :llm, :max_steps
4
+
5
+ def initialize(llm_provider:, tools: [], system_prompt: nil, max_steps: 10)
6
+ @llm = llm_provider
7
+ @memory = Memory.new
8
+ @max_steps = max_steps
9
+ @dynamic_tools = {}
10
+
11
+ # Initialize system prompt
12
+ default_system_prompt = "You are a helpful assistant that can use tools to solve tasks. " +
13
+ "When you need information or want to perform actions, use the provided tools. " +
14
+ "When you have the final answer, use the final_answer tool."
15
+
16
+ system_message = system_prompt || default_system_prompt
17
+ @memory.add_system_message(system_message)
18
+
19
+ # Set up tools
20
+ @tools = {}
21
+ tools.each { |tool| @tools[tool.name] = tool }
22
+
23
+ # Add final answer tool if not present
24
+ unless @tools["final_answer"]
25
+ final_answer_tool = FinalAnswerTool.new
26
+ @tools[final_answer_tool.name] = final_answer_tool
27
+ end
28
+ end
29
+
30
+ # Method to register a new tool during execution
31
+ def register_tool(tool)
32
+ @dynamic_tools[tool.name] = tool
33
+ # Update memory with information about the new tool
34
+ @memory.add_assistant_message("I've created a new tool called '#{tool.name}' that #{tool.description}")
35
+ end
36
+
37
+ def run(task, verbose: false)
38
+ @memory.add_user_message(task)
39
+ step_count = 0
40
+
41
+ while step_count < @max_steps
42
+ step_count += 1
43
+ puts "Step #{step_count}/#{@max_steps}" if verbose
44
+
45
+ # Get response from LLM
46
+ response = execute_step
47
+
48
+ # Check if we've reached a final answer
49
+ if response[:final_answer]
50
+ return response[:answer]
51
+ end
52
+
53
+ puts "Used tool: #{response[:tool_name]}" if verbose
54
+ end
55
+
56
+ # If we reach max steps without a final answer, return what we have
57
+ "I couldn't complete the task in the allowed number of steps. My progress so far: " +
58
+ @memory.history.map { |step| step[:type] == "tool_call" ? "#{step[:tool_name]}: #{step[:result]}" : "" }.join("\n")
59
+ end
60
+
61
+ private
62
+
63
+ def execute_step
64
+ # Get current messages
65
+ messages = @memory.to_openai_messages
66
+
67
+ # Combine static and dynamic tools
68
+ all_tools = @tools.merge(@dynamic_tools)
69
+
70
+ # Call LLM with all tools
71
+ response = @llm.chat(messages, tools: all_tools.values)
72
+
73
+ # Handle tool calls or final answer
74
+ tool_calls = @llm.extract_tool_calls(response)
75
+
76
+ if tool_calls.empty?
77
+ # No tool calls, add as assistant message
78
+ @memory.add_assistant_message(response.content)
79
+ return { content: response.content }
80
+ else
81
+ # Process the first tool call
82
+ tool_call = tool_calls.first
83
+ tool_name = tool_call[:name]
84
+ arguments = tool_call[:arguments]
85
+
86
+ # Record assistant message with tool call
87
+ @memory.add_assistant_message("I'll use the #{tool_name} tool.")
88
+
89
+ if tool_name == "final_answer"
90
+ # Handle final answer
91
+ answer = arguments["answer"] || arguments[:answer] || ""
92
+ @memory.add_final_answer(answer)
93
+ return { final_answer: true, answer: answer }
94
+ else
95
+ # Execute the tool
96
+ result = execute_tool(tool_name, arguments)
97
+
98
+ # Check if this is a tool creation tool (like CustomClassExecutorTool)
99
+ if tool_name == "create_tool" || tool_name == "custom_executor"
100
+ # Try to create a new tool from the result
101
+ begin
102
+ # Parse the code from the result if needed
103
+ code = extract_tool_code_from_result(result)
104
+
105
+ if code
106
+ # Try to create a tool from the code
107
+ new_tool = Tools::CustomToolFactory.create_from_code(code)
108
+
109
+ # Register the new tool
110
+ register_tool(new_tool)
111
+
112
+ # Add confirmation to the result
113
+ result += "\n\nTool '#{new_tool.name}' has been created and is now available for use."
114
+ end
115
+ rescue => e
116
+ result += "\n\nFailed to create tool: #{e.message}"
117
+ end
118
+ end
119
+
120
+ # Add tool result to memory
121
+ @memory.add_tool_message(tool_name, result)
122
+ @memory.add_tool_call(tool_name, arguments, result)
123
+
124
+ return { tool_name: tool_name, arguments: arguments, result: result }
125
+ end
126
+ end
127
+ end
128
+
129
+ def execute_tool(tool_name, arguments)
130
+ # Look in both static and dynamic tools
131
+ tool = @tools[tool_name] || @dynamic_tools[tool_name]
132
+
133
+ if tool.nil?
134
+ return "Error: Tool '#{tool_name}' not found"
135
+ end
136
+
137
+ begin
138
+ tool.call(arguments)
139
+ rescue => e
140
+ "Error executing tool: #{e.message}"
141
+ end
142
+ end
143
+
144
+ # Helper method to extract tool code from tool results
145
+ def extract_tool_code_from_result(result)
146
+ # Look for class definition in the result
147
+ if result.include?("class") && result.include?("CustomToolBase")
148
+ # For cleaner extraction, look for Ruby code blocks
149
+ if result.include?("```ruby")
150
+ # Extract code from markdown code blocks
151
+ code_blocks = result.scan(/```ruby\n(.*?)\n```/m)
152
+ return code_blocks.first.first if code_blocks.any?
153
+ end
154
+
155
+ # If no code blocks, try to extract class definition directly
156
+ class_match = result.match(/class\s+\w+\s*<\s*CustomToolBase.*?end/m)
157
+ return class_match[0] if class_match
158
+ end
159
+
160
+ # If we couldn't extract a class definition, return nil
161
+ nil
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,70 @@
1
+ module RSmolagent
2
+ class LLMProvider
3
+ attr_reader :last_prompt_tokens, :last_completion_tokens
4
+
5
+ def initialize(model_id:, **options)
6
+ @model_id = model_id
7
+ @options = options
8
+ @last_prompt_tokens = 0
9
+ @last_completion_tokens = 0
10
+ end
11
+
12
+ def chat(messages, tools: nil, tool_choice: nil)
13
+ raise NotImplementedError, "Subclasses must implement the 'chat' method"
14
+ end
15
+
16
+ def extract_tool_calls(response)
17
+ raise NotImplementedError, "Subclasses must implement the 'extract_tool_calls' method"
18
+ end
19
+
20
+ def parse_json_if_needed(text)
21
+ return text unless text.is_a?(String)
22
+
23
+ begin
24
+ JSON.parse(text)
25
+ rescue JSON::ParserError
26
+ text
27
+ end
28
+ end
29
+ end
30
+
31
+ class OpenAIProvider < LLMProvider
32
+ def initialize(model_id:, client:, **options)
33
+ super(model_id: model_id, **options)
34
+ @client = client
35
+ end
36
+
37
+ def chat(messages, tools: nil, tool_choice: nil)
38
+ params = {
39
+ model: @model_id,
40
+ messages: messages,
41
+ temperature: @options[:temperature] || 0.7,
42
+ }
43
+
44
+ # Add tools if provided
45
+ if tools && !tools.empty?
46
+ params[:tools] = tools.map(&:to_json_schema)
47
+ params[:tool_choice] = tool_choice || "auto"
48
+ end
49
+
50
+ response = @client.chat(parameters: params)
51
+
52
+ # Update token counts
53
+ @last_prompt_tokens = response.usage.prompt_tokens
54
+ @last_completion_tokens = response.usage.completion_tokens
55
+
56
+ response.choices.first.message
57
+ end
58
+
59
+ def extract_tool_calls(response)
60
+ return [] unless response.tool_calls
61
+
62
+ response.tool_calls.map do |tool_call|
63
+ {
64
+ name: tool_call.function.name,
65
+ arguments: parse_json_if_needed(tool_call.function.arguments)
66
+ }
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,65 @@
1
+ module RSmolagent
2
+ class Memory
3
+ attr_reader :messages, :history
4
+
5
+ def initialize
6
+ @messages = []
7
+ @history = []
8
+ end
9
+
10
+ def add_system_message(content)
11
+ add_message("system", content)
12
+ end
13
+
14
+ def add_user_message(content)
15
+ add_message("user", content)
16
+ end
17
+
18
+ def add_assistant_message(content)
19
+ add_message("assistant", content)
20
+ end
21
+
22
+ def add_tool_message(name, content)
23
+ @messages << { role: "tool", name: name, content: content }
24
+ end
25
+
26
+ def add_tool_call(tool_name, arguments, result)
27
+ step = {
28
+ type: "tool_call",
29
+ tool_name: tool_name,
30
+ arguments: arguments,
31
+ result: result,
32
+ timestamp: Time.now
33
+ }
34
+ @history << step
35
+ step
36
+ end
37
+
38
+ def add_final_answer(answer)
39
+ step = {
40
+ type: "final_answer",
41
+ answer: answer,
42
+ timestamp: Time.now
43
+ }
44
+ @history << step
45
+ step
46
+ end
47
+
48
+ def to_openai_messages
49
+ @messages.map do |msg|
50
+ if msg[:role] == "tool"
51
+ { role: "tool", tool_call_id: "call_#{msg[:name]}", content: msg[:content].to_s }
52
+ else
53
+ { role: msg[:role], content: msg[:content] }
54
+ end
55
+ end
56
+ end
57
+
58
+ private
59
+
60
+ def add_message(role, content)
61
+ @messages << { role: role, content: content }
62
+ { role: role, content: content }
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,73 @@
1
+ require 'json'
2
+
3
+ module RSmolagent
4
+ class Tool
5
+ attr_reader :name, :description, :input_schema
6
+
7
+ def initialize(name:, description:, input_schema: {}, output_type: "string")
8
+ @name = name
9
+ @description = description
10
+ @input_schema = input_schema
11
+ @output_type = output_type
12
+ end
13
+
14
+ def call(args = {})
15
+ args = args.transform_keys(&:to_sym) if args.is_a?(Hash)
16
+ execute(args)
17
+ end
18
+
19
+ def execute(args)
20
+ raise NotImplementedError, "Subclasses must implement the 'execute' method"
21
+ end
22
+
23
+ def to_json_schema
24
+ {
25
+ "name" => @name,
26
+ "description" => @description,
27
+ "parameters" => {
28
+ "type" => "object",
29
+ "properties" => formatted_input_schema,
30
+ "required" => required_parameters
31
+ }
32
+ }
33
+ end
34
+
35
+ private
36
+
37
+ def formatted_input_schema
38
+ result = {}
39
+ @input_schema.each do |name, details|
40
+ result[name.to_s] = {
41
+ "type" => details[:type] || "string",
42
+ "description" => details[:description] || ""
43
+ }
44
+ end
45
+ result
46
+ end
47
+
48
+ def required_parameters
49
+ @input_schema.select { |_, details| details[:required] != false }
50
+ .keys
51
+ .map(&:to_s)
52
+ end
53
+ end
54
+
55
+ class FinalAnswerTool < Tool
56
+ def initialize
57
+ super(
58
+ name: "final_answer",
59
+ description: "Use this to provide the final answer to the task",
60
+ input_schema: {
61
+ answer: {
62
+ type: "string",
63
+ description: "The final answer to the task"
64
+ }
65
+ }
66
+ )
67
+ end
68
+
69
+ def execute(args)
70
+ args[:answer].to_s
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,91 @@
1
+ module RSmolagent
2
+ module Tools
3
+ # Base class that custom tools can inherit from when using the CustomClassExecutorTool
4
+ class CustomToolBase
5
+ attr_reader :name, :description, :input_schema
6
+
7
+ def initialize(name, description, input_schema = {})
8
+ @name = name
9
+ @description = description
10
+ @input_schema = input_schema
11
+ end
12
+
13
+ # This method must be implemented by subclasses
14
+ def run(args = {})
15
+ raise NotImplementedError, "Subclasses must implement the 'run' method"
16
+ end
17
+
18
+ # Helper methods that custom tools can use
19
+
20
+ def fetch_url(url)
21
+ require 'net/http'
22
+ require 'uri'
23
+
24
+ uri = URI(url)
25
+ Net::HTTP.get(uri)
26
+ end
27
+
28
+ def parse_json(json_string)
29
+ require 'json'
30
+ JSON.parse(json_string)
31
+ end
32
+
33
+ def to_tool
34
+ # Convert this custom tool to a RSmolagent::Tool
35
+ RSmolagent::Tool.new(
36
+ name: @name,
37
+ description: @description,
38
+ input_schema: @input_schema
39
+ ) do |args|
40
+ run(args)
41
+ end
42
+ end
43
+
44
+ # Additional helper methods for safe file operations
45
+ def read_file(path)
46
+ # Only allow reading files from a specific directory in a real implementation
47
+ File.read(path)
48
+ end
49
+
50
+ def write_file(path, content)
51
+ # Only allow writing files to a specific directory in a real implementation
52
+ File.write(path, content)
53
+ end
54
+ end
55
+
56
+ # Factory for creating tools from custom tool classes
57
+ class CustomToolFactory
58
+ def self.create_from_code(code)
59
+ # Create a clean binding
60
+ context = Object.new.instance_eval { binding }
61
+
62
+ # Make CustomToolBase available in the context
63
+ context.eval("CustomToolBase = RSmolagent::Tools::CustomToolBase")
64
+
65
+ # Evaluate the code to define the class
66
+ context.eval(code)
67
+
68
+ # Find the class name
69
+ class_name = extract_class_name(code)
70
+
71
+ # Create an instance of the class
72
+ tool_instance = context.eval("#{class_name}.new")
73
+
74
+ # Ensure it's a CustomToolBase
75
+ unless tool_instance.is_a?(CustomToolBase)
76
+ raise "The class must inherit from CustomToolBase"
77
+ end
78
+
79
+ # Convert to a Tool
80
+ tool_instance.to_tool
81
+ end
82
+
83
+ private
84
+
85
+ def self.extract_class_name(code)
86
+ match = code.match(/class\s+([A-Z][A-Za-z0-9_]*)\s*(?:<\s*(?:CustomToolBase|[A-Z][A-Za-z0-9_]*))?\s*/)
87
+ match ? match[1] : nil
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,189 @@
1
+ require 'stringio'
2
+
3
+ module RSmolagent
4
+ module Tools
5
+ class RubyExecutorTool < Tool
6
+ def initialize(name: "ruby_executor", description: nil)
7
+ description ||= "Executes Ruby code in a controlled environment. Use this to run custom Ruby code."
8
+
9
+ super(
10
+ name: name,
11
+ description: description,
12
+ input_schema: {
13
+ code: {
14
+ type: "string",
15
+ description: "The Ruby code to execute. Can include class and method definitions."
16
+ }
17
+ }
18
+ )
19
+
20
+ # Set of allowed classes/modules to access
21
+ @allowed_constants = [
22
+ 'Array', 'Hash', 'String', 'Integer', 'Float', 'Time', 'Date',
23
+ 'Enumerable', 'Math', 'JSON', 'CSV', 'URI', 'Net', 'File', 'Dir',
24
+ 'StringIO', 'Regexp'
25
+ ]
26
+ end
27
+
28
+ def execute(args)
29
+ code = args[:code]
30
+ return "Error: No code provided" if code.nil? || code.strip.empty?
31
+
32
+ # Capture stdout to return it along with the result
33
+ original_stdout = $stdout
34
+ captured_stdout = StringIO.new
35
+ $stdout = captured_stdout
36
+
37
+ result = nil
38
+ begin
39
+ # Execute the code in the current binding with security limitations
40
+ result = execute_with_safety(code)
41
+
42
+ # Format the output
43
+ stdout_content = captured_stdout.string.strip
44
+
45
+ if stdout_content.empty?
46
+ "Result: #{result.inspect}"
47
+ else
48
+ "Output:\n#{stdout_content}\n\nResult: #{result.inspect}"
49
+ end
50
+ rescue => e
51
+ "Error: #{e.message}\n#{e.backtrace.first(3).join("\n")}"
52
+ ensure
53
+ # Restore stdout
54
+ $stdout = original_stdout
55
+ end
56
+ end
57
+
58
+ private
59
+
60
+ def execute_with_safety(code)
61
+ # Create a secure binding for execution
62
+ secure_binding = create_secure_binding
63
+
64
+ # Execute the code in the secure binding
65
+ secure_binding.eval(code)
66
+ end
67
+
68
+ def create_secure_binding
69
+ # Create a new binding with limited access to dangerous methods
70
+ safe_binding = binding.dup
71
+
72
+ # Disable potentially dangerous methods in the binding
73
+ disable_dangerous_methods(safe_binding)
74
+
75
+ # Return the secured binding
76
+ safe_binding
77
+ end
78
+
79
+ def disable_dangerous_methods(binding_obj)
80
+ # This is a simplified approach - in a production environment,
81
+ # you would want a more comprehensive security model
82
+ dangerous_methods = [
83
+ 'system', 'exec', '`', 'eval', 'syscall', 'fork', 'trap',
84
+ 'require', 'load', 'open', 'Class.new', 'define_method'
85
+ ]
86
+
87
+ # This is just a basic example - a real implementation would need
88
+ # more sophisticated sandboxing
89
+ # Note: This is not a complete security solution and should be enhanced
90
+ # for production use
91
+ end
92
+ end
93
+
94
+ # A more specific custom tool executor that defines a class template
95
+ class CustomClassExecutorTool < Tool
96
+ def initialize(name: "custom_executor", description: nil, base_class: nil)
97
+ description ||= "Executes a custom Ruby class with predefined methods. " +
98
+ "The class must implement a 'run' method that will be called."
99
+
100
+ @base_class = base_class
101
+
102
+ super(
103
+ name: name,
104
+ description: description,
105
+ input_schema: {
106
+ code: {
107
+ type: "string",
108
+ description: "The Ruby class definition, must include a 'run' method"
109
+ },
110
+ args: {
111
+ type: "object",
112
+ description: "Arguments to pass to the run method (optional)",
113
+ required: false
114
+ }
115
+ }
116
+ )
117
+ end
118
+
119
+ def execute(args)
120
+ code = args[:code]
121
+ run_args = args[:args] || {}
122
+
123
+ return "Error: No code provided" if code.nil? || code.strip.empty?
124
+
125
+ # Capture stdout
126
+ original_stdout = $stdout
127
+ captured_stdout = StringIO.new
128
+ $stdout = captured_stdout
129
+
130
+ begin
131
+ # Create a clean context
132
+ context = Object.new.instance_eval { binding }
133
+
134
+ # Inject base class if provided
135
+ if @base_class
136
+ context.eval("BaseClass = #{@base_class.name}")
137
+ end
138
+
139
+ # Evaluate the code to define the class
140
+ context.eval(code)
141
+
142
+ # Find the class we just defined
143
+ class_name = extract_class_name(code)
144
+
145
+ if class_name.nil?
146
+ return "Error: Could not find a class definition in the provided code"
147
+ end
148
+
149
+ # Instantiate the class
150
+ runner = context.eval("#{class_name}.new")
151
+
152
+ # Ensure it has a run method
153
+ unless runner.respond_to?(:run)
154
+ return "Error: The class must implement a 'run' method"
155
+ end
156
+
157
+ # Run the class's run method with provided arguments
158
+ if run_args.is_a?(Hash)
159
+ result = runner.run(**run_args)
160
+ else
161
+ result = runner.run(run_args)
162
+ end
163
+
164
+ # Format the output
165
+ stdout_content = captured_stdout.string.strip
166
+
167
+ if stdout_content.empty?
168
+ "Result: #{result.inspect}"
169
+ else
170
+ "Output:\n#{stdout_content}\n\nResult: #{result.inspect}"
171
+ end
172
+ rescue => e
173
+ "Error: #{e.message}\n#{e.backtrace.first(3).join("\n")}"
174
+ ensure
175
+ # Restore stdout
176
+ $stdout = original_stdout
177
+ end
178
+ end
179
+
180
+ private
181
+
182
+ def extract_class_name(code)
183
+ # Simple regex to extract the class name
184
+ match = code.match(/class\s+([A-Z][A-Za-z0-9_]*)\s*(?:<\s*(?:BaseClass|[A-Z][A-Za-z0-9_]*))?\s*/)
185
+ match ? match[1] : nil
186
+ end
187
+ end
188
+ end
189
+ end
@@ -0,0 +1,101 @@
1
+ require 'net/http'
2
+ require 'json'
3
+ require 'uri'
4
+ require 'cgi'
5
+
6
+ module RSmolagent
7
+ module Tools
8
+ class WebSearchTool < Tool
9
+ def initialize(max_results: 5)
10
+ super(
11
+ name: "web_search",
12
+ description: "Performs a web search based on your query (like a Google search) and returns the top search results",
13
+ input_schema: {
14
+ query: {
15
+ type: "string",
16
+ description: "The search query to perform"
17
+ }
18
+ }
19
+ )
20
+ @max_results = max_results
21
+ end
22
+
23
+ def execute(args)
24
+ query = args[:query]
25
+
26
+ # Ensure query isn't empty
27
+ return "Error: Search query cannot be empty" if query.nil? || query.strip.empty?
28
+
29
+ begin
30
+ results = search_duckduckgo(query)
31
+
32
+ if results.nil? || results.empty?
33
+ return "No results found for query: '#{query}'. Try a less restrictive/shorter query."
34
+ end
35
+
36
+ # Format the results as markdown
37
+ formatted_results = format_results(results)
38
+ return "## Search Results\n\n#{formatted_results}"
39
+ rescue => e
40
+ return "Error performing search: #{e.message}"
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def search_duckduckgo(query)
47
+ # Use the DuckDuckGo API
48
+ encoded_query = CGI.escape(query)
49
+ uri = URI("https://api.duckduckgo.com/?q=#{encoded_query}&format=json")
50
+
51
+ response = Net::HTTP.get_response(uri)
52
+
53
+ if response.is_a?(Net::HTTPSuccess)
54
+ # Parse the response
55
+ data = JSON.parse(response.body)
56
+
57
+ # Extract results
58
+ results = []
59
+
60
+ # Get results from "AbstractText" if available
61
+ if data["AbstractText"] && !data["AbstractText"].empty?
62
+ results << {
63
+ title: data["Heading"],
64
+ url: data["AbstractURL"],
65
+ body: data["AbstractText"]
66
+ }
67
+ end
68
+
69
+ # Get results from "RelatedTopics"
70
+ if data["RelatedTopics"]
71
+ data["RelatedTopics"].each do |topic|
72
+ next if topic["Text"].nil? || topic["FirstURL"].nil?
73
+
74
+ # Extract title from the text (first few words)
75
+ text_parts = topic["Text"].split(" - ", 2)
76
+ title = text_parts.first || "Related Topic"
77
+ body = text_parts.last || text_parts.first
78
+
79
+ results << {
80
+ title: title,
81
+ url: topic["FirstURL"],
82
+ body: body
83
+ }
84
+ end
85
+ end
86
+
87
+ # Limit results to max_results
88
+ return results.first(@max_results)
89
+ else
90
+ raise "Request failed with status: #{response.code}"
91
+ end
92
+ end
93
+
94
+ def format_results(results)
95
+ results.map do |result|
96
+ "[#{result[:title]}](#{result[:url]})\n#{result[:body]}"
97
+ end.join("\n\n")
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,9 @@
1
+ require 'rsmolagent/tools/web_search'
2
+ require 'rsmolagent/tools/ruby_executor'
3
+ require 'rsmolagent/tools/custom_tool_base'
4
+
5
+ module RSmolagent
6
+ module Tools
7
+ # Add more tools here as needed
8
+ end
9
+ end
@@ -0,0 +1,3 @@
1
+ module RSmolagent
2
+ VERSION = "0.1.0"
3
+ end
data/lib/rsmolagent.rb ADDED
@@ -0,0 +1,10 @@
1
+ require "rsmolagent/version"
2
+ require "rsmolagent/tool"
3
+ require "rsmolagent/agent"
4
+ require "rsmolagent/llm_provider"
5
+ require "rsmolagent/memory"
6
+ require "rsmolagent/tools"
7
+
8
+ module RSmolagent
9
+ class Error < StandardError; end
10
+ end
metadata ADDED
@@ -0,0 +1,99 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rsmolagent
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - gkosmo
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2025-03-10 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: json
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: ruby-openai
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '6.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '6.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '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: '3.0'
55
+ description: RSmolagent is a Ruby library for building AI agents that can use tools
56
+ to solve tasks
57
+ email:
58
+ - gkosmo1@hotmail.com
59
+ executables: []
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - README.md
64
+ - lib/rsmolagent.rb
65
+ - lib/rsmolagent/agent.rb
66
+ - lib/rsmolagent/llm_provider.rb
67
+ - lib/rsmolagent/memory.rb
68
+ - lib/rsmolagent/tool.rb
69
+ - lib/rsmolagent/tools.rb
70
+ - lib/rsmolagent/tools/custom_tool_base.rb
71
+ - lib/rsmolagent/tools/ruby_executor.rb
72
+ - lib/rsmolagent/tools/web_search.rb
73
+ - lib/rsmolagent/version.rb
74
+ homepage: https://github.com/gkosmo/rsmolagent
75
+ licenses:
76
+ - MIT
77
+ metadata:
78
+ homepage_uri: https://github.com/gkosmo/rsmolagent
79
+ source_code_uri: https://github.com/gkosmo/rsmolagent
80
+ post_install_message:
81
+ rdoc_options: []
82
+ require_paths:
83
+ - lib
84
+ required_ruby_version: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: 2.6.0
89
+ required_rubygems_version: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ requirements: []
95
+ rubygems_version: 3.5.9
96
+ signing_key:
97
+ specification_version: 4
98
+ summary: A simple AI agent framework in Ruby
99
+ test_files: []