aigency 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 +7 -0
- data/README.md +47 -0
- data/Rakefile +8 -0
- data/exe/aigency +9 -0
- data/lib/aigency/aigent.rb +208 -0
- data/lib/aigency/shell.rb +26 -0
- data/lib/aigency/tool.rb +9 -0
- data/lib/aigency/tools.rb +55 -0
- data/lib/aigency/usability.rb +32 -0
- data/lib/aigency/version.rb +5 -0
- data/lib/aigency/workspace/directory_deleter.rb +24 -0
- data/lib/aigency/workspace/directory_reader.rb +34 -0
- data/lib/aigency/workspace/file_deleter.rb +24 -0
- data/lib/aigency/workspace/file_reader.rb +33 -0
- data/lib/aigency/workspace/file_writer.rb +25 -0
- data/lib/aigency/workspace.rb +11 -0
- data/lib/aigency.rb +21 -0
- data/scaffold/.gitignore +1 -0
- data/scaffold/Gemfile +10 -0
- data/scaffold/IDENTITY.md +1 -0
- data/scaffold/README.md +74 -0
- data/scaffold/Rakefile +30 -0
- data/scaffold/config/aigent.yml +6 -0
- data/scaffold/config/schedule.rb +24 -0
- data/sig/aigency.rbs +4 -0
- metadata +104 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: c413cf37072efae1aa6df896884d291ee9bac175a1154e579d85833692f55959
|
|
4
|
+
data.tar.gz: feaacbce278ae242f36bd8f2b3024d014bb200dad97679f6fb023986def493ec
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: '09ba427def6dcce5abcb433c76d44276c58af597ad15f703e1cfb3503e8a527f2944951314c6a2a53e3b91cc54005d54c996e659288d4726e71d0565a33e7cef'
|
|
7
|
+
data.tar.gz: e4ef1fea768cd49fad82edf2ea4086b792118c545c7d12ea8772e0ba7c694793fa45c3024552ca35b9677d15a744a133a6f94bcf5f98a952008a6e5fefd9834b
|
data/README.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Aigency
|
|
2
|
+
|
|
3
|
+
A framework for creating AI agents with RubyLLM.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Install the gem from the command line:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
gem install aigency
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or add it to your Gemfile:
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
gem 'aigency'
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
### Creating a New Aigent
|
|
22
|
+
|
|
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
|
+
```bash
|
|
33
|
+
aigency enlist
|
|
34
|
+
cd aigent
|
|
35
|
+
bundle install
|
|
36
|
+
rake setup
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The bare minimum configuration you need to do is setting `openai_api_base` and `openai_api_key` in `config/aigent.yml`.
|
|
40
|
+
|
|
41
|
+
Test the aigent with with the `rake say` task.
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
rake say["How many fibers are intertwined in a shredded wheat biscuit?"]
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
See the aigent's README (scaffold/README.md in this codebase) for more information.
|
data/Rakefile
ADDED
data/exe/aigency
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'yaml'
|
|
5
|
+
|
|
6
|
+
module Aigency
|
|
7
|
+
class Aigent
|
|
8
|
+
# @return [IO]
|
|
9
|
+
attr_reader :log
|
|
10
|
+
|
|
11
|
+
def initialize(directory)
|
|
12
|
+
@base_directory = File.absolute_path(directory)
|
|
13
|
+
@log = $stdout
|
|
14
|
+
load_requires
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# @return [String]
|
|
18
|
+
def name
|
|
19
|
+
@name ||= config('aigent')['name'] || "an Aigent"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Get the aigent directory with optional additional paths.
|
|
23
|
+
#
|
|
24
|
+
# @param paths [Array<String>] Additional paths
|
|
25
|
+
# @return [String] The complete path
|
|
26
|
+
def directory(*paths)
|
|
27
|
+
File.join(@base_directory, *paths)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Get the aigent data directory with optional additional paths.
|
|
31
|
+
#
|
|
32
|
+
# @param paths [Array<String>] Additional paths
|
|
33
|
+
# @return [String] The complete path
|
|
34
|
+
def data(*paths)
|
|
35
|
+
directory('data', *paths)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Initialize a RubyLLM::Chat.
|
|
39
|
+
#
|
|
40
|
+
# @param extra [Array<String>] Additional instructions
|
|
41
|
+
# @return [RubyLLM::Chat]
|
|
42
|
+
def brief(*extra)
|
|
43
|
+
chat = rubyllm_context.chat(provider: :openai)
|
|
44
|
+
Tools.registry.each { |klass| chat.with_tool(klass.new(self)) }
|
|
45
|
+
chat.with_instructions(build_instructions(extra), append: true)
|
|
46
|
+
# @todo This should be configurable
|
|
47
|
+
chat.with_params(max_tokens: max_tokens)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def load filename, *extra, out: $stdout
|
|
51
|
+
stream File.read(filename), *extra, out: out
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def stream text, *extra, with: [], out: $stdout
|
|
55
|
+
original_log = @log
|
|
56
|
+
@log = out
|
|
57
|
+
brief(*extra).ask text, with: with do |chunk|
|
|
58
|
+
@log.print chunk.thinking&.text
|
|
59
|
+
@log.print chunk.content
|
|
60
|
+
end.tap { @log = original_log }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# @param name [String]
|
|
64
|
+
# @return [Hash]
|
|
65
|
+
def config(name)
|
|
66
|
+
configs[name] ||= read_config(name)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Initialize the aigent's tool configurations.
|
|
70
|
+
#
|
|
71
|
+
# @param only [Array<String>] Limit the tools being configured
|
|
72
|
+
# @param overwrite [Boolean] Replace existing configs if true
|
|
73
|
+
def setup(only: [], overwrite: false)
|
|
74
|
+
only = [only].flatten.compact
|
|
75
|
+
config_count = 0
|
|
76
|
+
Tools.config_map.each do |name, text|
|
|
77
|
+
next unless only.empty? || only.include?(name)
|
|
78
|
+
|
|
79
|
+
dir = directory('config')
|
|
80
|
+
FileUtils.mkdir_p dir
|
|
81
|
+
path = File.join(dir, "#{name}.yml")
|
|
82
|
+
if overwrite || !File.file?(path)
|
|
83
|
+
doing = File.file?(path) ? 'Overwriting' : 'Writing new'
|
|
84
|
+
log.puts "#{doing} config for #{name}"
|
|
85
|
+
File.write path, text
|
|
86
|
+
config_count += 1
|
|
87
|
+
else
|
|
88
|
+
log.puts "Config for #{name} exists, skipping"
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
dir_count = 0
|
|
92
|
+
Tools.directories.each do |path|
|
|
93
|
+
absolute = directory(path)
|
|
94
|
+
next if File.directory?(absolute)
|
|
95
|
+
|
|
96
|
+
FileUtils.mkdir_p absolute
|
|
97
|
+
dir_count += 1
|
|
98
|
+
end
|
|
99
|
+
log.puts "#{config_count} config files initialized."
|
|
100
|
+
log.puts "#{dir_count} directories created."
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Run code in an exclusive lock
|
|
104
|
+
#
|
|
105
|
+
def lock(&block)
|
|
106
|
+
File.open(data('lock'), 'w') do |file|
|
|
107
|
+
file.flock(File::LOCK_EX)
|
|
108
|
+
file.write(Process.pid)
|
|
109
|
+
file.flush
|
|
110
|
+
block&.call
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Run code if an exclusive lock is available or skip if a lock exists
|
|
115
|
+
#
|
|
116
|
+
def try_with_lock(&block)
|
|
117
|
+
File.open(data('lock'), 'w') do |file|
|
|
118
|
+
acquired = file.flock(File::LOCK_EX|File::LOCK_NB)
|
|
119
|
+
return unless acquired
|
|
120
|
+
file.write(Process.pid)
|
|
121
|
+
file.flush
|
|
122
|
+
block&.call
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
alias nonblock try_with_lock
|
|
126
|
+
|
|
127
|
+
# True if an exclusive lock exists
|
|
128
|
+
#
|
|
129
|
+
def locked?
|
|
130
|
+
File.open(data('lock'), File::RDWR|File::CREAT) do |file|
|
|
131
|
+
result = file.flock(File::LOCK_EX|File::LOCK_NB)
|
|
132
|
+
return !result
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
alias busy? locked?
|
|
136
|
+
|
|
137
|
+
def instructions
|
|
138
|
+
@instructions ||= build_instructions([])
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
private
|
|
142
|
+
|
|
143
|
+
def configs
|
|
144
|
+
@configs ||= {}
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def read_config(name)
|
|
148
|
+
base = directory('config')
|
|
149
|
+
filename = if name =~ /(\.yaml|\.yml)$/
|
|
150
|
+
name
|
|
151
|
+
else
|
|
152
|
+
"#{name}.yml"
|
|
153
|
+
end
|
|
154
|
+
full_path = File.join(directory, 'config', filename)
|
|
155
|
+
if File.file?(full_path)
|
|
156
|
+
YAML.load_file(full_path)
|
|
157
|
+
else
|
|
158
|
+
warn "Config name #{name} not found at #{full_path}"
|
|
159
|
+
{}
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def identity_file
|
|
164
|
+
@identity_file ||= File.join(directory, 'IDENTITY.md')
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def identity_file?
|
|
168
|
+
File.file?(identity_file)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def base_instructions
|
|
172
|
+
@base_instructions ||= identity_file? ? File.read(identity_file) : "You are a helpful AI assistant."
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def build_instructions(extra)
|
|
176
|
+
[base_instructions].concat tool_prompts
|
|
177
|
+
.push "Today's date is #{DateTime.now.strftime('%B %e, %Y')}."
|
|
178
|
+
.concat extra
|
|
179
|
+
.join("\n\n")
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def tool_prompts
|
|
183
|
+
Tools.config_map
|
|
184
|
+
.keys
|
|
185
|
+
.map { |name| config(name)['prompt'] }
|
|
186
|
+
.compact
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def load_requires
|
|
190
|
+
read_requires.each { |name| require name }
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def read_requires
|
|
194
|
+
config('aigent')['require'] || []
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def max_tokens
|
|
198
|
+
@max_tokens ||= (config('aigent')['max_tokens'] || -1).to_i
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def rubyllm_context
|
|
202
|
+
RubyLLM.context do |config|
|
|
203
|
+
config.openai_api_base = config('aigent')['openai_api_base']
|
|
204
|
+
config.openai_api_key = config('aigent')['openai_api_key']
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'thor'
|
|
5
|
+
|
|
6
|
+
module Aigency
|
|
7
|
+
class Shell < Thor
|
|
8
|
+
map %i[new] => :enlist
|
|
9
|
+
desc "enlist [DIRECTORY]", "Enable the current user as an aigent"
|
|
10
|
+
option :directory, type: :string, desc: 'Aigent directory', default: File.join(Dir.home, 'aigent')
|
|
11
|
+
def enlist
|
|
12
|
+
directory = options[:directory]
|
|
13
|
+
puts "Installing aigent at #{directory}"
|
|
14
|
+
if File.exist?(directory)
|
|
15
|
+
if !File.directory?(directory)
|
|
16
|
+
raise "#{directory} is not a directory."
|
|
17
|
+
end
|
|
18
|
+
if !Dir.empty?(directory)
|
|
19
|
+
raise "#{directory} is not empty."
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
FileUtils.mkdir(directory) unless File.directory?(directory)
|
|
23
|
+
FileUtils.copy_entry(File.join(GEM_PATH, 'scaffold'), directory)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
data/lib/aigency/tool.rb
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
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
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aigency
|
|
4
|
+
module Usability
|
|
5
|
+
attr_reader :aigent
|
|
6
|
+
|
|
7
|
+
def initialize(aigent)
|
|
8
|
+
@aigent = aigent
|
|
9
|
+
post_initialize
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def post_initialize
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Subclasses need to implement functionality here.
|
|
16
|
+
#
|
|
17
|
+
# @abstract
|
|
18
|
+
def execute(**opts)
|
|
19
|
+
raise 'Not implemented'
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.included other
|
|
23
|
+
other.extend ClassMethods
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
module ClassMethods
|
|
27
|
+
def execute(aigent, **opts)
|
|
28
|
+
new(aigent).execute(**opts)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aigency
|
|
4
|
+
module Workspace
|
|
5
|
+
class DirectoryDeleter < Tool
|
|
6
|
+
desc "Deletes a directory from your private workspace"
|
|
7
|
+
|
|
8
|
+
def execute(dirname:)
|
|
9
|
+
puts "Deleting directory #{dirname}"
|
|
10
|
+
|
|
11
|
+
resolved = File.absolute_path(dirname, aigent.data('workspace'))
|
|
12
|
+
return { dirname: dirname, success: false, error: "Permission denied" } unless resolved.start_with?(aigent.data('workspace'))
|
|
13
|
+
return { dirname: dirname, success: false, error: "Permission denied" } if resolved.include?('/.git/') || resolved.end_with?('/.git')
|
|
14
|
+
return { dirname: dirname, success: false, error: "Not a directory" } if File.file?(resolved)
|
|
15
|
+
return { dirname: dirname, sucess: false, error: "Directory not found" } unless File.directory?(resolved)
|
|
16
|
+
|
|
17
|
+
FileUtils.rm_rf(resolved)
|
|
18
|
+
{ dirname: dirname, success: true }
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
Aigency::Tools.register Aigency::Workspace::DirectoryDeleter
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aigency
|
|
4
|
+
module Workspace
|
|
5
|
+
class DirectoryReader < Tool
|
|
6
|
+
desc "Gets the contents of a directory from your private workspace"
|
|
7
|
+
|
|
8
|
+
def execute(directory_name:)
|
|
9
|
+
aigent.log.puts "[Workspace::DirectoryReader] Reading #{directory_name}"
|
|
10
|
+
directory = aigent.data('workspace')
|
|
11
|
+
resolved = File.absolute_path(directory_name, directory)
|
|
12
|
+
return { directory: directory_name,
|
|
13
|
+
success: false,
|
|
14
|
+
error: "Permission denied. Path must be local to #{directory}" } unless resolved.start_with?(directory)
|
|
15
|
+
|
|
16
|
+
if File.directory?(resolved)
|
|
17
|
+
{
|
|
18
|
+
directory: directory_name,
|
|
19
|
+
success: true,
|
|
20
|
+
entries: Dir[File.join(resolved, '*')].map { |dir| dir[resolved.length+1..]}
|
|
21
|
+
}
|
|
22
|
+
else
|
|
23
|
+
{
|
|
24
|
+
directory: directory_name,
|
|
25
|
+
success: false,
|
|
26
|
+
error: "Not a directory"
|
|
27
|
+
}
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
Aigency::Tools.register Aigency::Workspace::DirectoryReader
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aigency
|
|
4
|
+
module Workspace
|
|
5
|
+
class FileDeleter < Tool
|
|
6
|
+
desc "Deletes a file from your private workspace"
|
|
7
|
+
|
|
8
|
+
def execute(filename:)
|
|
9
|
+
aigent.log.puts "[Workspace::FileDeleter] Deleting file #{filename}"
|
|
10
|
+
|
|
11
|
+
directory = aigent.data('directory')
|
|
12
|
+
resolved = File.absolute_path(filename, directory)
|
|
13
|
+
return { filename: filename, error: "Permission denied" } unless resolved.start_with?(directory)
|
|
14
|
+
return { filename: filename, error: "Permission denied" } if resolved.include?('/.git/')
|
|
15
|
+
return { filename: filename, error: "File does not exist" } unless File.file?(resolved)
|
|
16
|
+
|
|
17
|
+
File.delete resolved
|
|
18
|
+
{ filename: filename, success: true }
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
Aigency::Tools.register Aigency::Workspace::FileDeleter
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aigency
|
|
4
|
+
module Workspace
|
|
5
|
+
class FileReader < Tool
|
|
6
|
+
desc "Reads a file from your private workspace"
|
|
7
|
+
|
|
8
|
+
def execute(filename:)
|
|
9
|
+
aigent.log.puts "[Workspace::FileReader] Reading #{filename}"
|
|
10
|
+
|
|
11
|
+
directory = aigent.data('workspace')
|
|
12
|
+
resolved = File.absolute_path(filename, directory)
|
|
13
|
+
return { filename: filename, success: false, error: "Permission denied" } unless resolved.start_with?(directory)
|
|
14
|
+
|
|
15
|
+
if File.file?(resolved)
|
|
16
|
+
{
|
|
17
|
+
filename: filename,
|
|
18
|
+
success: true,
|
|
19
|
+
content: File.read(resolved)
|
|
20
|
+
}
|
|
21
|
+
else
|
|
22
|
+
{
|
|
23
|
+
filename: filename,
|
|
24
|
+
success: false,
|
|
25
|
+
error: "File not found"
|
|
26
|
+
}
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
Aigency::Tools.register Aigency::Workspace::FileReader
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
|
|
5
|
+
module Aigency
|
|
6
|
+
module Workspace
|
|
7
|
+
class FileWriter < Tool
|
|
8
|
+
desc "Writes a file to your private workspace"
|
|
9
|
+
|
|
10
|
+
def execute(filename:, content:)
|
|
11
|
+
aigent.log.puts "[Workspace::FileWriter] Writing #{filename}"
|
|
12
|
+
|
|
13
|
+
directory = aigent.data('workspace')
|
|
14
|
+
resolved = File.absolute_path(filename, directory)
|
|
15
|
+
return { filename: filename, success: false, error: "Permission denied" } unless resolved.start_with?(directory)
|
|
16
|
+
|
|
17
|
+
FileUtils.mkdir_p File.dirname(resolved)
|
|
18
|
+
File.write resolved, content
|
|
19
|
+
return { filename: filename, success: true }
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
Aigency::Tools.register Aigency::Workspace::FileWriter
|
data/lib/aigency.rb
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'ruby_llm'
|
|
4
|
+
|
|
5
|
+
# Core
|
|
6
|
+
require 'aigency/version'
|
|
7
|
+
require 'aigency/usability'
|
|
8
|
+
require 'aigency/tool'
|
|
9
|
+
require 'aigency/tools'
|
|
10
|
+
require 'aigency/aigent'
|
|
11
|
+
require 'aigency/shell'
|
|
12
|
+
|
|
13
|
+
# Extensions
|
|
14
|
+
require 'aigency/workspace'
|
|
15
|
+
|
|
16
|
+
module Aigency
|
|
17
|
+
GEM_PATH = File.absolute_path(File.join(__dir__, '..'))
|
|
18
|
+
|
|
19
|
+
class Error < StandardError; end
|
|
20
|
+
# Your code goes here...
|
|
21
|
+
end
|
data/scaffold/.gitignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
data
|
data/scaffold/Gemfile
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
You are a helpful AI assistant.
|
data/scaffold/README.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Your Aigent
|
|
2
|
+
|
|
3
|
+
An aigent (AI agent) is an intelligent assistant powered by language models through OpenAI-compatible APIs. It can read and write files in its private workspace and perform scheduled tasks on your behalf.
|
|
4
|
+
|
|
5
|
+
## Configuration
|
|
6
|
+
|
|
7
|
+
### Install dependencies
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
bundle install
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### Initial setup
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
rake setup
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### 1. Configure the Aigent
|
|
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`).
|
|
26
|
+
|
|
27
|
+
### 2. Define the Aigent's Personality
|
|
28
|
+
|
|
29
|
+
Edit `IDENTITY.md` to define how your aigent should behave:
|
|
30
|
+
|
|
31
|
+
```markdown
|
|
32
|
+
You are a professional assistant named Alex. You respond in a friendly but concise manner.
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
This file sets the foundation for all the aigent's responses.
|
|
36
|
+
|
|
37
|
+
## Usage
|
|
38
|
+
|
|
39
|
+
### Interactive Mode
|
|
40
|
+
|
|
41
|
+
Talk to your aigent using the Rake task:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
rake say["Tell me about project progress"]
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### The Workspace
|
|
48
|
+
|
|
49
|
+
Your aigent has read/write access to everything in the `data/workspace` directory.
|
|
50
|
+
|
|
51
|
+
### Scheduling Tasks
|
|
52
|
+
|
|
53
|
+
The project includes the `whenever` gem for scheduling. Example of a scheduled task:
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
# config/schedule.rb
|
|
57
|
+
|
|
58
|
+
# Every day at 9am, the aigent will run the prompt in the DAILY_TASKS.md file
|
|
59
|
+
every 1.day, at: '9am' do
|
|
60
|
+
rake 'load[DAILY_TASKS.md]'
|
|
61
|
+
end
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Add the scheduled tasks to your crontab:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
bundle exec whenever --update-crontab
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
(Note: the `whenever` gem does not work on Windows.)
|
|
71
|
+
|
|
72
|
+
## Aigent Tools
|
|
73
|
+
|
|
74
|
+
You can give your aigent more functionality by installing additional tools. It ships with the `Workspace` tool, which gives it full read/write access to the `data/workspace` directory.
|
data/scaffold/Rakefile
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
require "bundler/setup"
|
|
2
|
+
require 'yaml'
|
|
3
|
+
require 'dotenv/load'
|
|
4
|
+
require 'aigency'
|
|
5
|
+
|
|
6
|
+
desc "Run a prompt from a file"
|
|
7
|
+
task :load, [:filename] do |_, args|
|
|
8
|
+
aigent = Aigency::Aigent.new('.')
|
|
9
|
+
aigent.nonblock do
|
|
10
|
+
filename = File.join('data', 'chats', "#{Time.now.strftime("%Y-%m-%d_%H%M%S")}_load_#{args[:filename].gsub('/', '_')}.txt")
|
|
11
|
+
File.open(filename, 'w') do |file|
|
|
12
|
+
aigent.load args[:filename], out: file
|
|
13
|
+
rescue StandardError => e
|
|
14
|
+
file.puts "[#{e.class}]: #{e.message}"
|
|
15
|
+
file.puts e.backtrace
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
desc "Talk to the aigent"
|
|
21
|
+
task :say, [:text] do |_, args|
|
|
22
|
+
aigent = Aigency::Aigent.new('.')
|
|
23
|
+
aigent.stream args[:text]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
desc "Set up aigent configurations"
|
|
27
|
+
task :setup do
|
|
28
|
+
aigent = Aigency::Aigent.new('.')
|
|
29
|
+
aigent.setup
|
|
30
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Use this file to easily define all of your cron jobs.
|
|
2
|
+
#
|
|
3
|
+
# It's helpful, but not entirely necessary to understand cron before proceeding.
|
|
4
|
+
# http://en.wikipedia.org/wiki/Cron
|
|
5
|
+
|
|
6
|
+
# Example:
|
|
7
|
+
#
|
|
8
|
+
# set :output, "/path/to/my/cron_log.log"
|
|
9
|
+
#
|
|
10
|
+
# every 2.hours do
|
|
11
|
+
# command "/usr/bin/some_great_command"
|
|
12
|
+
# runner "MyModel.some_method"
|
|
13
|
+
# rake "some:great:rake:task"
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
# every 4.days do
|
|
17
|
+
# runner "AnotherModel.prune_old_records"
|
|
18
|
+
# end
|
|
19
|
+
|
|
20
|
+
# Learn more: http://github.com/javan/whenever
|
|
21
|
+
|
|
22
|
+
# every 1.day do
|
|
23
|
+
# rake "load[DAILY.md]"
|
|
24
|
+
# end
|
data/sig/aigency.rbs
ADDED
metadata
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: aigency
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Fred Snyder
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: fileutils
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '1.8'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '1.8'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: ruby_llm
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '1.15'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '1.15'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: thor
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '1.5'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '1.5'
|
|
54
|
+
email:
|
|
55
|
+
- fsnyder@castwide.com
|
|
56
|
+
executables:
|
|
57
|
+
- aigency
|
|
58
|
+
extensions: []
|
|
59
|
+
extra_rdoc_files: []
|
|
60
|
+
files:
|
|
61
|
+
- README.md
|
|
62
|
+
- Rakefile
|
|
63
|
+
- exe/aigency
|
|
64
|
+
- lib/aigency.rb
|
|
65
|
+
- lib/aigency/aigent.rb
|
|
66
|
+
- lib/aigency/shell.rb
|
|
67
|
+
- lib/aigency/tool.rb
|
|
68
|
+
- lib/aigency/tools.rb
|
|
69
|
+
- lib/aigency/usability.rb
|
|
70
|
+
- lib/aigency/version.rb
|
|
71
|
+
- lib/aigency/workspace.rb
|
|
72
|
+
- lib/aigency/workspace/directory_deleter.rb
|
|
73
|
+
- lib/aigency/workspace/directory_reader.rb
|
|
74
|
+
- lib/aigency/workspace/file_deleter.rb
|
|
75
|
+
- lib/aigency/workspace/file_reader.rb
|
|
76
|
+
- lib/aigency/workspace/file_writer.rb
|
|
77
|
+
- scaffold/.gitignore
|
|
78
|
+
- scaffold/Gemfile
|
|
79
|
+
- scaffold/IDENTITY.md
|
|
80
|
+
- scaffold/README.md
|
|
81
|
+
- scaffold/Rakefile
|
|
82
|
+
- scaffold/config/aigent.yml
|
|
83
|
+
- scaffold/config/schedule.rb
|
|
84
|
+
- sig/aigency.rbs
|
|
85
|
+
licenses: []
|
|
86
|
+
metadata: {}
|
|
87
|
+
rdoc_options: []
|
|
88
|
+
require_paths:
|
|
89
|
+
- lib
|
|
90
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
91
|
+
requirements:
|
|
92
|
+
- - ">="
|
|
93
|
+
- !ruby/object:Gem::Version
|
|
94
|
+
version: 3.2.0
|
|
95
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
96
|
+
requirements:
|
|
97
|
+
- - ">="
|
|
98
|
+
- !ruby/object:Gem::Version
|
|
99
|
+
version: '0'
|
|
100
|
+
requirements: []
|
|
101
|
+
rubygems_version: 4.0.11
|
|
102
|
+
specification_version: 4
|
|
103
|
+
summary: A framework for building agentic AI workers.
|
|
104
|
+
test_files: []
|