rails_console_ai 0.36.0 → 0.38.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 +16 -0
- data/README.md +49 -11
- data/app/controllers/rails_console_ai/artifacts_controller.rb +67 -0
- data/app/models/rails_console_ai/artifact.rb +231 -0
- data/app/views/layouts/rails_console_ai/application.html.erb +1 -0
- data/app/views/rails_console_ai/artifacts/index.html.erb +60 -0
- data/app/views/rails_console_ai/artifacts/show.html.erb +119 -0
- data/app/views/rails_console_ai/sessions/show.html.erb +6 -0
- data/config/routes.rb +8 -0
- data/lib/generators/rails_console_ai/templates/initializer.rb +5 -0
- data/lib/rails_console_ai/channel/console.rb +12 -3
- data/lib/rails_console_ai/channel/slack.rb +1 -1
- data/lib/rails_console_ai/configuration.rb +72 -12
- data/lib/rails_console_ai/context_builder.rb +48 -18
- data/lib/rails_console_ai/conversation_engine.rb +97 -27
- data/lib/rails_console_ai/providers/anthropic.rb +3 -1
- data/lib/rails_console_ai/providers/base.rb +20 -0
- data/lib/rails_console_ai/providers/bedrock.rb +5 -2
- data/lib/rails_console_ai/providers/local.rb +1 -1
- data/lib/rails_console_ai/providers/openai.rb +1 -1
- data/lib/rails_console_ai/providers/openrouter.rb +9 -1
- data/lib/rails_console_ai/slack_bot.rb +40 -12
- data/lib/rails_console_ai/slash_commands.rb +1 -0
- data/lib/rails_console_ai/sub_agent.rb +28 -0
- data/lib/rails_console_ai/tools/registry.rb +231 -1
- data/lib/rails_console_ai/version.rb +1 -1
- data/lib/rails_console_ai.rb +222 -0
- metadata +5 -1
data/lib/rails_console_ai.rb
CHANGED
|
@@ -57,6 +57,146 @@ module RailsConsoleAi
|
|
|
57
57
|
@current_user = name
|
|
58
58
|
end
|
|
59
59
|
|
|
60
|
+
# Scheme + host for links the gem hands out, without a trailing slash.
|
|
61
|
+
# Empty string when nothing is configured and Rails has no url options, in
|
|
62
|
+
# which case callers fall back to a relative path.
|
|
63
|
+
def base_url
|
|
64
|
+
configured = configuration.base_url.to_s.strip
|
|
65
|
+
return normalize_base_url(configured) unless configured.empty?
|
|
66
|
+
|
|
67
|
+
from_rails = rails_default_url_base
|
|
68
|
+
from_rails ? normalize_base_url(from_rails) : ''
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def normalize_base_url(value)
|
|
72
|
+
value = value.to_s.strip.chomp('/')
|
|
73
|
+
return '' if value.empty?
|
|
74
|
+
# Forgiving about a host given without a scheme — "secure.apptoto.com".
|
|
75
|
+
value = "https://#{value}" unless value =~ %r{\A[a-z][a-z0-9+.-]*://}i
|
|
76
|
+
value
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Most apps already declare their canonical host for mailers or url helpers.
|
|
80
|
+
def rails_default_url_base
|
|
81
|
+
return nil unless defined?(::Rails) && ::Rails.respond_to?(:application) && ::Rails.application
|
|
82
|
+
|
|
83
|
+
opts = ::Rails.application.routes.default_url_options
|
|
84
|
+
opts = nil if opts.nil? || opts[:host].to_s.empty?
|
|
85
|
+
|
|
86
|
+
if opts.nil?
|
|
87
|
+
mailer = ::Rails.application.config.action_mailer.default_url_options rescue nil
|
|
88
|
+
opts = mailer if mailer && !mailer[:host].to_s.empty?
|
|
89
|
+
end
|
|
90
|
+
return nil unless opts && !opts[:host].to_s.empty?
|
|
91
|
+
|
|
92
|
+
protocol = opts[:protocol].to_s.sub(%r{://\z}, '')
|
|
93
|
+
protocol = 'https' if protocol.empty?
|
|
94
|
+
port = opts[:port]
|
|
95
|
+
host = opts[:host].to_s.chomp('/')
|
|
96
|
+
port_part = port && ![80, 443, '80', '443'].include?(port) ? ":#{port}" : ''
|
|
97
|
+
"#{protocol}://#{host}#{port_part}"
|
|
98
|
+
rescue StandardError
|
|
99
|
+
nil
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Where the host app mounted the engine, so a tool can hand the user a URL
|
|
103
|
+
# that actually resolves. Discovered from the route table rather than assumed,
|
|
104
|
+
# since the mount point is the host's choice.
|
|
105
|
+
def engine_mount_path
|
|
106
|
+
return @engine_mount_path if defined?(@engine_mount_path) && @engine_mount_path
|
|
107
|
+
|
|
108
|
+
@engine_mount_path =
|
|
109
|
+
begin
|
|
110
|
+
found = nil
|
|
111
|
+
if defined?(::Rails) && ::Rails.respond_to?(:application) && ::Rails.application
|
|
112
|
+
::Rails.application.routes.routes.each do |route|
|
|
113
|
+
app = route.app
|
|
114
|
+
# Mounted engines sit behind one or more wrappers; unwrap a bounded
|
|
115
|
+
# number of times rather than trusting the shape of any one Rails version.
|
|
116
|
+
5.times do
|
|
117
|
+
break unless app.respond_to?(:app) && !app.app.equal?(app)
|
|
118
|
+
app = app.app
|
|
119
|
+
end
|
|
120
|
+
next unless app == RailsConsoleAi::Engine
|
|
121
|
+
|
|
122
|
+
found = route.path.spec.to_s.sub(/\(\.:format\)\z/, '').chomp('/')
|
|
123
|
+
break
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
found.to_s.empty? ? '/rails_console_ai' : found
|
|
127
|
+
rescue StandardError
|
|
128
|
+
'/rails_console_ai'
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Where a logged session can be read back. Relative when no host is known.
|
|
133
|
+
def session_url(id)
|
|
134
|
+
"#{base_url}#{engine_mount_path}/sessions/#{id}"
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# The session row the current turn belongs to, so tools that persist
|
|
138
|
+
# something (save_artifact) can point back at the conversation that produced
|
|
139
|
+
# it. Set by the engine as it logs each turn; nil outside a logged session.
|
|
140
|
+
def current_session_id
|
|
141
|
+
@current_session_id
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def current_session_id=(id)
|
|
145
|
+
@current_session_id = id
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Save an artifact from Ruby, for content the console already holds.
|
|
149
|
+
#
|
|
150
|
+
# html = S3.read_file("report.html")
|
|
151
|
+
# RailsConsoleAi.save_artifact(name: "Mathnasium migration", content: html)
|
|
152
|
+
#
|
|
153
|
+
# This exists because the save_artifact TOOL can only carry content the model
|
|
154
|
+
# types out into its response, and a response has a hard output-token cap. A
|
|
155
|
+
# 40KB file does not fit under it: the call is cut off mid-argument, discarded,
|
|
156
|
+
# and retried identically forever. Going through Ruby, the bytes never enter
|
|
157
|
+
# the model's response at all — and what lands in the artifact is the string
|
|
158
|
+
# itself, byte for byte, not the model's recollection of it.
|
|
159
|
+
#
|
|
160
|
+
# Returns the URL. Saving under a name that already exists replaces that
|
|
161
|
+
# artifact and bumps its revision, exactly as the tool does.
|
|
162
|
+
def save_artifact(name:, content:, content_type: nil, description: nil)
|
|
163
|
+
model = artifact_model
|
|
164
|
+
raise 'Artifacts need the Rails engine loaded.' unless model
|
|
165
|
+
unless model.available?
|
|
166
|
+
raise "The rails_console_ai_artifacts table does not exist. Run `ai_db_setup` first."
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
content_type ||= model.infer_content_type(content)
|
|
170
|
+
# Called from inside execute_code, this INSERT would otherwise be stopped by
|
|
171
|
+
# the database_writes guard — which is the whole point of safe mode, except
|
|
172
|
+
# that the target is the gem's own artifacts table. The save_artifact tool
|
|
173
|
+
# writes to it unguarded already, being a tool rather than eval'd code; this
|
|
174
|
+
# is the same write reached a different way, so it gets the same exemption.
|
|
175
|
+
# Scoped to the one save, nothing the caller passes runs inside it.
|
|
176
|
+
artifact = configuration.safety_guards.without_guards do
|
|
177
|
+
model.store(name: name, content: content,
|
|
178
|
+
content_type: content_type, description: description)
|
|
179
|
+
end
|
|
180
|
+
if artifact.errors.any?
|
|
181
|
+
raise ArgumentError, "Could not save artifact: #{artifact.errors.full_messages.join('; ')}"
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
"#{base_url}#{engine_mount_path}/artifacts/#{artifact.slug}"
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Fully qualified on purpose. A bare `Artifact` here reads as
|
|
188
|
+
# RailsConsoleAi::Artifact to Ruby, but the enclosing scope is the singleton
|
|
189
|
+
# class — an anonymous module — so once Rails' autoloader is the one
|
|
190
|
+
# resolving it, const_missing has no namespace to derive from and goes
|
|
191
|
+
# looking for a top-level Artifact that does not exist. Referencing the full
|
|
192
|
+
# path is also what triggers the autoload in the first place; `defined?`
|
|
193
|
+
# would report "no engine" in a perfectly healthy app.
|
|
194
|
+
def artifact_model
|
|
195
|
+
RailsConsoleAi::Artifact
|
|
196
|
+
rescue NameError
|
|
197
|
+
nil
|
|
198
|
+
end
|
|
199
|
+
|
|
60
200
|
# Enqueue an agent run. Returns the Integer session id immediately;
|
|
61
201
|
# the actual work is picked up by `rake rails_console_ai:agents`.
|
|
62
202
|
#
|
|
@@ -185,6 +325,7 @@ module RailsConsoleAi
|
|
|
185
325
|
setup_skills_tables!(conn)
|
|
186
326
|
setup_memories_tables!(conn)
|
|
187
327
|
setup_agents_tables!(conn)
|
|
328
|
+
setup_artifacts_table!(conn)
|
|
188
329
|
|
|
189
330
|
migrate!
|
|
190
331
|
rescue => e
|
|
@@ -352,6 +493,80 @@ module RailsConsoleAi
|
|
|
352
493
|
end
|
|
353
494
|
end
|
|
354
495
|
|
|
496
|
+
def setup_artifacts_table!(conn)
|
|
497
|
+
table = 'rails_console_ai_artifacts'
|
|
498
|
+
return if conn.table_exists?(table)
|
|
499
|
+
|
|
500
|
+
conn.create_table(table) do |t|
|
|
501
|
+
t.string :name, limit: 255, null: false
|
|
502
|
+
# Stable handle derived from the name, so the assistant can be told
|
|
503
|
+
# "update the churn summary" in a later session and find the same row.
|
|
504
|
+
t.string :slug, limit: 255, null: false
|
|
505
|
+
t.text :description
|
|
506
|
+
t.string :content_type, limit: 100, null: false
|
|
507
|
+
t.text :content, null: false
|
|
508
|
+
t.integer :byte_size, default: 0, null: false
|
|
509
|
+
t.integer :revision, default: 1, null: false
|
|
510
|
+
# Addressed by an unguessable token rather than a sequential id, so a
|
|
511
|
+
# shared link can't be walked to the neighbouring artifact.
|
|
512
|
+
t.string :token, limit: 64, null: false
|
|
513
|
+
t.integer :session_id
|
|
514
|
+
t.string :created_by, limit: 255
|
|
515
|
+
t.datetime :created_at, null: false
|
|
516
|
+
t.datetime :updated_at, null: false
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
conn.add_index(table, :token, unique: true)
|
|
520
|
+
conn.add_index(table, :slug, unique: true)
|
|
521
|
+
conn.add_index(table, :created_at)
|
|
522
|
+
conn.add_index(table, :session_id)
|
|
523
|
+
$stdout.puts "\e[32mRailsConsoleAi: created #{table} table.\e[0m"
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
# Brings an artifacts table created before slugs existed up to date, and
|
|
527
|
+
# backfills a handle for every row already in it.
|
|
528
|
+
def upgrade_artifacts_table!(conn)
|
|
529
|
+
table = 'rails_console_ai_artifacts'
|
|
530
|
+
return [] unless conn.table_exists?(table)
|
|
531
|
+
|
|
532
|
+
added = []
|
|
533
|
+
|
|
534
|
+
unless conn.column_exists?(table, :revision)
|
|
535
|
+
conn.add_column(table, :revision, :integer, default: 1, null: false)
|
|
536
|
+
added << "#{table}.revision"
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
unless conn.column_exists?(table, :slug)
|
|
540
|
+
# Added nullable, backfilled, and only then made unique — an existing
|
|
541
|
+
# table has rows that need a handle before the index can be built.
|
|
542
|
+
conn.add_column(table, :slug, :string, limit: 255)
|
|
543
|
+
backfill_artifact_slugs!(conn, table)
|
|
544
|
+
conn.add_index(table, :slug, unique: true) unless conn.index_exists?(table, :slug)
|
|
545
|
+
added << "#{table}.slug"
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
added
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
def backfill_artifact_slugs!(conn, table)
|
|
552
|
+
require 'rails_console_ai/slash_commands'
|
|
553
|
+
seen = {}
|
|
554
|
+
rows = conn.select_all("SELECT id, name FROM #{table} WHERE slug IS NULL ORDER BY id")
|
|
555
|
+
rows.each do |row|
|
|
556
|
+
base = RailsConsoleAi::SlashCommands.slugify(row['name'])
|
|
557
|
+
base = "artifact-#{row['id']}" if base.empty?
|
|
558
|
+
slug = base
|
|
559
|
+
n = 1
|
|
560
|
+
# Two rows can share a name; the older one keeps the bare handle.
|
|
561
|
+
while seen.key?(slug) || conn.select_value("SELECT 1 FROM #{table} WHERE slug = #{conn.quote(slug)} LIMIT 1")
|
|
562
|
+
n += 1
|
|
563
|
+
slug = "#{base}-#{n}"
|
|
564
|
+
end
|
|
565
|
+
seen[slug] = true
|
|
566
|
+
conn.execute("UPDATE #{table} SET slug = #{conn.quote(slug)} WHERE id = #{row['id'].to_i}")
|
|
567
|
+
end
|
|
568
|
+
end
|
|
569
|
+
|
|
355
570
|
def migrate!
|
|
356
571
|
conn = session_connection
|
|
357
572
|
table = 'rails_console_ai_sessions'
|
|
@@ -435,6 +650,13 @@ module RailsConsoleAi
|
|
|
435
650
|
setup_memories_tables!(conn)
|
|
436
651
|
setup_agents_tables!(conn)
|
|
437
652
|
|
|
653
|
+
if conn.table_exists?('rails_console_ai_artifacts')
|
|
654
|
+
migrations.concat(upgrade_artifacts_table!(conn))
|
|
655
|
+
else
|
|
656
|
+
setup_artifacts_table!(conn)
|
|
657
|
+
migrations << 'rails_console_ai_artifacts'
|
|
658
|
+
end
|
|
659
|
+
|
|
438
660
|
[[:skills, 'rails_console_ai_skills'], [:memories, 'rails_console_ai_memories'], [:agents, 'rails_console_ai_agents']].each do |key, name|
|
|
439
661
|
post = table_columns(conn, name)
|
|
440
662
|
added = post - pre_columns[key]
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rails_console_ai
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.38.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Cortfr
|
|
@@ -106,6 +106,7 @@ files:
|
|
|
106
106
|
- app/controllers/rails_console_ai/agent_versions_controller.rb
|
|
107
107
|
- app/controllers/rails_console_ai/agents_controller.rb
|
|
108
108
|
- app/controllers/rails_console_ai/application_controller.rb
|
|
109
|
+
- app/controllers/rails_console_ai/artifacts_controller.rb
|
|
109
110
|
- app/controllers/rails_console_ai/memories_controller.rb
|
|
110
111
|
- app/controllers/rails_console_ai/memory_versions_controller.rb
|
|
111
112
|
- app/controllers/rails_console_ai/sessions_controller.rb
|
|
@@ -115,6 +116,7 @@ files:
|
|
|
115
116
|
- app/helpers/rails_console_ai/sessions_helper.rb
|
|
116
117
|
- app/models/rails_console_ai/agent.rb
|
|
117
118
|
- app/models/rails_console_ai/agent_version.rb
|
|
119
|
+
- app/models/rails_console_ai/artifact.rb
|
|
118
120
|
- app/models/rails_console_ai/memory.rb
|
|
119
121
|
- app/models/rails_console_ai/memory_version.rb
|
|
120
122
|
- app/models/rails_console_ai/session.rb
|
|
@@ -129,6 +131,8 @@ files:
|
|
|
129
131
|
- app/views/rails_console_ai/agents/index.html.erb
|
|
130
132
|
- app/views/rails_console_ai/agents/new.html.erb
|
|
131
133
|
- app/views/rails_console_ai/agents/show.html.erb
|
|
134
|
+
- app/views/rails_console_ai/artifacts/index.html.erb
|
|
135
|
+
- app/views/rails_console_ai/artifacts/show.html.erb
|
|
132
136
|
- app/views/rails_console_ai/memories/_form.html.erb
|
|
133
137
|
- app/views/rails_console_ai/memories/diff.html.erb
|
|
134
138
|
- app/views/rails_console_ai/memories/edit.html.erb
|