aigency 0.2.0 → 0.4.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 +31 -0
- data/README.md +12 -13
- data/lib/aigency/aigent/current_time.rb +33 -0
- data/lib/aigency/aigent/shell.rb +108 -0
- data/lib/aigency/aigent.rb +117 -65
- data/lib/aigency/delegator.rb +11 -0
- data/lib/aigency/plugin/shell.rb +11 -0
- data/lib/aigency/plugin.rb +39 -0
- data/lib/aigency/rake.rb +13 -0
- data/lib/aigency/sandbox/exec.rb +21 -0
- data/lib/aigency/sandbox.rb +22 -0
- data/lib/aigency/shell.rb +5 -5
- data/lib/aigency/tempdir/helpers.rb +15 -0
- data/lib/aigency/tempdir/list.rb +17 -0
- data/lib/aigency/tempdir/read.rb +32 -0
- data/lib/aigency/tempdir/size.rb +27 -0
- data/lib/aigency/tempdir/write.rb +28 -0
- data/lib/aigency/tempdir.rb +19 -0
- data/lib/aigency/tool.rb +5 -0
- data/lib/aigency/usability.rb +1 -0
- data/lib/aigency/version.rb +1 -1
- data/lib/aigency/workspace/cat.rb +11 -0
- data/lib/aigency/workspace/cmd.rb +59 -0
- data/lib/aigency/workspace/copy_tempdir_file.rb +28 -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/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 +20 -7
- data/lib/aigency.rb +16 -13
- data/scaffold/{Rakefile → aigent/aigent.rake} +2 -11
- data/scaffold/enlist/.env.example +4 -0
- data/scaffold/{.gitignore → enlist/.gitignore} +1 -0
- data/scaffold/{Gemfile → enlist/Gemfile} +1 -1
- data/scaffold/enlist/README.md +90 -0
- data/scaffold/enlist/Rakefile +9 -0
- data/scaffold/enlist/bin/aigent +11 -0
- data/scaffold/{config → enlist/config}/schedule.rb +5 -0
- data/scaffold/sandbox/Dockerfile +37 -0
- data/scaffold/sandbox/docker-compose.yml +37 -0
- data/scaffold/sandbox/squid.conf +25 -0
- data/scaffold/sandbox/ssh_config +2 -0
- metadata +114 -16
- 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
- data/scaffold/README.md +0 -74
- data/scaffold/config/aigent.yml +0 -6
- /data/scaffold/{IDENTITY.md → enlist/IDENTITY.md} +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4fce24d7da34c83fac5e0d3ce82d8bb79c31bede2b99ddfacec3b7efa4d157e3
|
|
4
|
+
data.tar.gz: 1a36424698a8a9b4bba0a0a0cf6a14e9b4cf8318a40a166893be3e093c909f9e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9ce7989e062eb10afbb767d3e61f297ddf521f0b7c5fe0459c8021428843e6b05069db1ae88afbda7c29ab4ea0ca9f9f277b7472393f085f87dc5b36866cd786
|
|
7
|
+
data.tar.gz: 9a7a1efaad6467200964715b3db7920b52cc1cc39c7292fd8a52a2b4d4d023b16502ea511ee6824664970fecbdab19b2529dd95a2a5d0a8dfc49f9c53a5e062d
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,34 @@
|
|
|
1
|
+
## 0.4.0 - October 21, 2026
|
|
2
|
+
- Config file directory
|
|
3
|
+
- Add openssh to sandbox
|
|
4
|
+
- Sandbox configuration
|
|
5
|
+
- Tempdir tools
|
|
6
|
+
- Aigent plugin
|
|
7
|
+
- Proxy environment variables
|
|
8
|
+
- apk update && apk upgrade
|
|
9
|
+
- Plugins add rake files
|
|
10
|
+
- Aigent setup in README
|
|
11
|
+
- Rearrange sandbox scaffold
|
|
12
|
+
- Add bin/aigent to aigent directory
|
|
13
|
+
- Fix grep spec for BusyBox
|
|
14
|
+
- CurrentTime tool
|
|
15
|
+
- Dockerfile packages and mise setup
|
|
16
|
+
- Instructions subcommand
|
|
17
|
+
- Aigent#stream errors write to error.log
|
|
18
|
+
- Aigent stream command
|
|
19
|
+
- Remove adduser recommendation
|
|
20
|
+
- Clarify Docker requirement
|
|
21
|
+
- Plugin shells
|
|
22
|
+
- Tempdir file size management
|
|
23
|
+
- CopyTempdirFile tool
|
|
24
|
+
- Aigent plugin setup
|
|
25
|
+
- Update chat calls for RubyLLM 2.0
|
|
26
|
+
- Use the chat completions protocol
|
|
27
|
+
|
|
28
|
+
## 0.3.0 - October 9, 2026
|
|
29
|
+
- New plugin architecture
|
|
30
|
+
- Workspace and Sandbox plugins
|
|
31
|
+
|
|
1
32
|
## 0.2.0 - October 2, 2026
|
|
2
33
|
- Log token usage
|
|
3
34
|
- Stale code
|
data/README.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
A framework for creating AI agents with RubyLLM.
|
|
4
4
|
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- Ruby 3.2+
|
|
8
|
+
- Docker 29+ (required for sandbox)
|
|
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:
|
|
@@ -20,28 +27,20 @@ gem 'aigency'
|
|
|
20
27
|
|
|
21
28
|
### Creating a New Aigent
|
|
22
29
|
|
|
23
|
-
The recommended way to create an aigent is to give it a dedicated system user.
|
|
24
|
-
|
|
25
|
-
```bash
|
|
26
|
-
useradd aigent_user
|
|
27
|
-
sudo su - aigent_user
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
Once you're logged in as the aigent user, create the aigent directory:
|
|
31
|
-
|
|
32
30
|
```bash
|
|
33
31
|
aigency enlist
|
|
34
32
|
cd aigent
|
|
35
33
|
bundle install
|
|
36
34
|
rake setup
|
|
35
|
+
cp .env.example .env
|
|
37
36
|
```
|
|
38
37
|
|
|
39
|
-
The bare minimum configuration you need to do is setting `
|
|
38
|
+
The bare minimum configuration you need to do is setting `OPENAI_API_BASE` and `OPENAI_API_KEY` in `.env`.
|
|
40
39
|
|
|
41
|
-
Test the aigent with
|
|
40
|
+
Test the aigent with the `say` command.
|
|
42
41
|
|
|
43
42
|
```bash
|
|
44
|
-
|
|
43
|
+
bin/aigent say "How many fibers are intertwined in a shredded wheat biscuit?"
|
|
45
44
|
```
|
|
46
45
|
|
|
47
|
-
See the aigent's README (scaffold/README.md in this codebase)
|
|
46
|
+
See the aigent's README for more information (`README.md` in your aigent directory or `scaffold/enlist/README.md` in this codebase).
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'date'
|
|
4
|
+
|
|
5
|
+
module Aigency
|
|
6
|
+
class Aigent
|
|
7
|
+
class CurrentTime < Aigency::Tool
|
|
8
|
+
description 'Get the current time'
|
|
9
|
+
|
|
10
|
+
parameters do
|
|
11
|
+
any_of :format, description: "Date/time format" do
|
|
12
|
+
string enum: %w[iso8601 unix]
|
|
13
|
+
'iso8601'
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def execute(format: 'iso8601')
|
|
18
|
+
aigent.log.puts "[Aigent::CurrentTime] #{format}"
|
|
19
|
+
now = DateTime.now
|
|
20
|
+
case format
|
|
21
|
+
when 'iso8601'
|
|
22
|
+
{ success: true, current_time: now.iso8601 }
|
|
23
|
+
when 'unix'
|
|
24
|
+
{ success: true, current_time: now.to_time.to_i }
|
|
25
|
+
else
|
|
26
|
+
{ success: false, error: "Invalid format: #{format}" }
|
|
27
|
+
end
|
|
28
|
+
rescue StandardError => e
|
|
29
|
+
{ success: false, error: "[#{e.class}] #{e.message}"}
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'thor'
|
|
4
|
+
|
|
5
|
+
module Aigency
|
|
6
|
+
class Aigent
|
|
7
|
+
# Commands that can be executed in aigent directories.
|
|
8
|
+
#
|
|
9
|
+
class Shell < Thor
|
|
10
|
+
include Delegator
|
|
11
|
+
|
|
12
|
+
desc "say PROMPT", "Send a prompt to the aigent"
|
|
13
|
+
def say(prompt)
|
|
14
|
+
aigent.stream prompt
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
desc "load FILE", "Load a prompt from a file"
|
|
18
|
+
def load(file)
|
|
19
|
+
aigent.load filename
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
desc "setup", "Initialize aigent plugins"
|
|
23
|
+
def setup
|
|
24
|
+
aigent.setup
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
desc "tools", "Get a list of available tools"
|
|
28
|
+
def tools
|
|
29
|
+
puts aigent.tools.map(&:to_s).sort
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
desc "status", "Get the aigent's current status"
|
|
33
|
+
def status
|
|
34
|
+
puts "Name: #{aigent.config('aigent')['name']}"
|
|
35
|
+
puts "Busy: #{aigent.busy?}"
|
|
36
|
+
if aigent.busy?
|
|
37
|
+
puts "Process: #{File.read(aigent.data('lock'))}"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
desc "instructions", "List base instructions"
|
|
42
|
+
long_desc <<~DESC
|
|
43
|
+
Show all instructions that will be given to the aigent before the user
|
|
44
|
+
prompt. They typically include the IDENTITY.md file and any prompts injected
|
|
45
|
+
by installed plugins.
|
|
46
|
+
DESC
|
|
47
|
+
def instructions
|
|
48
|
+
puts aigent.instructions
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
desc "stream", "Tail the currently active chat stream"
|
|
52
|
+
option :follow, type: :boolean, aliases: :f, desc: "Follow the stream (tail -f)"
|
|
53
|
+
option :lines, type: :numeric, aliases: :n, desc: "Number of lines (tail -n)", default: 10
|
|
54
|
+
long_desc <<~DESC
|
|
55
|
+
This is useful for monitoring non-interactive chat streams, e.g., chat sessions
|
|
56
|
+
that were started by cron jobs or rake tasks. It runs `tail` on the current chat
|
|
57
|
+
file in the aigent's data/chats folder.
|
|
58
|
+
DESC
|
|
59
|
+
def stream
|
|
60
|
+
if aigent.busy?
|
|
61
|
+
file = Dir.glob(aigent.data('chats', '*.txt')).max { |file| File.mtime(file).to_i }
|
|
62
|
+
cmd = ['tail', file, '-n', options[:lines].to_s]
|
|
63
|
+
cmd.push '-f' if options[:follow]
|
|
64
|
+
puts "(#{file})"
|
|
65
|
+
exec *cmd
|
|
66
|
+
else
|
|
67
|
+
puts "No active chat stream."
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
desc "kill", "Kill a blocking process"
|
|
72
|
+
long_desc <<~DESC
|
|
73
|
+
Kill the process that owns an exclusive lock on the aigent's data/lock file.
|
|
74
|
+
DESC
|
|
75
|
+
def kill
|
|
76
|
+
if aigent.busy?
|
|
77
|
+
pid = File.read(aigent.data('lock')).to_i
|
|
78
|
+
puts "Killing process #{pid}"
|
|
79
|
+
Process.kill 'TERM', pid
|
|
80
|
+
else
|
|
81
|
+
puts "No blocking process exists."
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
class << self
|
|
86
|
+
def mounted? = false
|
|
87
|
+
|
|
88
|
+
def start(args = ARGV, config = {})
|
|
89
|
+
return super if mounted?
|
|
90
|
+
build('.').start(args, config)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def build(directory)
|
|
94
|
+
Class.new(self) do
|
|
95
|
+
def self.mounted? = true
|
|
96
|
+
Aigent.new(directory).plugins.each do |plugin|
|
|
97
|
+
next unless plugin.shell
|
|
98
|
+
if all_commands.key?(plugin.name)
|
|
99
|
+
raise "Plugin '#{plugin.name}' cannot register CLI command '#{plugin.name}': already taken"
|
|
100
|
+
end
|
|
101
|
+
register(plugin.shell, plugin.name, "#{plugin.name} [SUBCOMMAND ...ARGS]", "#{plugin.name} plugin commands")
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
data/lib/aigency/aigent.rb
CHANGED
|
@@ -2,16 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
require 'fileutils'
|
|
4
4
|
require 'yaml'
|
|
5
|
+
require 'set'
|
|
5
6
|
|
|
6
7
|
module Aigency
|
|
7
8
|
class Aigent
|
|
9
|
+
require 'aigency/aigent/current_time'
|
|
10
|
+
require 'aigency/aigent/shell'
|
|
11
|
+
|
|
8
12
|
# @return [IO]
|
|
9
13
|
attr_reader :log
|
|
10
14
|
|
|
11
15
|
def initialize(directory)
|
|
12
16
|
@base_directory = File.absolute_path(directory)
|
|
13
17
|
@log = $stdout
|
|
14
|
-
load_requires
|
|
15
18
|
end
|
|
16
19
|
|
|
17
20
|
# @return [String]
|
|
@@ -38,30 +41,49 @@ module Aigency
|
|
|
38
41
|
# Initialize a RubyLLM::Chat.
|
|
39
42
|
#
|
|
40
43
|
# @param extra [Array<String>] Additional instructions
|
|
41
|
-
# @param
|
|
44
|
+
# @param except [Array<Class<Tool>>] Tools to exclude from this chat
|
|
42
45
|
# @return [RubyLLM::Chat]
|
|
43
|
-
def brief(*extra,
|
|
44
|
-
chat = rubyllm_context.chat(provider: :openai)
|
|
45
|
-
tools.each { |klass| chat.
|
|
46
|
-
chat.with_instructions(build_instructions(extra), append: true)
|
|
47
|
-
chat.
|
|
46
|
+
def brief(*extra, except: [])
|
|
47
|
+
chat = rubyllm_context.chat(provider: :openai, protocol: :chat_completions)
|
|
48
|
+
(tools - except).each { |klass| chat.with_tools(klass.new(self)) }
|
|
49
|
+
chat.with_instructions(build_instructions(extra).join, append: true)
|
|
50
|
+
chat.with_provider_options(max_tokens: max_tokens)
|
|
48
51
|
end
|
|
49
52
|
|
|
50
|
-
|
|
51
|
-
|
|
53
|
+
# Prompt a chat with a file.
|
|
54
|
+
#
|
|
55
|
+
# @param filename [String]
|
|
56
|
+
# @param extra [Array<String>] Additional instructions
|
|
57
|
+
# @param except [Array<Class<Tool>>] Tools to exclude from this chat
|
|
58
|
+
# @param out [IO]
|
|
59
|
+
# @return [RubyLLM::Chat]
|
|
60
|
+
def load filename, *extra, except: [], out: $stdout
|
|
61
|
+
stream File.read(filename), *extra, except: except, out: out
|
|
52
62
|
end
|
|
53
63
|
|
|
54
|
-
|
|
64
|
+
# Prompt a chat.
|
|
65
|
+
#
|
|
66
|
+
# @param text [String]
|
|
67
|
+
# @param extra [Array<String>] Additional instructions
|
|
68
|
+
# @param except [Array<Class<Tool>>] Tools to exclude from this chat
|
|
69
|
+
# @param out [IO]
|
|
70
|
+
# @return [RubyLLM::Chat]
|
|
71
|
+
def stream text, *extra, attach: [], except: [], out: $stdout
|
|
55
72
|
original_log = @log
|
|
56
73
|
@log = out
|
|
57
|
-
response = brief(*extra,
|
|
58
|
-
@log.print chunk.thinking&.text
|
|
59
|
-
|
|
60
|
-
|
|
74
|
+
response = brief(*extra, except: except).ask text, with: attach #do |chunk|
|
|
75
|
+
# @log.print chunk.thinking&.text
|
|
76
|
+
#end
|
|
77
|
+
@log.puts response.content
|
|
61
78
|
@log.puts "Input tokens: #{response.tokens.input}"
|
|
62
79
|
@log.puts "Output tokens: #{response.tokens.output}"
|
|
63
|
-
@log = original_log
|
|
64
80
|
response
|
|
81
|
+
rescue StandardError => e
|
|
82
|
+
message = "[#{e.class}] #{e.message}\n" + e.backtrace.map { |line| " #{line}\n" }.join
|
|
83
|
+
@log.puts message
|
|
84
|
+
File.write data('log', 'error.log'), "#{DateTime.now.iso8601} #{message}", mode: 'a'
|
|
85
|
+
ensure
|
|
86
|
+
@log = original_log
|
|
65
87
|
end
|
|
66
88
|
|
|
67
89
|
# @param name [String]
|
|
@@ -70,43 +92,16 @@ module Aigency
|
|
|
70
92
|
configs[name] ||= read_config(name)
|
|
71
93
|
end
|
|
72
94
|
|
|
73
|
-
# Initialize the aigent's
|
|
95
|
+
# Initialize the aigent's plugin configurations.
|
|
74
96
|
#
|
|
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."
|
|
97
|
+
# @return [void]
|
|
98
|
+
def setup
|
|
99
|
+
plugins.each { |plugin| boot plugin }
|
|
105
100
|
end
|
|
106
101
|
|
|
107
102
|
# Run code in an exclusive lock
|
|
108
103
|
#
|
|
109
|
-
def
|
|
104
|
+
def block(&block)
|
|
110
105
|
File.open(data('lock'), 'w') do |file|
|
|
111
106
|
file.flock(File::LOCK_EX)
|
|
112
107
|
file.write(Process.pid)
|
|
@@ -114,6 +109,8 @@ module Aigency
|
|
|
114
109
|
block&.call
|
|
115
110
|
end
|
|
116
111
|
end
|
|
112
|
+
alias lock block
|
|
113
|
+
alias flock block
|
|
117
114
|
|
|
118
115
|
# Run code if an exclusive lock is available or skip if a lock exists
|
|
119
116
|
#
|
|
@@ -138,12 +135,56 @@ module Aigency
|
|
|
138
135
|
end
|
|
139
136
|
alias busy? locked?
|
|
140
137
|
|
|
138
|
+
# @return [String]
|
|
141
139
|
def instructions
|
|
142
|
-
@instructions ||= build_instructions([])
|
|
140
|
+
@instructions ||= build_instructions([]).join("\n\n")
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# @return [Array<Plugin>]
|
|
144
|
+
def plugins
|
|
145
|
+
config('aigent').fetch('plugins', [])
|
|
146
|
+
.unshift('aigent')
|
|
147
|
+
.uniq
|
|
148
|
+
.map { |name| Plugin.registry[name] }
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def tools
|
|
152
|
+
plugins.flat_map(&:tools).uniq
|
|
143
153
|
end
|
|
144
154
|
|
|
145
155
|
private
|
|
146
156
|
|
|
157
|
+
# @param plugin [Plugin]
|
|
158
|
+
def boot(plugin)
|
|
159
|
+
plugin.directories.each do |dir|
|
|
160
|
+
absolute = File.absolute_path(dir, directory)
|
|
161
|
+
if File.directory?(absolute)
|
|
162
|
+
log.puts "Directory #{dir} exists, skipping"
|
|
163
|
+
else
|
|
164
|
+
log.puts "Creating #{dir}"
|
|
165
|
+
FileUtils.mkdir_p absolute
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
unless plugin.config.empty?
|
|
169
|
+
config_file = File.absolute_path(File.join('config', "#{plugin.name}.yml"), directory)
|
|
170
|
+
if File.exist?(config_file)
|
|
171
|
+
log.puts "File config/#{plugin.name}.yml exists, skipping"
|
|
172
|
+
else
|
|
173
|
+
log.puts "Creating config/#{plugin.name}.yml"
|
|
174
|
+
File.write config_file, plugin.config
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
plugin.files.each do |file, content|
|
|
178
|
+
absolute = File.absolute_path(file, directory)
|
|
179
|
+
if File.exist?(absolute)
|
|
180
|
+
log.puts "File #{file} exists, skipping"
|
|
181
|
+
else
|
|
182
|
+
log.puts "Creating #{file}"
|
|
183
|
+
File.write absolute, content
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
147
188
|
def configs
|
|
148
189
|
@configs ||= {}
|
|
149
190
|
end
|
|
@@ -157,7 +198,9 @@ module Aigency
|
|
|
157
198
|
end
|
|
158
199
|
full_path = File.join(directory, 'config', filename)
|
|
159
200
|
if File.file?(full_path)
|
|
160
|
-
|
|
201
|
+
content = File.read(full_path)
|
|
202
|
+
.gsub(/\${([^}]+)}/) { ENV.fetch($1) }
|
|
203
|
+
YAML.load(content)
|
|
161
204
|
else
|
|
162
205
|
warn "Config name #{name} not found at #{full_path}"
|
|
163
206
|
{}
|
|
@@ -176,26 +219,17 @@ module Aigency
|
|
|
176
219
|
@base_instructions ||= identity_file? ? File.read(identity_file) : "You are a helpful AI assistant."
|
|
177
220
|
end
|
|
178
221
|
|
|
222
|
+
# @return [Array<String>]
|
|
179
223
|
def build_instructions(extra)
|
|
180
|
-
[base_instructions].concat
|
|
181
|
-
.
|
|
182
|
-
.concat extra
|
|
183
|
-
.join("\n\n")
|
|
224
|
+
[base_instructions].concat(tool_prompts)
|
|
225
|
+
.concat(extra)
|
|
184
226
|
end
|
|
185
227
|
|
|
186
228
|
def tool_prompts
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
end
|
|
192
|
-
|
|
193
|
-
def load_requires
|
|
194
|
-
read_requires.each { |name| require name }
|
|
195
|
-
end
|
|
196
|
-
|
|
197
|
-
def read_requires
|
|
198
|
-
config('aigent')['require'] || []
|
|
229
|
+
# @todo Should aigents load prompts from config files that do not have
|
|
230
|
+
# a corresponding plugin?
|
|
231
|
+
plugins.map { |plugin| config(plugin.name)['prompt'] }
|
|
232
|
+
.compact
|
|
199
233
|
end
|
|
200
234
|
|
|
201
235
|
def max_tokens
|
|
@@ -208,5 +242,23 @@ module Aigency
|
|
|
208
242
|
config.openai_api_key = config('aigent')['openai_api_key']
|
|
209
243
|
end
|
|
210
244
|
end
|
|
245
|
+
|
|
246
|
+
Plugin.register Plugin.new(
|
|
247
|
+
'aigent',
|
|
248
|
+
tools: [CurrentTime],
|
|
249
|
+
directories: ['config', 'data/chats', 'data/log'],
|
|
250
|
+
rakefile: File.join(GEM_PATH, 'scaffold', 'aigent', 'aigent.rake'),
|
|
251
|
+
config: <<~FILE
|
|
252
|
+
name: Aigent
|
|
253
|
+
openai_api_base: ${OPENAI_API_BASE}
|
|
254
|
+
openai_api_key: ${OPENAI_API_KEY}
|
|
255
|
+
# max_tokens: 65536
|
|
256
|
+
plugins:
|
|
257
|
+
# You should only need either a workspace or a sandbox, not both. If you
|
|
258
|
+
# enable one, it's recommended that you disable the other.
|
|
259
|
+
# - sandbox
|
|
260
|
+
- workspace
|
|
261
|
+
FILE
|
|
262
|
+
)
|
|
211
263
|
end
|
|
212
264
|
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aigency
|
|
4
|
+
class Plugin
|
|
5
|
+
require 'aigency/plugin/shell'
|
|
6
|
+
|
|
7
|
+
attr_reader :name, :config, :tools, :directories, :files, :rakefile, :shell
|
|
8
|
+
|
|
9
|
+
# @param name [String]
|
|
10
|
+
# @param tools [Array<Class<Tool>>]
|
|
11
|
+
# @param config [String]
|
|
12
|
+
# @param directories [Array<String>]
|
|
13
|
+
# @param files [Hash{String => String}]
|
|
14
|
+
def initialize name, tools: [], config: '', directories: [], files: {}, rakefile: nil, shell: nil
|
|
15
|
+
raise "A plugin named '#{name}' already exists" if self.class.registry.key?(name)
|
|
16
|
+
@name = name
|
|
17
|
+
@config = config
|
|
18
|
+
@tools = tools
|
|
19
|
+
@directories = directories
|
|
20
|
+
@files = files
|
|
21
|
+
@rakefile = rakefile
|
|
22
|
+
@shell = shell
|
|
23
|
+
self.class.registry[name] = self
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.registry
|
|
27
|
+
@registry ||= {}
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @param plugin [Plugin]
|
|
31
|
+
def self.register plugin
|
|
32
|
+
registry[plugin.name] = plugin
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.create(...)
|
|
36
|
+
register new(...)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
data/lib/aigency/rake.rb
ADDED
|
@@ -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
|
+
description 'Run a shell command in your sandbox'
|
|
9
|
+
|
|
10
|
+
parameters 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,22 @@
|
|
|
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: ['sandbox', 'data/sandbox', 'data/sandbox_ssh'],
|
|
11
|
+
files: {
|
|
12
|
+
'sandbox/Dockerfile' => File.read(File.join(GEM_PATH, 'scaffold', 'sandbox', 'Dockerfile')),
|
|
13
|
+
'sandbox/squid.conf' => File.read(File.join(GEM_PATH, 'scaffold', 'sandbox', 'squid.conf')),
|
|
14
|
+
'docker-compose.yml' => File.read(File.join(GEM_PATH, 'scaffold', 'sandbox', 'docker-compose.yml')),
|
|
15
|
+
'data/sandbox_ssh/config' => File.read(File.join(GEM_PATH, 'scaffold', 'sandbox', 'ssh_config'))
|
|
16
|
+
},
|
|
17
|
+
config: <<~FILE
|
|
18
|
+
prompt: "The sandbox tools provide access to a Docker container."
|
|
19
|
+
FILE
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
end
|
data/lib/aigency/shell.rb
CHANGED
|
@@ -4,12 +4,12 @@ require 'fileutils'
|
|
|
4
4
|
require 'thor'
|
|
5
5
|
|
|
6
6
|
module Aigency
|
|
7
|
+
# Top-level aigency commands for creating aigents.
|
|
8
|
+
#
|
|
7
9
|
class Shell < Thor
|
|
8
10
|
map %i[new] => :enlist
|
|
9
|
-
desc "enlist [DIRECTORY]", "
|
|
10
|
-
|
|
11
|
-
def enlist
|
|
12
|
-
directory = options[:directory]
|
|
11
|
+
desc "enlist [DIRECTORY]", "Create an aigent directory"
|
|
12
|
+
def enlist directory = 'aigent'
|
|
13
13
|
puts "Installing aigent at #{directory}"
|
|
14
14
|
if File.exist?(directory)
|
|
15
15
|
if !File.directory?(directory)
|
|
@@ -20,7 +20,7 @@ module Aigency
|
|
|
20
20
|
end
|
|
21
21
|
end
|
|
22
22
|
FileUtils.mkdir(directory) unless File.directory?(directory)
|
|
23
|
-
FileUtils.copy_entry(File.join(GEM_PATH, 'scaffold'), directory)
|
|
23
|
+
FileUtils.copy_entry(File.join(GEM_PATH, 'scaffold', 'enlist'), directory)
|
|
24
24
|
end
|
|
25
25
|
end
|
|
26
26
|
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aigency
|
|
4
|
+
module Tempdir
|
|
5
|
+
module Helpers
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def expand_in_tempdir path
|
|
9
|
+
absolute = File.absolute_path(path, PATH)
|
|
10
|
+
raise PathError, "#{path} is not relative to tempdir" unless absolute.start_with?(PATH)
|
|
11
|
+
absolute
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|