rubycode 0.1.1 → 0.1.2
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/.rubocop.yml +8 -0
- data/README.md +82 -13
- data/config/tools/bash.json +17 -0
- data/config/tools/done.json +17 -0
- data/config/tools/read.json +25 -0
- data/config/tools/search.json +29 -0
- data/lib/rubycode/adapters/base.rb +2 -1
- data/lib/rubycode/adapters/ollama.rb +45 -38
- data/lib/rubycode/agent_loop.rb +134 -0
- data/lib/rubycode/client/display_formatter.rb +53 -0
- data/lib/rubycode/client/response_handler.rb +55 -0
- data/lib/rubycode/client.rb +24 -172
- data/lib/rubycode/configuration.rb +7 -8
- data/lib/rubycode/context_builder.rb +3 -2
- data/lib/rubycode/errors.rb +21 -0
- data/lib/rubycode/history.rb +16 -14
- data/lib/rubycode/tools/base.rb +80 -0
- data/lib/rubycode/tools/bash.rb +34 -43
- data/lib/rubycode/tools/done.rb +5 -23
- data/lib/rubycode/tools/read.rb +40 -55
- data/lib/rubycode/tools/search.rb +42 -67
- data/lib/rubycode/tools.rb +10 -5
- data/lib/rubycode/value_objects.rb +102 -0
- data/lib/rubycode/version.rb +2 -2
- data/lib/rubycode.rb +5 -3
- data/rubycode_cli.rb +12 -13
- metadata +16 -4
- data/rubycode-0.1.0.gem +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3de3303b47050cfd2d290e35fcbf834e57c5eeb53d85a6b697e0cd554a5b05c0
|
|
4
|
+
data.tar.gz: 3a60d16a0f7fc111e6ccf305b1d4d0427f8b9b42296e79455f9b3caae06ce2ee
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ca04ec8192747e4e97cef35c2a8ec6440e82b65a412e7c13a979f84a5d58de92c615e90e58cebe28b8761ae05d462f4a5e9a38bdeb0388005234b76bb02edc67
|
|
7
|
+
data.tar.gz: 84066bb9aa138c598eedad4a48472448d253f0eaeb1551ed1041a2bb4a3fc0087cabbc4c46dffeaf14ff83c1570096ff88db3c439537debbaea0f710eb01414c
|
data/.rubocop.yml
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
AllCops:
|
|
2
2
|
TargetRubyVersion: 3.1
|
|
3
|
+
NewCops: enable
|
|
3
4
|
|
|
4
5
|
Style/StringLiterals:
|
|
5
6
|
EnforcedStyle: double_quotes
|
|
6
7
|
|
|
7
8
|
Style/StringLiteralsInInterpolation:
|
|
8
9
|
EnforcedStyle: double_quotes
|
|
10
|
+
|
|
11
|
+
# Metrics - reasonable limits for maintainability
|
|
12
|
+
Metrics/ClassLength:
|
|
13
|
+
Max: 200
|
|
14
|
+
|
|
15
|
+
Metrics/MethodLength:
|
|
16
|
+
Max: 20
|
data/README.md
CHANGED
|
@@ -1,38 +1,107 @@
|
|
|
1
|
-
#
|
|
1
|
+
# RubyCode
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://badge.fury.io/rb/rubycode)
|
|
4
|
+
[](https://github.com/jonasmedeiros/rubycode)
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
A Ruby-native AI coding assistant with pluggable LLM adapters. RubyCode provides an agent-based system that can explore codebases, search files, execute commands, and assist with coding tasks.
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
**GitHub Repository**: [github.com/jonasmedeiros/rubycode](https://github.com/jonasmedeiros/rubycode)
|
|
9
|
+
|
|
10
|
+
## Features
|
|
8
11
|
|
|
9
|
-
|
|
12
|
+
- **AI Agent Loop**: Autonomous task execution with tool calling
|
|
13
|
+
- **Pluggable LLM Adapters**: Currently supports Ollama, easily extendable to other LLMs
|
|
14
|
+
- **Built-in Tools**:
|
|
15
|
+
- `bash`: Execute safe bash commands for filesystem exploration
|
|
16
|
+
- `search`: Search file contents using grep with regex support
|
|
17
|
+
- `read`: Read files and directories with line numbers
|
|
18
|
+
- `done`: Signal task completion with final answer
|
|
19
|
+
- **Conversation History**: Maintains context across interactions
|
|
20
|
+
- **Environment Context**: Automatically provides Ruby version, platform, and working directory info
|
|
10
21
|
|
|
11
|
-
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
Install the gem by executing:
|
|
12
25
|
|
|
13
26
|
```bash
|
|
14
|
-
|
|
27
|
+
gem install rubycode
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Or add it to your application's Gemfile:
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
gem "rubycode"
|
|
15
34
|
```
|
|
16
35
|
|
|
17
|
-
|
|
36
|
+
Then execute:
|
|
18
37
|
|
|
19
38
|
```bash
|
|
20
|
-
|
|
39
|
+
bundle install
|
|
21
40
|
```
|
|
22
41
|
|
|
23
42
|
## Usage
|
|
24
43
|
|
|
25
|
-
|
|
44
|
+
### Basic Usage
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
require "rubycode"
|
|
48
|
+
|
|
49
|
+
# Configure the LLM adapter
|
|
50
|
+
RubyCode.configure do |config|
|
|
51
|
+
config.adapter = :ollama
|
|
52
|
+
config.url = "http://localhost:11434"
|
|
53
|
+
config.model = "deepseek-v3.1:671b-cloud"
|
|
54
|
+
config.root_path = Dir.pwd
|
|
55
|
+
config.debug = false
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Create a client and ask a question
|
|
59
|
+
client = RubyCode::Client.new
|
|
60
|
+
response = client.ask(prompt: "Find the User model in the codebase")
|
|
61
|
+
puts response
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Configuration Options
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
RubyCode.configure do |config|
|
|
68
|
+
config.adapter = :ollama # LLM adapter to use
|
|
69
|
+
config.url = "http://localhost:11434" # Ollama server URL
|
|
70
|
+
config.model = "deepseek-v3.1:671b-cloud" # Model name
|
|
71
|
+
config.root_path = Dir.pwd # Project root directory
|
|
72
|
+
config.debug = false # Enable debug output
|
|
73
|
+
config.enable_tool_injection_workaround = true # Force tool usage (enabled by default)
|
|
74
|
+
end
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Available Tools
|
|
78
|
+
|
|
79
|
+
The agent has access to four built-in tools:
|
|
80
|
+
|
|
81
|
+
1. **bash**: Execute safe bash commands including:
|
|
82
|
+
- Directory exploration: `ls`, `pwd`, `find`, `tree`
|
|
83
|
+
- File inspection: `cat`, `head`, `tail`, `wc`, `file`
|
|
84
|
+
- Content search: `grep`, `rg` (ripgrep)
|
|
85
|
+
- Examples: `grep -rn "button" app/views`, `find . -name "*.rb"`
|
|
86
|
+
2. **search**: Simplified search wrapper (use bash + grep for more control)
|
|
87
|
+
3. **read**: Read files with line numbers or list directory contents
|
|
88
|
+
4. **done**: Signal completion and provide the final answer
|
|
89
|
+
|
|
90
|
+
**Note**: Tool schemas are externalized in `config/tools/*.json` for easy customization.
|
|
26
91
|
|
|
27
92
|
## Development
|
|
28
93
|
|
|
29
|
-
After checking out the repo, run `
|
|
94
|
+
After checking out the repo, run `bundle install` to install dependencies.
|
|
95
|
+
|
|
96
|
+
To install this gem onto your local machine, run:
|
|
30
97
|
|
|
31
|
-
|
|
98
|
+
```bash
|
|
99
|
+
bundle exec rake install
|
|
100
|
+
```
|
|
32
101
|
|
|
33
102
|
## Contributing
|
|
34
103
|
|
|
35
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/
|
|
104
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/jonasmedeiros/rubycode.
|
|
36
105
|
|
|
37
106
|
## License
|
|
38
107
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type": "function",
|
|
3
|
+
"function": {
|
|
4
|
+
"name": "bash",
|
|
5
|
+
"description": "Execute safe bash commands for exploring the filesystem and terminal operations.\n\nUse this for any command-line operations including:\n- Directory exploration: ls, find, tree\n- File inspection: cat, head, tail, wc, file\n- Content search: grep, rg (ripgrep)\n\nExamples:\n- grep -rn 'button' app/views\n- find . -name '*.rb' -type f\n- ls -la app/\n\nWhitelisted commands: ls, pwd, find, tree, cat, head, tail, wc, file, which, echo, grep, rg",
|
|
6
|
+
"parameters": {
|
|
7
|
+
"type": "object",
|
|
8
|
+
"properties": {
|
|
9
|
+
"command": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"description": "The bash command to execute. Examples:\n- 'grep -rn \"button\" app/views' (search for text in files)\n- 'find . -name \"*.rb\"' (find files by name)\n- 'ls -la app/' (list directory contents)"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"required": ["command"]
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type": "function",
|
|
3
|
+
"function": {
|
|
4
|
+
"name": "done",
|
|
5
|
+
"description": "Call this when you have found the code and are ready to provide your final answer. This signals you are finished exploring.",
|
|
6
|
+
"parameters": {
|
|
7
|
+
"type": "object",
|
|
8
|
+
"properties": {
|
|
9
|
+
"answer": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"description": "Your final answer showing the file, line number, current code, and suggested change"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"required": ["answer"]
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type": "function",
|
|
3
|
+
"function": {
|
|
4
|
+
"name": "read",
|
|
5
|
+
"description": "Read a file or directory from the filesystem.\n\n- Use this when you know the file path and want to see its contents\n- Returns file contents with line numbers (format: 'line_number: content')\n- For directories, returns a listing of entries\n- Use the search tool to find specific content in files\n- Use bash with 'ls' or 'find' to discover what files exist",
|
|
6
|
+
"parameters": {
|
|
7
|
+
"type": "object",
|
|
8
|
+
"properties": {
|
|
9
|
+
"file_path": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"description": "Absolute or relative path to the file to read"
|
|
12
|
+
},
|
|
13
|
+
"offset": {
|
|
14
|
+
"type": "integer",
|
|
15
|
+
"description": "Line number to start reading from (1-indexed). Optional."
|
|
16
|
+
},
|
|
17
|
+
"limit": {
|
|
18
|
+
"type": "integer",
|
|
19
|
+
"description": "Maximum number of lines to read. Default 2000. Optional."
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"required": ["file_path"]
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type": "function",
|
|
3
|
+
"function": {
|
|
4
|
+
"name": "search",
|
|
5
|
+
"description": "Search INSIDE file contents for patterns using grep. Returns matching lines with file paths and line numbers.\n\n- Searches file CONTENTS using regular expressions\n- Use this when you need to find WHERE specific text/code appears inside files\n- Returns file paths, line numbers, and the matching content\n- Example: search for 'button' to find files containing that text",
|
|
6
|
+
"parameters": {
|
|
7
|
+
"type": "object",
|
|
8
|
+
"properties": {
|
|
9
|
+
"pattern": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"description": "The pattern to search for (supports regex)"
|
|
12
|
+
},
|
|
13
|
+
"path": {
|
|
14
|
+
"type": "string",
|
|
15
|
+
"description": "Directory or file to search in. Defaults to '.' (current directory). Optional."
|
|
16
|
+
},
|
|
17
|
+
"include": {
|
|
18
|
+
"type": "string",
|
|
19
|
+
"description": "File pattern to include (e.g., '*.rb', '*.js'). Optional."
|
|
20
|
+
},
|
|
21
|
+
"case_insensitive": {
|
|
22
|
+
"type": "boolean",
|
|
23
|
+
"description": "Perform case-insensitive search. Optional."
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"required": ["pattern"]
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -3,55 +3,62 @@
|
|
|
3
3
|
require "net/http"
|
|
4
4
|
require "json"
|
|
5
5
|
|
|
6
|
-
module
|
|
6
|
+
module RubyCode
|
|
7
7
|
module Adapters
|
|
8
|
+
# Ollama adapter for local LLM integration
|
|
8
9
|
class Ollama < Base
|
|
9
10
|
def generate(messages:, system: nil, tools: nil)
|
|
10
11
|
uri = URI("#{@config.url}/api/chat")
|
|
12
|
+
payload = build_payload(messages, system, tools)
|
|
13
|
+
request = build_request(uri, payload)
|
|
11
14
|
|
|
12
|
-
|
|
13
|
-
|
|
15
|
+
debug_request(uri, payload) if @config.debug
|
|
16
|
+
|
|
17
|
+
body = send_request(uri, request)
|
|
14
18
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
debug_response(body) if @config.debug
|
|
20
|
+
|
|
21
|
+
body
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def build_payload(messages, system, tools)
|
|
27
|
+
payload = { model: @config.model, messages: messages, stream: false }
|
|
20
28
|
payload[:system] = system if system
|
|
21
29
|
payload[:tools] = tools if tools
|
|
30
|
+
payload
|
|
31
|
+
end
|
|
22
32
|
|
|
33
|
+
def build_request(uri, payload)
|
|
34
|
+
request = Net::HTTP::Post.new(uri)
|
|
35
|
+
request["Content-Type"] = "application/json"
|
|
23
36
|
request.body = payload.to_json
|
|
37
|
+
request
|
|
38
|
+
end
|
|
24
39
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
puts "=" * 80
|
|
48
|
-
puts JSON.pretty_generate(body)
|
|
49
|
-
puts "=" * 80 + "\n"
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
# /api/chat always returns this structure:
|
|
53
|
-
# { "message": { "role": "assistant", "content": "...", "tool_calls": [...] } }
|
|
54
|
-
body
|
|
40
|
+
def send_request(uri, request)
|
|
41
|
+
response = Net::HTTP.start(uri.hostname, uri.port) { |http| http.request(request) }
|
|
42
|
+
JSON.parse(response.body)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def debug_request(uri, payload)
|
|
46
|
+
puts "\n#{"=" * 80}"
|
|
47
|
+
puts "📤 REQUEST TO LLM"
|
|
48
|
+
puts "=" * 80
|
|
49
|
+
puts "URL: #{uri}"
|
|
50
|
+
puts "Model: #{@config.model}"
|
|
51
|
+
puts "\nPayload:"
|
|
52
|
+
puts JSON.pretty_generate(payload)
|
|
53
|
+
puts "=" * 80
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def debug_response(body)
|
|
57
|
+
puts "\n#{"=" * 80}"
|
|
58
|
+
puts "📥 RESPONSE FROM LLM"
|
|
59
|
+
puts "=" * 80
|
|
60
|
+
puts JSON.pretty_generate(body)
|
|
61
|
+
puts "#{"=" * 80}\n"
|
|
55
62
|
end
|
|
56
63
|
end
|
|
57
64
|
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "client/response_handler"
|
|
4
|
+
require_relative "client/display_formatter"
|
|
5
|
+
|
|
6
|
+
module RubyCode
|
|
7
|
+
# Manages the agent loop - iterates until task completion or limits reached
|
|
8
|
+
class AgentLoop
|
|
9
|
+
MAX_ITERATIONS = 25
|
|
10
|
+
MAX_TOOL_CALLS = 50
|
|
11
|
+
|
|
12
|
+
def initialize(adapter:, history:, config:, system_prompt:)
|
|
13
|
+
@adapter = adapter
|
|
14
|
+
@history = history
|
|
15
|
+
@config = config
|
|
16
|
+
@system_prompt = system_prompt
|
|
17
|
+
@response_handler = Client::ResponseHandler.new(history: @history, config: @config)
|
|
18
|
+
@display_formatter = Client::DisplayFormatter.new(config: @config)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def run
|
|
22
|
+
iteration = 0
|
|
23
|
+
total_tool_calls = 0
|
|
24
|
+
|
|
25
|
+
loop do
|
|
26
|
+
iteration += 1
|
|
27
|
+
|
|
28
|
+
return @response_handler.handle_max_iterations(iteration) if iteration > MAX_ITERATIONS
|
|
29
|
+
|
|
30
|
+
content, tool_calls = llm_response
|
|
31
|
+
|
|
32
|
+
if tool_calls.empty?
|
|
33
|
+
result = @response_handler.handle_empty_tool_calls(content, iteration, total_tool_calls)
|
|
34
|
+
return result if result
|
|
35
|
+
|
|
36
|
+
next # Continue loop for workaround case
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
total_tool_calls += tool_calls.length
|
|
40
|
+
return @response_handler.handle_max_tool_calls(content, total_tool_calls) if total_tool_calls > MAX_TOOL_CALLS
|
|
41
|
+
|
|
42
|
+
done_result = execute_tool_calls(tool_calls, iteration)
|
|
43
|
+
return @response_handler.finalize_response(done_result, iteration, total_tool_calls) if done_result
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def llm_response
|
|
50
|
+
messages = @history.to_llm_format
|
|
51
|
+
response_body = @adapter.generate(
|
|
52
|
+
messages: messages,
|
|
53
|
+
system: @system_prompt,
|
|
54
|
+
tools: Tools.definitions
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
assistant_message = response_body["message"]
|
|
58
|
+
content = assistant_message["content"] || ""
|
|
59
|
+
tool_calls = assistant_message["tool_calls"] || []
|
|
60
|
+
|
|
61
|
+
@history.add_message(role: "assistant", content: content)
|
|
62
|
+
[content, tool_calls]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def execute_tool_calls(tool_calls, iteration)
|
|
66
|
+
puts "\n🤖 Iteration #{iteration}: Calling #{tool_calls.length} tool(s)..." unless @config.debug
|
|
67
|
+
|
|
68
|
+
done_result = nil
|
|
69
|
+
tool_calls.each do |tool_call|
|
|
70
|
+
result = execute_tool(tool_call)
|
|
71
|
+
|
|
72
|
+
if tool_call.dig("function", "name") == "done"
|
|
73
|
+
done_result = result
|
|
74
|
+
break
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
done_result
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def execute_tool(tool_call)
|
|
81
|
+
tool_name = tool_call.dig("function", "name")
|
|
82
|
+
arguments = tool_call.dig("function", "arguments")
|
|
83
|
+
|
|
84
|
+
@display_formatter.display_tool_info(tool_name, arguments)
|
|
85
|
+
|
|
86
|
+
params = parse_arguments(arguments)
|
|
87
|
+
return nil unless params
|
|
88
|
+
|
|
89
|
+
result = run_tool(tool_name, params)
|
|
90
|
+
return nil unless result
|
|
91
|
+
|
|
92
|
+
@display_formatter.display_result(result)
|
|
93
|
+
add_tool_result_to_history(tool_name, result)
|
|
94
|
+
result
|
|
95
|
+
rescue ToolError, StandardError => e
|
|
96
|
+
# Handle all errors - add to history and continue
|
|
97
|
+
handle_tool_error(e)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def parse_arguments(arguments)
|
|
101
|
+
arguments.is_a?(Hash) ? arguments : JSON.parse(arguments)
|
|
102
|
+
rescue JSON::ParserError => e
|
|
103
|
+
error_msg = "Error parsing tool arguments: #{e.message}"
|
|
104
|
+
puts " ✗ #{error_msg}"
|
|
105
|
+
@history.add_message(role: "user", content: error_msg)
|
|
106
|
+
nil
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def run_tool(tool_name, params)
|
|
110
|
+
context = { root_path: @config.root_path }
|
|
111
|
+
Tools.execute(tool_name: tool_name, params: params, context: context)
|
|
112
|
+
rescue ToolError => e
|
|
113
|
+
# Re-raise tool errors to be caught by execute_tool
|
|
114
|
+
raise e
|
|
115
|
+
rescue StandardError => e
|
|
116
|
+
# Wrap unexpected errors
|
|
117
|
+
error_msg = "Error executing tool: #{e.message}"
|
|
118
|
+
puts " ✗ #{error_msg}"
|
|
119
|
+
@history.add_message(role: "user", content: error_msg)
|
|
120
|
+
nil
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def add_tool_result_to_history(tool_name, result)
|
|
124
|
+
@history.add_message(role: "user", content: "Tool '#{tool_name}' result:\n#{result}")
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def handle_tool_error(error)
|
|
128
|
+
error_msg = "Error: #{error.message}"
|
|
129
|
+
puts " ✗ #{error_msg}"
|
|
130
|
+
@history.add_message(role: "user", content: error_msg)
|
|
131
|
+
nil
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module RubyCode
|
|
6
|
+
class Client
|
|
7
|
+
# Handles formatting and display of tool information and results
|
|
8
|
+
class DisplayFormatter
|
|
9
|
+
TOOL_ICONS = {
|
|
10
|
+
"bash" => ["💻", "command"],
|
|
11
|
+
"read" => ["📖", "file_path"],
|
|
12
|
+
"search" => ["🔍", "pattern"]
|
|
13
|
+
}.freeze
|
|
14
|
+
|
|
15
|
+
def initialize(config:)
|
|
16
|
+
@config = config
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def display_tool_info(tool_name, arguments)
|
|
20
|
+
if @config.debug
|
|
21
|
+
puts "\n🔧 Tool: #{tool_name}"
|
|
22
|
+
puts " Args: #{arguments.inspect}"
|
|
23
|
+
else
|
|
24
|
+
display_minimal_tool_info(tool_name, arguments)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def display_result(result)
|
|
29
|
+
return unless @config.debug
|
|
30
|
+
|
|
31
|
+
first_line = result.lines.first&.strip || "(empty)"
|
|
32
|
+
suffix = result.lines.count > 1 ? "... (#{result.lines.count} lines)" : ""
|
|
33
|
+
puts " ✓ Result: #{first_line}#{suffix}"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def display_minimal_tool_info(tool_name, arguments)
|
|
39
|
+
return unless TOOL_ICONS.key?(tool_name)
|
|
40
|
+
|
|
41
|
+
icon, key = TOOL_ICONS[tool_name]
|
|
42
|
+
value = extract_argument_value(arguments, key)
|
|
43
|
+
puts " #{icon} #{value}" if value
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def extract_argument_value(arguments, key)
|
|
47
|
+
arguments.is_a?(Hash) ? arguments[key] : JSON.parse(arguments)[key]
|
|
48
|
+
rescue JSON::ParserError
|
|
49
|
+
nil
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyCode
|
|
4
|
+
class Client
|
|
5
|
+
# Handles response scenarios (max iterations, empty tool calls, etc.)
|
|
6
|
+
class ResponseHandler
|
|
7
|
+
MAX_ITERATIONS = 25
|
|
8
|
+
MAX_TOOL_CALLS = 50
|
|
9
|
+
|
|
10
|
+
def initialize(history:, config:)
|
|
11
|
+
@history = history
|
|
12
|
+
@config = config
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def handle_max_iterations(_iteration)
|
|
16
|
+
error_msg = "⚠️ Reached maximum iterations (#{MAX_ITERATIONS}). The agent may be stuck in a loop."
|
|
17
|
+
puts "\n#{error_msg}\n"
|
|
18
|
+
@history.add_message(role: "assistant", content: error_msg)
|
|
19
|
+
error_msg
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def handle_empty_tool_calls(content, iteration, total_tool_calls)
|
|
23
|
+
if @config.enable_tool_injection_workaround && iteration < 10
|
|
24
|
+
inject_tool_reminder(iteration)
|
|
25
|
+
return nil
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
puts "\n✅ Agent finished (#{iteration} iterations, #{total_tool_calls} tool calls)\n" unless @config.debug
|
|
29
|
+
content
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def handle_max_tool_calls(content, _total_tool_calls)
|
|
33
|
+
error_msg = "⚠️ Reached maximum tool calls (#{MAX_TOOL_CALLS}). Stopping to prevent excessive operations."
|
|
34
|
+
puts "\n#{error_msg}\n"
|
|
35
|
+
@history.add_message(role: "assistant", content: error_msg)
|
|
36
|
+
content.empty? ? error_msg : content
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def finalize_response(done_result, iteration, total_tool_calls)
|
|
40
|
+
puts "\n✅ Agent finished (#{iteration} iterations, #{total_tool_calls + 1} tool calls)\n" unless @config.debug
|
|
41
|
+
done_result
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def inject_tool_reminder(iteration)
|
|
47
|
+
puts " ⚠️ No tool calls - injecting reminder (iteration #{iteration})" unless @config.debug
|
|
48
|
+
@history.add_message(
|
|
49
|
+
role: "user",
|
|
50
|
+
content: "You MUST call a tool. Do not respond with text. Call search, read, bash, or done tool now."
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|