aigency 0.2.0 → 0.3.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/CHANGELOG.md +4 -0
- data/README.md +8 -1
- data/lib/aigency/aigent.rb +79 -44
- data/lib/aigency/plugin.rb +35 -0
- data/lib/aigency/sandbox/Dockerfile.tpl +13 -0
- data/lib/aigency/sandbox/docker-compose.yml.tpl +26 -0
- data/lib/aigency/sandbox/exec.rb +21 -0
- data/lib/aigency/sandbox/squid.conf.tpl +23 -0
- data/lib/aigency/sandbox.rb +21 -0
- data/lib/aigency/shell.rb +10 -3
- data/lib/aigency/version.rb +1 -1
- data/lib/aigency/workspace/cat.rb +11 -0
- data/lib/aigency/workspace/cp.rb +11 -0
- data/lib/aigency/workspace/diff.rb +11 -0
- data/lib/aigency/workspace/grep.rb +15 -0
- data/lib/aigency/workspace/head.rb +11 -0
- data/lib/aigency/workspace/helpers.rb +40 -3
- data/lib/aigency/workspace/ls.rb +11 -0
- data/lib/aigency/workspace/mkdir.rb +11 -0
- data/lib/aigency/workspace/mv.rb +11 -0
- data/lib/aigency/workspace/rm.rb +11 -0
- data/lib/aigency/workspace/shell.rb +59 -0
- data/lib/aigency/workspace/tail.rb +11 -0
- data/lib/aigency/workspace/wc.rb +13 -0
- data/lib/aigency/workspace/write.rb +36 -0
- data/lib/aigency/workspace.rb +21 -7
- data/lib/aigency.rb +2 -1
- data/scaffold/.env.example +4 -0
- data/scaffold/Gemfile +1 -1
- data/scaffold/README.md +31 -19
- data/scaffold/Rakefile +1 -1
- data/scaffold/config/aigent.yml +8 -5
- metadata +77 -7
- data/lib/aigency/tools.rb +0 -55
- data/lib/aigency/workspace/directory_deleter.rb +0 -23
- data/lib/aigency/workspace/directory_reader.rb +0 -33
- data/lib/aigency/workspace/file_deleter.rb +0 -22
- data/lib/aigency/workspace/file_reader.rb +0 -27
- data/lib/aigency/workspace/file_writer.rb +0 -26
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8dc4124a357fd322bcbcd526f42cafc247b5f70e2a367db8bdec26dc2cd5af75
|
|
4
|
+
data.tar.gz: 8cea262865e5e6ffd4b9c1d7efc71b688814363a446684a71ae671bcbe9b316c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a989b935e3554e6cbb081130910b0e81a2c0146507d81ca527024d1f356c419dc8be026d2846ee85fb8233a767a4eef0b2337d1facad1d99b840214042283972
|
|
7
|
+
data.tar.gz: c545ffd5bba322f455931ee5658cfb0c8b72d22d4da07a231214968160d17d109791ae1293ee9564532171327dec405508edb05915173e12c64744a8dbf22216
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
A framework for creating AI agents with RubyLLM.
|
|
4
4
|
|
|
5
|
+
## Requierments
|
|
6
|
+
|
|
7
|
+
- Ruby 3.2+
|
|
8
|
+
- Docker 29+
|
|
9
|
+
|
|
10
|
+
(Note: Aigency is not tested on Windows. Some features are known to be Windows-incompatible.)
|
|
11
|
+
|
|
5
12
|
## Installation
|
|
6
13
|
|
|
7
14
|
Install the gem from the command line:
|
|
@@ -38,7 +45,7 @@ rake setup
|
|
|
38
45
|
|
|
39
46
|
The bare minimum configuration you need to do is setting `openai_api_base` and `openai_api_key` in `config/aigent.yml`.
|
|
40
47
|
|
|
41
|
-
Test the aigent with
|
|
48
|
+
Test the aigent with the `rake say` task.
|
|
42
49
|
|
|
43
50
|
```bash
|
|
44
51
|
rake say["How many fibers are intertwined in a shredded wheat biscuit?"]
|
data/lib/aigency/aigent.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'fileutils'
|
|
4
4
|
require 'yaml'
|
|
5
|
+
require 'set'
|
|
5
6
|
|
|
6
7
|
module Aigency
|
|
7
8
|
class Aigent
|
|
@@ -38,30 +39,48 @@ module Aigency
|
|
|
38
39
|
# Initialize a RubyLLM::Chat.
|
|
39
40
|
#
|
|
40
41
|
# @param extra [Array<String>] Additional instructions
|
|
41
|
-
# @param
|
|
42
|
+
# @param except [Array<Class<Tool>>] Tools to exclude from this chat
|
|
42
43
|
# @return [RubyLLM::Chat]
|
|
43
|
-
def brief(*extra,
|
|
44
|
+
def brief(*extra, except: [])
|
|
44
45
|
chat = rubyllm_context.chat(provider: :openai)
|
|
45
|
-
tools.each { |klass| chat.with_tool(klass.new(self)) }
|
|
46
|
+
(tools - except).each { |klass| chat.with_tool(klass.new(self)) }
|
|
46
47
|
chat.with_instructions(build_instructions(extra), append: true)
|
|
47
48
|
chat.with_params(max_tokens: max_tokens)
|
|
48
49
|
end
|
|
49
50
|
|
|
50
|
-
|
|
51
|
-
|
|
51
|
+
# Prompt a chat with a file.
|
|
52
|
+
#
|
|
53
|
+
# @param filename [String]
|
|
54
|
+
# @param extra [Array<String>] Additional instructions
|
|
55
|
+
# @param except [Array<Class<Tool>>] Tools to exclude from this chat
|
|
56
|
+
# @param out [IO]
|
|
57
|
+
# @return [RubyLLM::Chat]
|
|
58
|
+
def load filename, *extra, except: [], out: $stdout
|
|
59
|
+
stream File.read(filename), *extra, except: except, out: out
|
|
52
60
|
end
|
|
53
61
|
|
|
54
|
-
|
|
62
|
+
# Prompt a chat.
|
|
63
|
+
#
|
|
64
|
+
# @param text [String]
|
|
65
|
+
# @param extra [Array<String>] Additional instructions
|
|
66
|
+
# @param except [Array<Class<Tool>>] Tools to exclude from this chat
|
|
67
|
+
# @param out [IO]
|
|
68
|
+
# @return [RubyLLM::Chat]
|
|
69
|
+
def stream text, *extra, attach: [], except: [], out: $stdout
|
|
55
70
|
original_log = @log
|
|
56
71
|
@log = out
|
|
57
|
-
response = brief(*extra,
|
|
72
|
+
response = brief(*extra, except: except).ask text, with: attach do |chunk|
|
|
58
73
|
@log.print chunk.thinking&.text
|
|
59
74
|
@log.print chunk.content
|
|
60
75
|
end
|
|
61
76
|
@log.puts "Input tokens: #{response.tokens.input}"
|
|
62
77
|
@log.puts "Output tokens: #{response.tokens.output}"
|
|
63
|
-
@log = original_log
|
|
64
78
|
response
|
|
79
|
+
rescue StandardError => e
|
|
80
|
+
@log.puts "[#{e.class}] #{e.message}"
|
|
81
|
+
@log.puts e.backtrace
|
|
82
|
+
ensure
|
|
83
|
+
@log = original_log
|
|
65
84
|
end
|
|
66
85
|
|
|
67
86
|
# @param name [String]
|
|
@@ -70,38 +89,11 @@ module Aigency
|
|
|
70
89
|
configs[name] ||= read_config(name)
|
|
71
90
|
end
|
|
72
91
|
|
|
73
|
-
# Initialize the aigent's
|
|
92
|
+
# Initialize the aigent's plugin configurations.
|
|
74
93
|
#
|
|
75
|
-
# @
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
only = [only].flatten.compact
|
|
79
|
-
config_count = 0
|
|
80
|
-
Tools.config_map.each do |name, text|
|
|
81
|
-
next unless only.empty? || only.include?(name)
|
|
82
|
-
|
|
83
|
-
dir = directory('config')
|
|
84
|
-
FileUtils.mkdir_p dir
|
|
85
|
-
path = File.join(dir, "#{name}.yml")
|
|
86
|
-
if overwrite || !File.file?(path)
|
|
87
|
-
doing = File.file?(path) ? 'Overwriting' : 'Writing new'
|
|
88
|
-
log.puts "#{doing} config for #{name}"
|
|
89
|
-
File.write path, text
|
|
90
|
-
config_count += 1
|
|
91
|
-
else
|
|
92
|
-
log.puts "Config for #{name} exists, skipping"
|
|
93
|
-
end
|
|
94
|
-
end
|
|
95
|
-
dir_count = 0
|
|
96
|
-
Tools.directories.each do |path|
|
|
97
|
-
absolute = directory(path)
|
|
98
|
-
next if File.directory?(absolute)
|
|
99
|
-
|
|
100
|
-
FileUtils.mkdir_p absolute
|
|
101
|
-
dir_count += 1
|
|
102
|
-
end
|
|
103
|
-
log.puts "#{config_count} config files initialized."
|
|
104
|
-
log.puts "#{dir_count} directories created."
|
|
94
|
+
# @return [void]
|
|
95
|
+
def setup
|
|
96
|
+
plugins.each { |plugin| boot plugin }
|
|
105
97
|
end
|
|
106
98
|
|
|
107
99
|
# Run code in an exclusive lock
|
|
@@ -142,8 +134,49 @@ module Aigency
|
|
|
142
134
|
@instructions ||= build_instructions([])
|
|
143
135
|
end
|
|
144
136
|
|
|
137
|
+
# @return [Array<Plugin>]
|
|
138
|
+
def plugins
|
|
139
|
+
config('aigent').fetch('plugins', [])
|
|
140
|
+
.map { |name| Plugin.registry[name] }
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def tools
|
|
144
|
+
plugins.flat_map(&:tools)
|
|
145
|
+
end
|
|
146
|
+
|
|
145
147
|
private
|
|
146
148
|
|
|
149
|
+
# @param plugin [Plugin]
|
|
150
|
+
def boot(plugin)
|
|
151
|
+
unless plugin.config.empty?
|
|
152
|
+
config_file = File.absolute_path("#{plugin.name}.yml", directory)
|
|
153
|
+
if File.exist?(config_file)
|
|
154
|
+
log.puts "File #{plugin.name}.yml exists, skipping"
|
|
155
|
+
else
|
|
156
|
+
log.puts "Creating #{plugin.name}.yml"
|
|
157
|
+
File.write config_file, plugin.config
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
plugin.directories.each do |dir|
|
|
161
|
+
absolute = File.absolute_path(dir, directory)
|
|
162
|
+
if File.directory?(absolute)
|
|
163
|
+
log.puts "Directory #{dir} exists, skipping"
|
|
164
|
+
else
|
|
165
|
+
log.puts "Creating #{dir}"
|
|
166
|
+
FileUtils.mkdir_p absolute
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
plugin.files.each do |file, content|
|
|
170
|
+
absolute = File.absolute_path(file, directory)
|
|
171
|
+
if File.exist?(absolute)
|
|
172
|
+
log.puts "File #{file} exists, skipping"
|
|
173
|
+
else
|
|
174
|
+
log.puts "Creating #{file}"
|
|
175
|
+
File.write absolute, content
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
147
180
|
def configs
|
|
148
181
|
@configs ||= {}
|
|
149
182
|
end
|
|
@@ -157,7 +190,9 @@ module Aigency
|
|
|
157
190
|
end
|
|
158
191
|
full_path = File.join(directory, 'config', filename)
|
|
159
192
|
if File.file?(full_path)
|
|
160
|
-
|
|
193
|
+
content = File.read(full_path)
|
|
194
|
+
.gsub(/\${([^}]+)}/) { ENV.fetch($1) }
|
|
195
|
+
YAML.load(content)
|
|
161
196
|
else
|
|
162
197
|
warn "Config name #{name} not found at #{full_path}"
|
|
163
198
|
{}
|
|
@@ -184,10 +219,10 @@ module Aigency
|
|
|
184
219
|
end
|
|
185
220
|
|
|
186
221
|
def tool_prompts
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
222
|
+
# @todo Should aigents load prompts from config files that do not have
|
|
223
|
+
# a corresponding plugin?
|
|
224
|
+
plugins.map { |plugin| config(plugin.name)['prompt'] }
|
|
225
|
+
.compact
|
|
191
226
|
end
|
|
192
227
|
|
|
193
228
|
def load_requires
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aigency
|
|
4
|
+
class Plugin
|
|
5
|
+
attr_reader :name, :config, :tools, :directories, :files
|
|
6
|
+
|
|
7
|
+
# @param name [String]
|
|
8
|
+
# @param tools [Array<Class<Tool>>]
|
|
9
|
+
# @param config [String]
|
|
10
|
+
# @param directories [Array<String>]
|
|
11
|
+
# @param files [Hash{String => String}]
|
|
12
|
+
def initialize name, tools: [], config: '', directories: [], files: {}
|
|
13
|
+
raise "A plugin named '#{name}' already exists" if self.class.registry.key?(name)
|
|
14
|
+
@name = name
|
|
15
|
+
@config = config
|
|
16
|
+
@tools = tools
|
|
17
|
+
@directories = directories
|
|
18
|
+
@files = files
|
|
19
|
+
self.class.registry[name] = self
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.registry
|
|
23
|
+
@registry ||= {}
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @param plugin [Plugin]
|
|
27
|
+
def self.register plugin
|
|
28
|
+
registry[plugin.name] = plugin
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.create(...)
|
|
32
|
+
register new(...)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
services:
|
|
2
|
+
app:
|
|
3
|
+
build:
|
|
4
|
+
context: .
|
|
5
|
+
dockerfile: Dockerfile
|
|
6
|
+
volumes:
|
|
7
|
+
- ./data/sandbox:/sandbox
|
|
8
|
+
- ./data/sandbox_ssh:/sandbox/.ssh:ro
|
|
9
|
+
working_dir: /sandbox
|
|
10
|
+
command: ["sleep", "infinity"]
|
|
11
|
+
|
|
12
|
+
egress:
|
|
13
|
+
image: ubuntu/squid
|
|
14
|
+
networks: [sandbox, outside]
|
|
15
|
+
volumes:
|
|
16
|
+
- ./squid.conf:/etc/squid/squid.conf:ro
|
|
17
|
+
|
|
18
|
+
networks:
|
|
19
|
+
sandbox:
|
|
20
|
+
internal: true
|
|
21
|
+
driver: bridge
|
|
22
|
+
driver_opts:
|
|
23
|
+
com.docker.network.bridge.gateway_mode_ipv4: isolated
|
|
24
|
+
com.docker.network.bridge.gateway_mode_ipv6: isolated
|
|
25
|
+
outside:
|
|
26
|
+
driver: bridge
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'open3'
|
|
4
|
+
|
|
5
|
+
module Aigency
|
|
6
|
+
module Sandbox
|
|
7
|
+
class Exec < Aigency::Tool
|
|
8
|
+
desc 'Run a shell command in your sandbox'
|
|
9
|
+
|
|
10
|
+
params do
|
|
11
|
+
string :command, description: "The complete command to execute", required: true
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def execute(command:)
|
|
15
|
+
aigent.log.puts "[Docker::Compose::Exec] #{command}"
|
|
16
|
+
output, error, result = Open3.capture3('docker', 'compose', 'exec', 'app', 'sh', '-c', command, chdir: aigent.directory)
|
|
17
|
+
{ success: result.success?, stdout: output, stderr: error }
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Allowlist-only forward proxy.
|
|
2
|
+
# NOTE: this file deliberately does NOT `include /etc/squid/conf.d/*`, so the
|
|
3
|
+
# rules Ubuntu ships in the image can't silently widen the allowlist.
|
|
4
|
+
|
|
5
|
+
http_port 3128
|
|
6
|
+
visible_hostname egress
|
|
7
|
+
cache deny all # forward proxy only, nothing to disk
|
|
8
|
+
access_log daemon:/var/log/squid/access.log
|
|
9
|
+
logfile_rotate 0
|
|
10
|
+
forwarded_for off # don't leak the container's IP downstream
|
|
11
|
+
|
|
12
|
+
acl SSL_ports port 443
|
|
13
|
+
acl Safe_ports port 80 443
|
|
14
|
+
acl http_targets dstdomain example.com
|
|
15
|
+
|
|
16
|
+
acl SSH_ports port 22
|
|
17
|
+
acl ssh_targets dstdomain example.com
|
|
18
|
+
|
|
19
|
+
http_access allow CONNECT ssh_targets SSH_ports
|
|
20
|
+
http_access deny !Safe_ports
|
|
21
|
+
http_access deny CONNECT !SSL_ports
|
|
22
|
+
http_access allow http_targets
|
|
23
|
+
http_access deny all
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aigency
|
|
4
|
+
module Sandbox
|
|
5
|
+
require 'aigency/sandbox/exec'
|
|
6
|
+
|
|
7
|
+
Plugin.register Plugin.new(
|
|
8
|
+
'sandbox',
|
|
9
|
+
tools: [Sandbox::Exec],
|
|
10
|
+
directories: ['data/sandbox', 'data/sandbox_ssh'],
|
|
11
|
+
files: {
|
|
12
|
+
'Dockerfile' => File.read(File.join(__dir__, 'sandbox', 'Dockerfile.tpl')),
|
|
13
|
+
'docker-compose.yml' => File.read(File.join(__dir__, 'sandbox', 'docker-compose.yml.tpl')),
|
|
14
|
+
'squid.conf' => File.read(File.join(__dir__, 'sandbox', 'squid.conf.tpl'))
|
|
15
|
+
},
|
|
16
|
+
config: <<~FILE
|
|
17
|
+
prompt: "The sandbox tools provide access to a Docker container."
|
|
18
|
+
FILE
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
end
|
data/lib/aigency/shell.rb
CHANGED
|
@@ -7,9 +7,7 @@ module Aigency
|
|
|
7
7
|
class Shell < Thor
|
|
8
8
|
map %i[new] => :enlist
|
|
9
9
|
desc "enlist [DIRECTORY]", "Enable the current user as an aigent"
|
|
10
|
-
|
|
11
|
-
def enlist
|
|
12
|
-
directory = options[:directory]
|
|
10
|
+
def enlist directory = 'aigent'
|
|
13
11
|
puts "Installing aigent at #{directory}"
|
|
14
12
|
if File.exist?(directory)
|
|
15
13
|
if !File.directory?(directory)
|
|
@@ -22,5 +20,14 @@ module Aigency
|
|
|
22
20
|
FileUtils.mkdir(directory) unless File.directory?(directory)
|
|
23
21
|
FileUtils.copy_entry(File.join(GEM_PATH, 'scaffold'), directory)
|
|
24
22
|
end
|
|
23
|
+
|
|
24
|
+
# desc "setup PLUGIN [...PLUGIN]", "Initialize plugin files"
|
|
25
|
+
# option :all, type: :boolean, alias: :a, description: "Set up all plugins", default: false
|
|
26
|
+
# option :overwrite, type: :boolean, alias: :o, description: "Overwrite existing files", default: false
|
|
27
|
+
# def setup *plugins
|
|
28
|
+
# # @todo Assuming all for now
|
|
29
|
+
# aigent = Aigent.new(aigent)
|
|
30
|
+
# aigent.setup
|
|
31
|
+
# end
|
|
25
32
|
end
|
|
26
33
|
end
|
data/lib/aigency/version.rb
CHANGED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aigency
|
|
4
|
+
module Workspace
|
|
5
|
+
class Grep < Shell
|
|
6
|
+
desc 'Run the `grep` shell command in your workspace'
|
|
7
|
+
|
|
8
|
+
def command = 'grep'
|
|
9
|
+
|
|
10
|
+
def non_path_arguments = %w[PATTERN]
|
|
11
|
+
|
|
12
|
+
def prohibited_arguments = %w[-f --file]
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -3,21 +3,58 @@
|
|
|
3
3
|
module Aigency
|
|
4
4
|
module Workspace
|
|
5
5
|
module Helpers
|
|
6
|
+
module_function
|
|
7
|
+
|
|
6
8
|
# @!method aigent
|
|
7
9
|
# @abstract
|
|
8
10
|
# @return [Aigency::Aigent]
|
|
9
11
|
|
|
10
|
-
# Expand a path relative to the aigent's workspace
|
|
12
|
+
# Expand a path relative to the aigent's workspace.
|
|
11
13
|
#
|
|
12
14
|
# @raise [PathError] if the expanded path is not inside the workspace
|
|
13
15
|
# @param path [String]
|
|
14
16
|
# @return [String]
|
|
15
|
-
def
|
|
17
|
+
def expand_in_workspace(path)
|
|
16
18
|
workspace = aigent.data('workspace')
|
|
17
19
|
resolved = File.expand_path(path, workspace)
|
|
18
20
|
return resolved if resolved == workspace || resolved.start_with?("#{workspace}/")
|
|
21
|
+
raise PathError, "Permission denied: `#{path}` is not relative to your workspace"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @raise [PathError] if the expanded path is not inside the workspace
|
|
25
|
+
# @param arguments [Array<String>]
|
|
26
|
+
# @param start [Integer]
|
|
27
|
+
# @return [void]
|
|
28
|
+
def validate_path_arguments(arguments, start = 0)
|
|
29
|
+
current = 0
|
|
30
|
+
glued_shorts = arguments.select { |arg| arg =~ /^-[^\-].+/ }
|
|
31
|
+
unless glued_shorts.empty?
|
|
32
|
+
raise PathError, "Permission denied: glued short options (#{glued_shorts.inspect}) are not allowed"
|
|
33
|
+
end
|
|
34
|
+
workspace = aigent.data('workspace')
|
|
35
|
+
path_arguments(arguments).each do |arg|
|
|
36
|
+
if !arg.start_with?('-')
|
|
37
|
+
current += 1
|
|
38
|
+
next unless current > start
|
|
39
|
+
end
|
|
40
|
+
path = split_path_part(arg)
|
|
41
|
+
resolved = File.expand_path(path, workspace)
|
|
42
|
+
next if resolved == workspace || resolved.start_with?("#{workspace}/")
|
|
43
|
+
raise PathError, "Permission denied: `#{path}` is not relative to your workspace"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def path_arguments(arguments)
|
|
48
|
+
arguments.reject { |arg| arg =~ /^-[^\-]$/ || (arg.start_with?('--') && !arg.include?('=')) }
|
|
49
|
+
end
|
|
19
50
|
|
|
20
|
-
|
|
51
|
+
def split_path_part argument
|
|
52
|
+
return argument unless argument.start_with?('-')
|
|
53
|
+
if argument.start_with?('--')
|
|
54
|
+
argument.split('=', 2).last
|
|
55
|
+
else
|
|
56
|
+
argument[2..]
|
|
57
|
+
end
|
|
21
58
|
end
|
|
22
59
|
end
|
|
23
60
|
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'open3'
|
|
4
|
+
|
|
5
|
+
module Aigency
|
|
6
|
+
module Workspace
|
|
7
|
+
class Shell < Tool
|
|
8
|
+
include Helpers
|
|
9
|
+
|
|
10
|
+
# @abstract
|
|
11
|
+
# @return [String]
|
|
12
|
+
def command
|
|
13
|
+
raise 'Not implemented'
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def non_path_arguments = []
|
|
17
|
+
|
|
18
|
+
def prohibited_arguments = []
|
|
19
|
+
|
|
20
|
+
def execute(directory: '.', arguments: [])
|
|
21
|
+
aigent.log.puts "[Workspace::Shell] #{command} #{arguments.inspect}"
|
|
22
|
+
if any_prohibited?(arguments)
|
|
23
|
+
return { success: false, error: "Permission denied: the following arguments to `#{command}` are prohibited: #{prohibited_arguments.inspect}" }
|
|
24
|
+
end
|
|
25
|
+
resolved = expand_in_workspace(directory)
|
|
26
|
+
if arguments != ['--help'] && arguments[0, non_path_arguments.length].any? { |arg| arg.start_with?('-') }
|
|
27
|
+
return { success: false, error: non_path_argument_error }
|
|
28
|
+
end
|
|
29
|
+
validate_path_arguments(arguments, non_path_arguments.length)
|
|
30
|
+
output, error, result = Open3.capture3(command, *arguments, chdir: resolved)
|
|
31
|
+
{ success: result.success?, stdout: output, stderr: error }
|
|
32
|
+
rescue StandardError => e
|
|
33
|
+
{ success: false, error: "[#{e.class}] #{e.message}" }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.inherited other
|
|
37
|
+
other.params do
|
|
38
|
+
string :directory, description: "The working directory relative to workspace", required: false
|
|
39
|
+
array :arguments, of: :string, description: "Command arguments", required: false
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def non_path_argument_error
|
|
46
|
+
arg_count = if non_path_arguments.length > 1
|
|
47
|
+
"#{non_path_arguments.length} arguments"
|
|
48
|
+
else
|
|
49
|
+
'argument'
|
|
50
|
+
end
|
|
51
|
+
"Permission denied: first #{arg_count} must be [#{non_path_arguments.join(', ')}] unless the only argument is `--help`"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def any_prohibited?(arguments)
|
|
55
|
+
arguments.any? { |arg| arg.start_with?(*prohibited_arguments) }
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'open3'
|
|
4
|
+
|
|
5
|
+
module Aigency
|
|
6
|
+
module Workspace
|
|
7
|
+
# @note Unlike most of the Workspace tools, this one does not execute a shell
|
|
8
|
+
# command. It uses ruby's File module instead.
|
|
9
|
+
#
|
|
10
|
+
class Write < Tool
|
|
11
|
+
include Helpers
|
|
12
|
+
|
|
13
|
+
desc 'Write or append to a file in the workspace'
|
|
14
|
+
|
|
15
|
+
params do
|
|
16
|
+
string :filename, description: "The filename relative to the workspace", required: true
|
|
17
|
+
string :content, description: "The data to write to the file", required: true
|
|
18
|
+
string :append, description: "Append if true or overwrite if false", required: true
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def execute(filename:, content:, append:)
|
|
22
|
+
aigent.log.puts "[Workspace::Write] #{filename}"
|
|
23
|
+
resolved = expand_in_workspace(filename)
|
|
24
|
+
write_mode = if append
|
|
25
|
+
"a"
|
|
26
|
+
else
|
|
27
|
+
"w"
|
|
28
|
+
end
|
|
29
|
+
File.write resolved, content, mode: "#{write_mode}b"
|
|
30
|
+
{ success: true, filename: filename }
|
|
31
|
+
rescue StandardError => e
|
|
32
|
+
{ success: false, error: "[#{e.class}] #{e.message}" }
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
data/lib/aigency/workspace.rb
CHANGED
|
@@ -2,14 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
module Aigency
|
|
4
4
|
module Workspace
|
|
5
|
+
class PathError < ArgumentError; end
|
|
6
|
+
|
|
5
7
|
require 'aigency/workspace/helpers'
|
|
6
|
-
require 'aigency/workspace/
|
|
7
|
-
require 'aigency/workspace/
|
|
8
|
-
require 'aigency/workspace/
|
|
9
|
-
require 'aigency/workspace/
|
|
8
|
+
require 'aigency/workspace/shell'
|
|
9
|
+
require 'aigency/workspace/cat'
|
|
10
|
+
require 'aigency/workspace/cp'
|
|
11
|
+
require 'aigency/workspace/diff'
|
|
12
|
+
require 'aigency/workspace/grep'
|
|
13
|
+
require 'aigency/workspace/ls'
|
|
14
|
+
require 'aigency/workspace/mkdir'
|
|
15
|
+
require 'aigency/workspace/mv'
|
|
16
|
+
require 'aigency/workspace/rm'
|
|
17
|
+
require 'aigency/workspace/wc'
|
|
18
|
+
require 'aigency/workspace/write'
|
|
10
19
|
|
|
11
|
-
|
|
20
|
+
Plugin.register Plugin.new(
|
|
21
|
+
'workspace',
|
|
22
|
+
directories: ['data/workspace'],
|
|
23
|
+
tools: [Cat, Cp, Diff, Grep, Ls, Mkdir, Mv, Rm, Wc, Write],
|
|
24
|
+
config: <<~FILE
|
|
25
|
+
prompt: "The workspace tools provide access to a readable and writeable directory."
|
|
26
|
+
FILE
|
|
27
|
+
)
|
|
12
28
|
end
|
|
13
|
-
|
|
14
|
-
Tools.mkdir_p 'data/workspace'
|
|
15
29
|
end
|
data/lib/aigency.rb
CHANGED
|
@@ -6,12 +6,13 @@ require 'ruby_llm'
|
|
|
6
6
|
require 'aigency/version'
|
|
7
7
|
require 'aigency/usability'
|
|
8
8
|
require 'aigency/tool'
|
|
9
|
-
require 'aigency/tools'
|
|
10
9
|
require 'aigency/aigent'
|
|
10
|
+
require 'aigency/plugin'
|
|
11
11
|
require 'aigency/shell'
|
|
12
12
|
|
|
13
13
|
# Extensions
|
|
14
14
|
require 'aigency/workspace'
|
|
15
|
+
require 'aigency/sandbox'
|
|
15
16
|
|
|
16
17
|
module Aigency
|
|
17
18
|
GEM_PATH = File.absolute_path(File.join(__dir__, '..'))
|
data/scaffold/Gemfile
CHANGED
data/scaffold/README.md
CHANGED
|
@@ -4,27 +4,23 @@ An aigent (AI agent) is an intelligent assistant powered by language models thro
|
|
|
4
4
|
|
|
5
5
|
## Configuration
|
|
6
6
|
|
|
7
|
-
### Install dependencies
|
|
7
|
+
### 1. Install dependencies
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
10
|
bundle install
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
###
|
|
13
|
+
### 2. Configure OpenAI API Connection
|
|
14
|
+
|
|
15
|
+
Copy the example env file:
|
|
14
16
|
|
|
15
17
|
```bash
|
|
16
|
-
|
|
18
|
+
cp .env.example .env
|
|
17
19
|
```
|
|
18
20
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
Configure the aigent's settings in `config/aigent.yml`.
|
|
22
|
-
|
|
23
|
-
#### OpenAI-Compatible Providers
|
|
24
|
-
|
|
25
|
-
If you're using a local AI server or any other provider that is compatible with OpenAI, you can set the OPENAI_API_BASE config option to point there instead (e.g., `http://localhost:8080/v1`).
|
|
21
|
+
Modify the `OPENAI_API_BASE` and `OPENAI_API_KEY` as needed. If you're using a local AI server or any other provider that is compatible with OpenAI, you can set the OPENAI_API_BASE config option to point there instead (e.g., `http://localhost:8080/v1`).
|
|
26
22
|
|
|
27
|
-
###
|
|
23
|
+
### 3. Define the Aigent's Personality
|
|
28
24
|
|
|
29
25
|
Edit `IDENTITY.md` to define how your aigent should behave:
|
|
30
26
|
|
|
@@ -34,6 +30,22 @@ You are a professional assistant named Alex. You respond in a friendly but conci
|
|
|
34
30
|
|
|
35
31
|
This file sets the foundation for all the aigent's responses.
|
|
36
32
|
|
|
33
|
+
### 4. Configure Plugins
|
|
34
|
+
|
|
35
|
+
Aigency ships with two plugins: `workspace` and `sandbox`. The `workspace` plugin is enabled by default. It's recommended that you use one or the other, but not both.
|
|
36
|
+
|
|
37
|
+
The `workspace` plugin gives your aigent a directory where it can manage its own files.
|
|
38
|
+
|
|
39
|
+
The `sandbox` plugin provides a container where the aigent can not only manage files, but also run commands and scripts.
|
|
40
|
+
|
|
41
|
+
The `workspace` plugin is sufficient for many aigents. Switch to `sandbox` if you need your aigent to perform complex tasks.
|
|
42
|
+
|
|
43
|
+
### 5. Run setup
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
rake setup
|
|
47
|
+
```
|
|
48
|
+
|
|
37
49
|
## Usage
|
|
38
50
|
|
|
39
51
|
### Interactive Mode
|
|
@@ -41,16 +53,12 @@ This file sets the foundation for all the aigent's responses.
|
|
|
41
53
|
Talk to your aigent using the Rake task:
|
|
42
54
|
|
|
43
55
|
```bash
|
|
44
|
-
rake say["
|
|
56
|
+
rake say["How many fibers are intertwined in a shredded wheat biscuit?"]
|
|
45
57
|
```
|
|
46
58
|
|
|
47
|
-
### The Workspace
|
|
48
|
-
|
|
49
|
-
Your aigent has read/write access to everything in the `data/workspace` directory.
|
|
50
|
-
|
|
51
59
|
### Scheduling Tasks
|
|
52
60
|
|
|
53
|
-
|
|
61
|
+
Aigents use the `whenever` gem for scheduling. Example of a scheduled task:
|
|
54
62
|
|
|
55
63
|
```ruby
|
|
56
64
|
# config/schedule.rb
|
|
@@ -69,6 +77,10 @@ bundle exec whenever --update-crontab
|
|
|
69
77
|
|
|
70
78
|
(Note: the `whenever` gem does not work on Windows.)
|
|
71
79
|
|
|
72
|
-
|
|
80
|
+
### Using the Sandbox
|
|
81
|
+
|
|
82
|
+
If you enable the `sandbox` plugin, you'll need to start the container:
|
|
73
83
|
|
|
74
|
-
|
|
84
|
+
```bash
|
|
85
|
+
docker compose up -d
|
|
86
|
+
```
|
data/scaffold/Rakefile
CHANGED
data/scaffold/config/aigent.yml
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
name:
|
|
2
|
-
openai_api_base:
|
|
3
|
-
openai_api_key:
|
|
1
|
+
name: Aigent
|
|
2
|
+
openai_api_base: ${OPENAI_API_BASE}
|
|
3
|
+
openai_api_key: ${OPENAI_API_KEY}
|
|
4
4
|
# max_tokens: 65536
|
|
5
|
-
|
|
6
|
-
#
|
|
5
|
+
plugins:
|
|
6
|
+
# You should only need either a workspace or a sandbox, not both. If you
|
|
7
|
+
# enable one, it's recommended that you disable the other.
|
|
8
|
+
# - sandbox
|
|
9
|
+
- workspace
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: aigency
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Fred Snyder
|
|
@@ -9,6 +9,20 @@ bindir: exe
|
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: dotenv
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '3.2'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '3.2'
|
|
12
26
|
- !ruby/object:Gem::Dependency
|
|
13
27
|
name: fileutils
|
|
14
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -51,6 +65,48 @@ dependencies:
|
|
|
51
65
|
- - "~>"
|
|
52
66
|
- !ruby/object:Gem::Version
|
|
53
67
|
version: '1.5'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: rake
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '13.0'
|
|
75
|
+
type: :runtime
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '13.0'
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: rspec
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - "~>"
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '3.0'
|
|
89
|
+
type: :development
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - "~>"
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '3.0'
|
|
96
|
+
- !ruby/object:Gem::Dependency
|
|
97
|
+
name: simplecov
|
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
|
99
|
+
requirements:
|
|
100
|
+
- - "~>"
|
|
101
|
+
- !ruby/object:Gem::Version
|
|
102
|
+
version: '0.21'
|
|
103
|
+
type: :development
|
|
104
|
+
prerelease: false
|
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
106
|
+
requirements:
|
|
107
|
+
- - "~>"
|
|
108
|
+
- !ruby/object:Gem::Version
|
|
109
|
+
version: '0.21'
|
|
54
110
|
email:
|
|
55
111
|
- fsnyder@castwide.com
|
|
56
112
|
executables:
|
|
@@ -64,18 +120,32 @@ files:
|
|
|
64
120
|
- exe/aigency
|
|
65
121
|
- lib/aigency.rb
|
|
66
122
|
- lib/aigency/aigent.rb
|
|
123
|
+
- lib/aigency/plugin.rb
|
|
124
|
+
- lib/aigency/sandbox.rb
|
|
125
|
+
- lib/aigency/sandbox/Dockerfile.tpl
|
|
126
|
+
- lib/aigency/sandbox/docker-compose.yml.tpl
|
|
127
|
+
- lib/aigency/sandbox/exec.rb
|
|
128
|
+
- lib/aigency/sandbox/squid.conf.tpl
|
|
67
129
|
- lib/aigency/shell.rb
|
|
68
130
|
- lib/aigency/tool.rb
|
|
69
|
-
- lib/aigency/tools.rb
|
|
70
131
|
- lib/aigency/usability.rb
|
|
71
132
|
- lib/aigency/version.rb
|
|
72
133
|
- lib/aigency/workspace.rb
|
|
73
|
-
- lib/aigency/workspace/
|
|
74
|
-
- lib/aigency/workspace/
|
|
75
|
-
- lib/aigency/workspace/
|
|
76
|
-
- lib/aigency/workspace/
|
|
77
|
-
- lib/aigency/workspace/
|
|
134
|
+
- lib/aigency/workspace/cat.rb
|
|
135
|
+
- lib/aigency/workspace/cp.rb
|
|
136
|
+
- lib/aigency/workspace/diff.rb
|
|
137
|
+
- lib/aigency/workspace/grep.rb
|
|
138
|
+
- lib/aigency/workspace/head.rb
|
|
78
139
|
- lib/aigency/workspace/helpers.rb
|
|
140
|
+
- lib/aigency/workspace/ls.rb
|
|
141
|
+
- lib/aigency/workspace/mkdir.rb
|
|
142
|
+
- lib/aigency/workspace/mv.rb
|
|
143
|
+
- lib/aigency/workspace/rm.rb
|
|
144
|
+
- lib/aigency/workspace/shell.rb
|
|
145
|
+
- lib/aigency/workspace/tail.rb
|
|
146
|
+
- lib/aigency/workspace/wc.rb
|
|
147
|
+
- lib/aigency/workspace/write.rb
|
|
148
|
+
- scaffold/.env.example
|
|
79
149
|
- scaffold/.gitignore
|
|
80
150
|
- scaffold/Gemfile
|
|
81
151
|
- scaffold/IDENTITY.md
|
data/lib/aigency/tools.rb
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Aigency
|
|
4
|
-
module Tools
|
|
5
|
-
# @return [Array<Class<Tool>>]
|
|
6
|
-
def self.registry
|
|
7
|
-
@registry ||= []
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
# @return [Hash{String => String}]
|
|
11
|
-
def self.config_map
|
|
12
|
-
@configs ||= {}
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
# @return [Array<String>]
|
|
16
|
-
def self.directories
|
|
17
|
-
@directories ||= []
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
# Register a tool.
|
|
21
|
-
#
|
|
22
|
-
# @param klass [Class<Tool>]
|
|
23
|
-
# @return [void]
|
|
24
|
-
def self.register klass
|
|
25
|
-
registry.push klass
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
# Define a default configuration.
|
|
29
|
-
#
|
|
30
|
-
# @param name [String] The name of the config section
|
|
31
|
-
# @param text [String] YAML-formatted config data
|
|
32
|
-
# @return [void]
|
|
33
|
-
def self.configure name, text
|
|
34
|
-
config_map[name] = text
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
# Define directories to be created relative to an aigent's root.
|
|
38
|
-
#
|
|
39
|
-
def self.mkdir_p *paths
|
|
40
|
-
directories.concat paths
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
Tools.configure 'aigent', <<~EOF
|
|
45
|
-
name: an Aigent
|
|
46
|
-
openai_api_base: https://api.openai.com/v1
|
|
47
|
-
openai_api_key: CHANGE_ME
|
|
48
|
-
require:
|
|
49
|
-
- aigency/email
|
|
50
|
-
- aigency/wikipedia
|
|
51
|
-
- aigency/workspace
|
|
52
|
-
EOF
|
|
53
|
-
|
|
54
|
-
Tools.mkdir_p 'data/chats', 'data/log'
|
|
55
|
-
end
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Aigency
|
|
4
|
-
module Workspace
|
|
5
|
-
class DirectoryDeleter < Tool
|
|
6
|
-
include Helpers
|
|
7
|
-
|
|
8
|
-
desc "Delete a directory and all of its contents from your private workspace"
|
|
9
|
-
|
|
10
|
-
def execute(directory_name:)
|
|
11
|
-
aigent.log.puts "[Workspace::DirectoryDeleter] #{directory_name}"
|
|
12
|
-
|
|
13
|
-
resolved = expand_to_workspace(directory_name)
|
|
14
|
-
FileUtils.rm_rf(resolved)
|
|
15
|
-
{ dirname: directory_name, success: true }
|
|
16
|
-
rescue StandardError => e
|
|
17
|
-
{ success: false, error: "[#{e.class}] #{e.message}"}
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
Aigency::Tools.register Aigency::Workspace::DirectoryDeleter
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Aigency
|
|
4
|
-
module Workspace
|
|
5
|
-
class DirectoryReader < Tool
|
|
6
|
-
include Helpers
|
|
7
|
-
|
|
8
|
-
desc "Get the contents of a directory from your private workspace"
|
|
9
|
-
|
|
10
|
-
def execute(directory_name:)
|
|
11
|
-
aigent.log.puts "[Workspace::DirectoryReader] Reading #{directory_name}"
|
|
12
|
-
resolved = expand_to_workspace(directory_name)
|
|
13
|
-
if File.directory?(resolved)
|
|
14
|
-
{
|
|
15
|
-
directory: directory_name,
|
|
16
|
-
success: true,
|
|
17
|
-
entries: Dir.entries(resolved)[2..]
|
|
18
|
-
}
|
|
19
|
-
else
|
|
20
|
-
{
|
|
21
|
-
directory: directory_name,
|
|
22
|
-
success: false,
|
|
23
|
-
error: "Not a directory"
|
|
24
|
-
}
|
|
25
|
-
end
|
|
26
|
-
rescue StandardError => e
|
|
27
|
-
{ success: false, error: "[#{e.class}] #{e.message}" }
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
Aigency::Tools.register Aigency::Workspace::DirectoryReader
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Aigency
|
|
4
|
-
module Workspace
|
|
5
|
-
class FileDeleter < Tool
|
|
6
|
-
include Helpers
|
|
7
|
-
|
|
8
|
-
desc "Delete a file from your private workspace"
|
|
9
|
-
|
|
10
|
-
def execute(filename:)
|
|
11
|
-
aigent.log.puts "[Workspace::FileDeleter] Deleting file #{filename}"
|
|
12
|
-
resolved = expand_to_workspace(filename)
|
|
13
|
-
File.delete resolved
|
|
14
|
-
{ filename: filename, success: true }
|
|
15
|
-
rescue StandardError => e
|
|
16
|
-
{ success: false, error: "[#{e.class}] #{e.message}" }
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
Aigency::Tools.register Aigency::Workspace::FileDeleter
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Aigency
|
|
4
|
-
module Workspace
|
|
5
|
-
class FileReader < Tool
|
|
6
|
-
include Helpers
|
|
7
|
-
|
|
8
|
-
desc "Read a file from your private workspace"
|
|
9
|
-
|
|
10
|
-
def execute(filename:)
|
|
11
|
-
aigent.log.puts "[Workspace::FileReader] Reading #{filename}"
|
|
12
|
-
|
|
13
|
-
resolved = expand_to_workspace(filename)
|
|
14
|
-
|
|
15
|
-
{
|
|
16
|
-
success: true,
|
|
17
|
-
filename: filename,
|
|
18
|
-
content: File.read(resolved)
|
|
19
|
-
}
|
|
20
|
-
rescue StandardError => e
|
|
21
|
-
{ success: false, error: "[#{e.class}] #{e.message}"}
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
Aigency::Tools.register Aigency::Workspace::FileReader
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'fileutils'
|
|
4
|
-
|
|
5
|
-
module Aigency
|
|
6
|
-
module Workspace
|
|
7
|
-
class FileWriter < Tool
|
|
8
|
-
include Helpers
|
|
9
|
-
|
|
10
|
-
desc "Write a file to your private workspace"
|
|
11
|
-
|
|
12
|
-
def execute(filename:, content:)
|
|
13
|
-
aigent.log.puts "[Workspace::FileWriter] Writing #{filename}"
|
|
14
|
-
|
|
15
|
-
resolved = expand_to_workspace(filename)
|
|
16
|
-
FileUtils.mkdir_p File.dirname(resolved)
|
|
17
|
-
File.write resolved, content
|
|
18
|
-
return { filename: filename, success: true }
|
|
19
|
-
rescue StandardError => e
|
|
20
|
-
{ success: false, error: "[#{e.class}] #{e.message}" }
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
Aigency::Tools.register Aigency::Workspace::FileWriter
|