agent_c 2.71828
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/.rubocop.yml +10 -0
- data/.ruby-version +1 -0
- data/CLAUDE.md +21 -0
- data/README.md +360 -0
- data/Rakefile +16 -0
- data/TODO.md +12 -0
- data/agent_c.gemspec +38 -0
- data/docs/chat-methods.md +157 -0
- data/docs/cost-reporting.md +86 -0
- data/docs/pipeline-tips-and-tricks.md +71 -0
- data/docs/session-configuration.md +274 -0
- data/docs/testing.md +747 -0
- data/docs/tools.md +103 -0
- data/docs/versioned-store.md +840 -0
- data/lib/agent_c/agent/chat.rb +211 -0
- data/lib/agent_c/agent/chat_response.rb +32 -0
- data/lib/agent_c/agent/chats/anthropic_bedrock.rb +48 -0
- data/lib/agent_c/batch.rb +102 -0
- data/lib/agent_c/configs/repo.rb +90 -0
- data/lib/agent_c/context.rb +56 -0
- data/lib/agent_c/costs/data.rb +39 -0
- data/lib/agent_c/costs/report.rb +219 -0
- data/lib/agent_c/db/store.rb +162 -0
- data/lib/agent_c/errors.rb +19 -0
- data/lib/agent_c/pipeline.rb +188 -0
- data/lib/agent_c/processor.rb +98 -0
- data/lib/agent_c/prompts.yml +53 -0
- data/lib/agent_c/schema.rb +85 -0
- data/lib/agent_c/session.rb +207 -0
- data/lib/agent_c/store.rb +72 -0
- data/lib/agent_c/test_helpers.rb +173 -0
- data/lib/agent_c/tools/dir_glob.rb +46 -0
- data/lib/agent_c/tools/edit_file.rb +112 -0
- data/lib/agent_c/tools/file_metadata.rb +43 -0
- data/lib/agent_c/tools/grep.rb +119 -0
- data/lib/agent_c/tools/paths.rb +36 -0
- data/lib/agent_c/tools/read_file.rb +94 -0
- data/lib/agent_c/tools/run_rails_test.rb +87 -0
- data/lib/agent_c/tools.rb +60 -0
- data/lib/agent_c/utils/git.rb +75 -0
- data/lib/agent_c/utils/shell.rb +58 -0
- data/lib/agent_c/version.rb +5 -0
- data/lib/agent_c.rb +32 -0
- data/lib/versioned_store/base.rb +314 -0
- data/lib/versioned_store/config.rb +26 -0
- data/lib/versioned_store/stores/schema.rb +127 -0
- data/lib/versioned_store/version.rb +5 -0
- data/lib/versioned_store.rb +5 -0
- data/template/Gemfile +9 -0
- data/template/Gemfile.lock +152 -0
- data/template/README.md +61 -0
- data/template/Rakefile +50 -0
- data/template/bin/rake +27 -0
- data/template/lib/autoload.rb +10 -0
- data/template/lib/config.rb +59 -0
- data/template/lib/pipeline.rb +19 -0
- data/template/lib/prompts.yml +57 -0
- data/template/lib/store.rb +17 -0
- data/template/test/pipeline_test.rb +221 -0
- data/template/test/test_helper.rb +18 -0
- metadata +191 -0
data/lib/agent_c.rb
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ruby_llm"
|
|
4
|
+
require "active_support/all"
|
|
5
|
+
|
|
6
|
+
# this shows an annyoing warning
|
|
7
|
+
begin
|
|
8
|
+
old_stderr = $stderr
|
|
9
|
+
$stderr = StringIO.new
|
|
10
|
+
require "async"
|
|
11
|
+
require "async/semaphore"
|
|
12
|
+
$stderr = old_stderr
|
|
13
|
+
ensure
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
require "zeitwerk"
|
|
17
|
+
loader = Zeitwerk::Loader.for_gem(warn_on_extra_files: false)
|
|
18
|
+
loader.setup
|
|
19
|
+
|
|
20
|
+
# Configure i18n how I like it:
|
|
21
|
+
|
|
22
|
+
require "i18n"
|
|
23
|
+
MissingTranslation = Class.new(StandardError)
|
|
24
|
+
I18n.singleton_class.prepend(Module.new do
|
|
25
|
+
def t(*a, **p)
|
|
26
|
+
super(*a, __force_exception_raising__: true, **p)
|
|
27
|
+
end
|
|
28
|
+
end)
|
|
29
|
+
I18n.exception_handler = ->(_, _, key, _) { raise MissingTranslation.new(key.inspect) }
|
|
30
|
+
I18n.load_path << File.join(__dir__, "./agent_c/prompts.yml")
|
|
31
|
+
|
|
32
|
+
module AgentC; end
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_record"
|
|
4
|
+
|
|
5
|
+
module VersionedStore
|
|
6
|
+
class Base
|
|
7
|
+
class << self
|
|
8
|
+
def inherited(subclass)
|
|
9
|
+
super
|
|
10
|
+
# If parent has a schema, duplicate it for the child
|
|
11
|
+
# Otherwise create a new empty schema
|
|
12
|
+
parent_schema = @schema
|
|
13
|
+
if parent_schema
|
|
14
|
+
subclass.instance_variable_set(:@schema, parent_schema.dup)
|
|
15
|
+
else
|
|
16
|
+
subclass.instance_variable_set(:@schema, Stores::Schema.new)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def schema
|
|
21
|
+
@schema ||= Stores::Schema.new
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def record(name, table: nil, &block)
|
|
25
|
+
schema.record(name, table: table, &block)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def migrate(version = nil, &block)
|
|
29
|
+
if version.nil?
|
|
30
|
+
schema.migrate(&block)
|
|
31
|
+
else
|
|
32
|
+
schema.migrate(version, &block)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
attr_reader :schema, :version, :config
|
|
38
|
+
def initialize(version: :root, **config)
|
|
39
|
+
@schema = self.class.schema
|
|
40
|
+
@schema.add_table_migrations!
|
|
41
|
+
@version = version
|
|
42
|
+
@config = Config.new(**config)
|
|
43
|
+
@klasses = {}
|
|
44
|
+
|
|
45
|
+
if File.exist?(@config.db_filename)
|
|
46
|
+
@config&.logger&.info("using existing store at: #{@config.db_filename}")
|
|
47
|
+
else
|
|
48
|
+
@config&.logger&.info("creating store at: #{@config.db_filename}")
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# Define accessor methods for each record type
|
|
53
|
+
schema.records.each do |name, spec|
|
|
54
|
+
singleton_class.define_method(name) { class_for(spec) }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# eagerly construct classes
|
|
58
|
+
schema.records.each_value { class_for(_1) }
|
|
59
|
+
|
|
60
|
+
# Run post-initialization hooks for setting up associations
|
|
61
|
+
schema.post_init_hooks.each { |hook| hook.call(self) }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def transaction(&)
|
|
65
|
+
base_class.transaction(&)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def dir
|
|
69
|
+
@config.dir
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def versions
|
|
73
|
+
return [self] unless root?
|
|
74
|
+
|
|
75
|
+
version_files = Dir.glob(File.join(versions_dir, "*.sqlite3")).sort
|
|
76
|
+
version_files.map do |file|
|
|
77
|
+
version_num = File.basename(file, ".sqlite3").to_i
|
|
78
|
+
self.class.new(version: version_num, **config.to_h)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def restore(name = nil)
|
|
83
|
+
if name
|
|
84
|
+
# Named snapshot restore
|
|
85
|
+
raise "Can only restore from root store" unless root?
|
|
86
|
+
|
|
87
|
+
snapshot_path = File.join(snapshots_dir, "#{name}.sqlite3")
|
|
88
|
+
raise "Snapshot '#{name}' does not exist" unless File.exist?(snapshot_path)
|
|
89
|
+
|
|
90
|
+
# Copy the snapshot to the main database
|
|
91
|
+
main_db = File.join(dir, config.db_filename)
|
|
92
|
+
FileUtils.cp(snapshot_path, main_db)
|
|
93
|
+
|
|
94
|
+
# Create a new version snapshot of the restored state
|
|
95
|
+
timestamp = Process.clock_gettime(Process::CLOCK_REALTIME, :nanosecond)
|
|
96
|
+
version_file = File.join(versions_dir, "#{timestamp}.sqlite3")
|
|
97
|
+
FileUtils.mkdir_p(File.dirname(version_file))
|
|
98
|
+
FileUtils.cp(main_db, version_file)
|
|
99
|
+
|
|
100
|
+
# Return a new root store for the restored state
|
|
101
|
+
self.class.new(version:, **config.to_h)
|
|
102
|
+
else
|
|
103
|
+
# Version-based restore (existing behavior)
|
|
104
|
+
raise "Can only restore from a version snapshot, not from root" if root?
|
|
105
|
+
|
|
106
|
+
# Copy the version database to the main database
|
|
107
|
+
main_db = File.join(dir, config.db_filename)
|
|
108
|
+
FileUtils.cp(db_path, main_db)
|
|
109
|
+
|
|
110
|
+
# Renumber versions: keep all versions up to and including this one,
|
|
111
|
+
# then create a new version snapshot for the restore
|
|
112
|
+
version_files = Dir.glob(File.join(versions_dir, "*.sqlite3")).sort
|
|
113
|
+
version_files.each do |file|
|
|
114
|
+
file_version = File.basename(file, ".sqlite3").to_i
|
|
115
|
+
if file_version > version
|
|
116
|
+
FileUtils.rm(file)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Create a new version snapshot of the restored state
|
|
121
|
+
new_version_num = version + 1
|
|
122
|
+
version_file = File.join(versions_dir, "#{new_version_num}.sqlite3")
|
|
123
|
+
FileUtils.cp(main_db, version_file)
|
|
124
|
+
|
|
125
|
+
# Return a new root store for the restored state
|
|
126
|
+
self.class.new(version: :root, **config.to_h)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def snapshot(name)
|
|
131
|
+
raise "Can only create snapshots from root store" unless root?
|
|
132
|
+
|
|
133
|
+
# Create snapshots directory if it doesn't exist
|
|
134
|
+
FileUtils.mkdir_p(snapshots_dir)
|
|
135
|
+
|
|
136
|
+
# Copy current database to named snapshot
|
|
137
|
+
snapshot_path = File.join(snapshots_dir, "#{name}.sqlite3")
|
|
138
|
+
FileUtils.cp(db_path, snapshot_path)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
private
|
|
142
|
+
|
|
143
|
+
def schema_root_mod_name
|
|
144
|
+
@schema_root_mod_name ||+ "SchemaRoot_#{schema.object_id}_#{object_id}"
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def schema_root_mod
|
|
148
|
+
@schema_root_mod ||= (
|
|
149
|
+
if Base.const_defined?(schema_root_mod_name)
|
|
150
|
+
Base.const_get(schema_root_mod_name)
|
|
151
|
+
else
|
|
152
|
+
Module.new.tap { Base.const_set(schema_root_mod_name, _1) }
|
|
153
|
+
end
|
|
154
|
+
)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def base_class
|
|
158
|
+
@base_class ||= (
|
|
159
|
+
me = self
|
|
160
|
+
klass = Class.new(ActiveRecord::Base) do
|
|
161
|
+
define_singleton_method(:transaction_mutex) do
|
|
162
|
+
@transaction_mutex ||= Mutex.new
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
define_singleton_method(:class_name) do |name|
|
|
166
|
+
"#{me.send(:schema_root_mod)}::Record_#{name}"
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
define_singleton_method(:store) do
|
|
170
|
+
me
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
define_method(:store) do
|
|
174
|
+
self.class.store
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
define_singleton_method(:transaction) do |**options, &block|
|
|
178
|
+
# If already in a transaction, just call super without creating a backup
|
|
179
|
+
return super(**options, &block) if connection.transaction_open?
|
|
180
|
+
|
|
181
|
+
transaction_mutex.synchronize do
|
|
182
|
+
result = super(**options, &block)
|
|
183
|
+
|
|
184
|
+
if me.config.versioned
|
|
185
|
+
timestamp = Process.clock_gettime(Process::CLOCK_REALTIME, :nanosecond)
|
|
186
|
+
backup_path = File.join(me.send(:versions_dir), "#{timestamp}.sqlite3")
|
|
187
|
+
FileUtils.mkdir_p(File.dirname(backup_path))
|
|
188
|
+
FileUtils.cp(me.send(:db_path), backup_path)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
result
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# ActiveRecord doesn't let this be anonymous?
|
|
197
|
+
schema_root_mod.const_set(
|
|
198
|
+
"Base",
|
|
199
|
+
klass
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
klass.abstract_class = true
|
|
203
|
+
klass.establish_connection(
|
|
204
|
+
adapter: 'sqlite3',
|
|
205
|
+
database: db_path
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
# Configure SQLite to use DELETE journal mode instead of WAL
|
|
209
|
+
# This avoids creating -wal and -shm files
|
|
210
|
+
klass.connection.execute("PRAGMA journal_mode=DELETE")
|
|
211
|
+
klass.connection.execute("PRAGMA locking_mode=NORMAL")
|
|
212
|
+
|
|
213
|
+
# Only run migrations for the root store, not for version snapshots
|
|
214
|
+
if root? && schema.migrations.any?
|
|
215
|
+
# Ensure schema_migrations table exists
|
|
216
|
+
unless klass.connection.table_exists?(:schema_migrations)
|
|
217
|
+
klass.connection.create_table(:schema_migrations, id: false) do |t|
|
|
218
|
+
t.string :version, null: false
|
|
219
|
+
end
|
|
220
|
+
klass.connection.add_index(:schema_migrations, :version, unique: true)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Run any migrations that haven't been run yet
|
|
224
|
+
schema.migrations.each do |migration|
|
|
225
|
+
version = migration.version.to_s
|
|
226
|
+
|
|
227
|
+
# Check if migration has already been run using quote method for SQL safety
|
|
228
|
+
existing = klass.connection.select_value(
|
|
229
|
+
"SELECT 1 FROM schema_migrations WHERE version = #{klass.connection.quote(version)} LIMIT 1"
|
|
230
|
+
)
|
|
231
|
+
next if existing
|
|
232
|
+
|
|
233
|
+
# Run the migration
|
|
234
|
+
klass.connection.instance_exec(&migration.block)
|
|
235
|
+
|
|
236
|
+
# Record that this migration has been run
|
|
237
|
+
klass.connection.execute(
|
|
238
|
+
"INSERT INTO schema_migrations (version) VALUES (#{klass.connection.quote(version)})"
|
|
239
|
+
)
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
klass
|
|
244
|
+
)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def class_for(spec)
|
|
248
|
+
@klasses[spec.name] ||= (
|
|
249
|
+
store = self
|
|
250
|
+
Class.new(base_class) do
|
|
251
|
+
self.table_name = spec.table
|
|
252
|
+
|
|
253
|
+
# Execute all blocks if present, but define a schema method to ignore schema calls
|
|
254
|
+
if spec.blocks && !spec.blocks.empty?
|
|
255
|
+
define_singleton_method(:schema) { |*args, &blk| }
|
|
256
|
+
spec.blocks.each do |blk|
|
|
257
|
+
class_exec(&blk) if blk
|
|
258
|
+
end
|
|
259
|
+
singleton_class.remove_method(:schema) rescue nil
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Make all records readonly if this is a version snapshot
|
|
263
|
+
if not store.send(:root?)
|
|
264
|
+
define_method(:readonly?) { true }
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# Override polymorphic_name to return simple string name (both instance and class methods)
|
|
268
|
+
simple_name = spec.name.to_s
|
|
269
|
+
|
|
270
|
+
define_method(:polymorphic_name) do
|
|
271
|
+
simple_name
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
define_singleton_method(:polymorphic_name) do
|
|
275
|
+
simple_name
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# Override polymorphic_class_for to resolve simple names back to classes
|
|
279
|
+
define_singleton_method(:polymorphic_class_for) do |name|
|
|
280
|
+
store.send(:class_for, store.schema.records.fetch(name.to_sym))
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
).tap {
|
|
284
|
+
schema_root_mod.const_set("Record_#{spec.name}", _1)
|
|
285
|
+
}
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def db_prefix
|
|
289
|
+
@db_prefix ||= config.db_filename.sub(/\.sqlite3$/, "")
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def versions_dir
|
|
293
|
+
@versions_dir ||= File.join(dir, "#{db_prefix}_versions")
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def snapshots_dir
|
|
297
|
+
@snapshots_dir ||= File.join(dir, "#{db_prefix}_snapshots")
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def db_path
|
|
301
|
+
@db_path ||= (
|
|
302
|
+
if root?
|
|
303
|
+
File.join(dir, config.db_filename)
|
|
304
|
+
else
|
|
305
|
+
File.join(versions_dir, "#{version}.sqlite3")
|
|
306
|
+
end
|
|
307
|
+
)
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def root?
|
|
311
|
+
version == :root
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module VersionedStore
|
|
4
|
+
Config = Data.define(:dir, :db_filename, :logger, :versioned) do
|
|
5
|
+
def initialize(dir: nil, path: nil, logger: nil, versioned: true, db_filename: nil)
|
|
6
|
+
raise ArgumentError, "Must provide either dir: or path:, not both" if dir && path
|
|
7
|
+
raise ArgumentError, "Must provide either dir: or path:" unless dir || path
|
|
8
|
+
|
|
9
|
+
db_filename ||= (
|
|
10
|
+
if path
|
|
11
|
+
dir = File.dirname(path)
|
|
12
|
+
db_filename = File.basename(path)
|
|
13
|
+
else
|
|
14
|
+
db_filename = "db.sqlite3"
|
|
15
|
+
end
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
super(
|
|
19
|
+
dir:,
|
|
20
|
+
db_filename:,
|
|
21
|
+
logger:,
|
|
22
|
+
versioned:
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "set"
|
|
4
|
+
module VersionedStore
|
|
5
|
+
module Stores
|
|
6
|
+
class Schema
|
|
7
|
+
Record = Data.define(:name, :table, :blocks)
|
|
8
|
+
Migration = Data.define(:version, :block)
|
|
9
|
+
|
|
10
|
+
class RecordContext
|
|
11
|
+
attr_reader :schema_instance, :table_name, :record_name
|
|
12
|
+
|
|
13
|
+
def initialize(schema_instance, record_name)
|
|
14
|
+
@schema_instance = schema_instance
|
|
15
|
+
@record_name = record_name
|
|
16
|
+
@table_name = nil
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def schema(table_name = nil, &block)
|
|
20
|
+
# If no table name provided, infer from record name
|
|
21
|
+
if table_name.nil?
|
|
22
|
+
name_str = record_name.to_s
|
|
23
|
+
table_name = (name_str.end_with?('s') ? name_str : "#{name_str}s").to_sym
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Store the table name for later use
|
|
27
|
+
@table_name = table_name
|
|
28
|
+
|
|
29
|
+
# Collect all schema blocks for a table
|
|
30
|
+
schema_instance.schema_blocks[table_name] ||= []
|
|
31
|
+
schema_instance.schema_blocks[table_name] << block if block
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def method_missing(...)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def respond_to_missing?(...)
|
|
38
|
+
true
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
attr_reader :migrations, :records, :migrated_tables, :schema_blocks, :post_init_hooks
|
|
43
|
+
def initialize
|
|
44
|
+
@migrations = []
|
|
45
|
+
@records = {}
|
|
46
|
+
@migration_counter = 1
|
|
47
|
+
@migrated_tables = Set.new
|
|
48
|
+
@schema_blocks = {}
|
|
49
|
+
@post_init_hooks = []
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def dup
|
|
53
|
+
new_schema = Schema.new
|
|
54
|
+
new_schema.instance_variable_set(:@migrations, @migrations.dup)
|
|
55
|
+
new_schema.instance_variable_set(:@records, @records.dup)
|
|
56
|
+
new_schema.instance_variable_set(:@migration_counter, @migration_counter)
|
|
57
|
+
new_schema.instance_variable_set(:@migrated_tables, @migrated_tables.dup)
|
|
58
|
+
new_schema.instance_variable_set(:@schema_blocks, @schema_blocks.dup)
|
|
59
|
+
new_schema.instance_variable_set(:@post_init_hooks, @post_init_hooks.dup)
|
|
60
|
+
new_schema.instance_variable_set(:@dir, @dir)
|
|
61
|
+
new_schema
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def dir(path = nil)
|
|
65
|
+
@dir = path if path
|
|
66
|
+
@dir
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def migrate(version = @migration_counter += 1, &block)
|
|
70
|
+
migrations << Migration.new(version: version, block: block)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def prepend_migration(version, &block)
|
|
74
|
+
migrations.unshift(Migration.new(version: version, block: block))
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def record(name, table: nil, &block)
|
|
78
|
+
# If block is given, execute it in RecordContext to extract schema calls
|
|
79
|
+
extracted_table = table
|
|
80
|
+
if block
|
|
81
|
+
context = RecordContext.new(self, name)
|
|
82
|
+
context.instance_exec(&block)
|
|
83
|
+
extracted_table ||= context.table_name
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
prev = records[name]
|
|
87
|
+
blocks = prev ? prev.blocks.dup : []
|
|
88
|
+
blocks << block if block
|
|
89
|
+
# Prefer the first non-nil table name, otherwise default to name + "s"
|
|
90
|
+
table_name = prev&.table || extracted_table
|
|
91
|
+
if table_name.nil?
|
|
92
|
+
name_str = name.to_s
|
|
93
|
+
table_name = (name_str.end_with?('s') ? name_str : "#{name_str}s").to_sym
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
records[name] = Record.new(
|
|
97
|
+
name: name,
|
|
98
|
+
table: table_name,
|
|
99
|
+
blocks: blocks
|
|
100
|
+
)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Add a migration for each table with collected schema blocks (called after all record blocks)
|
|
104
|
+
def add_table_migrations!
|
|
105
|
+
schema_blocks.each do |table_name, blocks|
|
|
106
|
+
next if migrated_tables.include?(table_name) || table_name.nil?
|
|
107
|
+
blocks_to_eval = blocks.dup
|
|
108
|
+
migration_block = Proc.new do
|
|
109
|
+
create_table(table_name) do |t|
|
|
110
|
+
blocks_to_eval.each { |blk| blk.call(t) }
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
version = "table_#{table_name}"
|
|
114
|
+
prepend_migration(version, &migration_block)
|
|
115
|
+
migrated_tables.add(table_name)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def self.call(&block)
|
|
120
|
+
schema = new
|
|
121
|
+
schema.instance_exec(&block) if block
|
|
122
|
+
schema.add_table_migrations!
|
|
123
|
+
schema
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
data/template/Gemfile
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
PATH
|
|
2
|
+
remote: ..
|
|
3
|
+
specs:
|
|
4
|
+
agent_c (2.71828)
|
|
5
|
+
activerecord
|
|
6
|
+
async
|
|
7
|
+
json-schema
|
|
8
|
+
ruby_llm
|
|
9
|
+
sqlite3
|
|
10
|
+
zeitwerk
|
|
11
|
+
|
|
12
|
+
GEM
|
|
13
|
+
remote: https://rubygems.org/
|
|
14
|
+
specs:
|
|
15
|
+
activemodel (8.1.2)
|
|
16
|
+
activesupport (= 8.1.2)
|
|
17
|
+
activerecord (8.1.2)
|
|
18
|
+
activemodel (= 8.1.2)
|
|
19
|
+
activesupport (= 8.1.2)
|
|
20
|
+
timeout (>= 0.4.0)
|
|
21
|
+
activesupport (8.1.2)
|
|
22
|
+
base64
|
|
23
|
+
bigdecimal
|
|
24
|
+
concurrent-ruby (~> 1.0, >= 1.3.1)
|
|
25
|
+
connection_pool (>= 2.2.5)
|
|
26
|
+
drb
|
|
27
|
+
i18n (>= 1.6, < 2)
|
|
28
|
+
json
|
|
29
|
+
logger (>= 1.4.2)
|
|
30
|
+
minitest (>= 5.1)
|
|
31
|
+
securerandom (>= 0.3)
|
|
32
|
+
tzinfo (~> 2.0, >= 2.0.5)
|
|
33
|
+
uri (>= 0.13.1)
|
|
34
|
+
addressable (2.8.8)
|
|
35
|
+
public_suffix (>= 2.0.2, < 8.0)
|
|
36
|
+
async (2.36.0)
|
|
37
|
+
console (~> 1.29)
|
|
38
|
+
fiber-annotation
|
|
39
|
+
io-event (~> 1.11)
|
|
40
|
+
metrics (~> 0.12)
|
|
41
|
+
traces (~> 0.18)
|
|
42
|
+
base64 (0.3.0)
|
|
43
|
+
bigdecimal (4.0.1)
|
|
44
|
+
concurrent-ruby (1.3.6)
|
|
45
|
+
connection_pool (3.0.2)
|
|
46
|
+
console (1.34.2)
|
|
47
|
+
fiber-annotation
|
|
48
|
+
fiber-local (~> 1.1)
|
|
49
|
+
json
|
|
50
|
+
date (3.5.1)
|
|
51
|
+
debug (1.11.1)
|
|
52
|
+
irb (~> 1.10)
|
|
53
|
+
reline (>= 0.3.8)
|
|
54
|
+
drb (2.2.3)
|
|
55
|
+
erb (6.0.1)
|
|
56
|
+
event_stream_parser (1.0.0)
|
|
57
|
+
faraday (2.14.0)
|
|
58
|
+
faraday-net_http (>= 2.0, < 3.5)
|
|
59
|
+
json
|
|
60
|
+
logger
|
|
61
|
+
faraday-multipart (1.2.0)
|
|
62
|
+
multipart-post (~> 2.0)
|
|
63
|
+
faraday-net_http (3.4.2)
|
|
64
|
+
net-http (~> 0.5)
|
|
65
|
+
faraday-retry (2.4.0)
|
|
66
|
+
faraday (~> 2.0)
|
|
67
|
+
fiber-annotation (0.2.0)
|
|
68
|
+
fiber-local (1.1.0)
|
|
69
|
+
fiber-storage
|
|
70
|
+
fiber-storage (1.0.1)
|
|
71
|
+
i18n (1.14.8)
|
|
72
|
+
concurrent-ruby (~> 1.0)
|
|
73
|
+
io-console (0.8.2)
|
|
74
|
+
io-event (1.14.2)
|
|
75
|
+
irb (1.16.0)
|
|
76
|
+
pp (>= 0.6.0)
|
|
77
|
+
rdoc (>= 4.0.0)
|
|
78
|
+
reline (>= 0.4.2)
|
|
79
|
+
json (2.18.0)
|
|
80
|
+
json-schema (6.1.0)
|
|
81
|
+
addressable (~> 2.8)
|
|
82
|
+
bigdecimal (>= 3.1, < 5)
|
|
83
|
+
logger (1.7.0)
|
|
84
|
+
marcel (1.1.0)
|
|
85
|
+
metrics (0.15.0)
|
|
86
|
+
minitest (6.0.1)
|
|
87
|
+
prism (~> 1.5)
|
|
88
|
+
multipart-post (2.4.1)
|
|
89
|
+
net-http (0.9.1)
|
|
90
|
+
uri (>= 0.11.1)
|
|
91
|
+
pp (0.6.3)
|
|
92
|
+
prettyprint
|
|
93
|
+
prettyprint (0.2.0)
|
|
94
|
+
prism (1.8.0)
|
|
95
|
+
psych (5.3.1)
|
|
96
|
+
date
|
|
97
|
+
stringio
|
|
98
|
+
public_suffix (7.0.2)
|
|
99
|
+
rake (13.3.1)
|
|
100
|
+
rdoc (7.1.0)
|
|
101
|
+
erb
|
|
102
|
+
psych (>= 4.0.0)
|
|
103
|
+
tsort
|
|
104
|
+
reline (0.6.3)
|
|
105
|
+
io-console (~> 0.5)
|
|
106
|
+
ruby_llm (1.11.0)
|
|
107
|
+
base64
|
|
108
|
+
event_stream_parser (~> 1)
|
|
109
|
+
faraday (>= 1.10.0)
|
|
110
|
+
faraday-multipart (>= 1)
|
|
111
|
+
faraday-net_http (>= 1)
|
|
112
|
+
faraday-retry (>= 1)
|
|
113
|
+
marcel (~> 1.0)
|
|
114
|
+
ruby_llm-schema (~> 0.2.1)
|
|
115
|
+
zeitwerk (~> 2)
|
|
116
|
+
ruby_llm-schema (0.2.5)
|
|
117
|
+
securerandom (0.4.1)
|
|
118
|
+
sqlite3 (2.9.0-aarch64-linux-gnu)
|
|
119
|
+
sqlite3 (2.9.0-aarch64-linux-musl)
|
|
120
|
+
sqlite3 (2.9.0-arm-linux-gnu)
|
|
121
|
+
sqlite3 (2.9.0-arm-linux-musl)
|
|
122
|
+
sqlite3 (2.9.0-arm64-darwin)
|
|
123
|
+
sqlite3 (2.9.0-x86_64-darwin)
|
|
124
|
+
sqlite3 (2.9.0-x86_64-linux-gnu)
|
|
125
|
+
sqlite3 (2.9.0-x86_64-linux-musl)
|
|
126
|
+
stringio (3.2.0)
|
|
127
|
+
timeout (0.6.0)
|
|
128
|
+
traces (0.18.2)
|
|
129
|
+
tsort (0.2.0)
|
|
130
|
+
tzinfo (2.0.6)
|
|
131
|
+
concurrent-ruby (~> 1.0)
|
|
132
|
+
uri (1.1.1)
|
|
133
|
+
zeitwerk (2.7.4)
|
|
134
|
+
|
|
135
|
+
PLATFORMS
|
|
136
|
+
aarch64-linux-gnu
|
|
137
|
+
aarch64-linux-musl
|
|
138
|
+
arm-linux-gnu
|
|
139
|
+
arm-linux-musl
|
|
140
|
+
arm64-darwin
|
|
141
|
+
x86_64-darwin
|
|
142
|
+
x86_64-linux-gnu
|
|
143
|
+
x86_64-linux-musl
|
|
144
|
+
|
|
145
|
+
DEPENDENCIES
|
|
146
|
+
agent_c!
|
|
147
|
+
debug
|
|
148
|
+
minitest
|
|
149
|
+
rake
|
|
150
|
+
|
|
151
|
+
BUNDLED WITH
|
|
152
|
+
2.6.5
|