rails_console_ai 0.30.0 → 0.31.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 +6 -0
- data/README.md +18 -1
- data/app/controllers/rails_console_ai/agent_versions_controller.rb +1 -8
- data/app/controllers/rails_console_ai/agents_controller.rb +24 -47
- data/app/controllers/rails_console_ai/memories_controller.rb +13 -36
- data/app/controllers/rails_console_ai/memory_versions_controller.rb +1 -5
- data/app/controllers/rails_console_ai/skill_versions_controller.rb +1 -7
- data/app/controllers/rails_console_ai/skills_controller.rb +18 -47
- data/app/models/rails_console_ai/agent.rb +33 -65
- data/app/models/rails_console_ai/agent_version.rb +8 -20
- data/app/models/rails_console_ai/memory.rb +28 -23
- data/app/models/rails_console_ai/memory_version.rb +5 -20
- data/app/models/rails_console_ai/skill.rb +55 -105
- data/app/models/rails_console_ai/skill_version.rb +7 -28
- data/app/views/rails_console_ai/agents/_form.html.erb +9 -34
- data/app/views/rails_console_ai/agents/diff.html.erb +1 -5
- data/app/views/rails_console_ai/agents/new.html.erb +0 -16
- data/app/views/rails_console_ai/memories/_form.html.erb +6 -13
- data/app/views/rails_console_ai/memories/diff.html.erb +1 -5
- data/app/views/rails_console_ai/memories/new.html.erb +0 -15
- data/app/views/rails_console_ai/skills/_form.html.erb +7 -29
- data/app/views/rails_console_ai/skills/diff.html.erb +1 -8
- data/app/views/rails_console_ai/skills/new.html.erb +0 -17
- data/config/routes.rb +0 -3
- data/lib/rails_console_ai/agent_loader.rb +18 -10
- data/lib/rails_console_ai/agent_runner.rb +55 -4
- data/lib/rails_console_ai/session_logger.rb +4 -0
- data/lib/rails_console_ai/skill_loader.rb +18 -9
- data/lib/rails_console_ai/storage/database_storage.rb +14 -20
- data/lib/rails_console_ai/tools/memory_tools.rb +8 -0
- data/lib/rails_console_ai/version.rb +1 -1
- data/lib/rails_console_ai.rb +54 -70
- metadata +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
require '
|
|
1
|
+
require 'rails_console_ai/tools/memory_tools'
|
|
2
2
|
|
|
3
3
|
module RailsConsoleAi
|
|
4
4
|
class MemoryVersion < ActiveRecord::Base
|
|
@@ -21,26 +21,11 @@ module RailsConsoleAi
|
|
|
21
21
|
end
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
-
def
|
|
25
|
-
|
|
24
|
+
def parsed
|
|
25
|
+
@parsed ||= (RailsConsoleAi::Tools::MemoryTools.parse(content.to_s) || {})
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
def
|
|
29
|
-
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
private
|
|
33
|
-
|
|
34
|
-
def decode_json_array(raw)
|
|
35
|
-
return [] if raw.nil? || (raw.respond_to?(:empty?) && raw.empty?)
|
|
36
|
-
return raw if raw.is_a?(Array)
|
|
37
|
-
JSON.parse(raw)
|
|
38
|
-
rescue JSON::ParserError
|
|
39
|
-
[]
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def encode_json_array(value)
|
|
43
|
-
JSON.dump(Array(value))
|
|
44
|
-
end
|
|
28
|
+
def description; parsed['description']; end
|
|
29
|
+
def tags; Array(parsed['tags']); end
|
|
45
30
|
end
|
|
46
31
|
end
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
require '
|
|
1
|
+
require 'rails_console_ai/skill_loader'
|
|
2
2
|
|
|
3
3
|
module RailsConsoleAi
|
|
4
4
|
class Skill < ActiveRecord::Base
|
|
@@ -8,19 +8,18 @@ module RailsConsoleAi
|
|
|
8
8
|
STATUS_APPROVED = 'approved'.freeze
|
|
9
9
|
STATUSES = [STATUS_PROPOSED, STATUS_APPROVED].freeze
|
|
10
10
|
|
|
11
|
-
# Attributes that, if changed, invalidate the current approval and revert
|
|
12
|
-
# the skill back to "proposed". Status / approver columns are excluded so
|
|
13
|
-
# that an explicit approve! call doesn't reset its own approval.
|
|
14
|
-
CONTENT_ATTRIBUTES = %w[name description body tags bypass_guards_for_methods].freeze
|
|
15
|
-
|
|
16
11
|
has_many :versions,
|
|
17
12
|
-> { order(created_at: :desc) },
|
|
18
13
|
class_name: 'RailsConsoleAi::SkillVersion',
|
|
19
14
|
foreign_key: :skill_id,
|
|
20
15
|
dependent: :nullify
|
|
21
16
|
|
|
17
|
+
validates :content, presence: true
|
|
22
18
|
validates :name, presence: true, uniqueness: { case_sensitive: false }
|
|
23
|
-
validates :status, inclusion: { in: STATUSES }
|
|
19
|
+
validates :status, inclusion: { in: STATUSES }
|
|
20
|
+
validate :content_parses
|
|
21
|
+
|
|
22
|
+
before_validation :sync_name_from_content
|
|
24
23
|
|
|
25
24
|
scope :alphabetical, -> { order(Arel.sql('LOWER(name)')) }
|
|
26
25
|
scope :approved, -> { where(status: STATUS_APPROVED) }
|
|
@@ -36,42 +35,38 @@ module RailsConsoleAi
|
|
|
36
35
|
end
|
|
37
36
|
end
|
|
38
37
|
|
|
39
|
-
#
|
|
40
|
-
#
|
|
41
|
-
def
|
|
42
|
-
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def tags=(value)
|
|
46
|
-
write_attribute(:tags, encode_json_array(value))
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def bypass_guards_for_methods
|
|
50
|
-
decode_json_array(read_attribute(:bypass_guards_for_methods))
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def bypass_guards_for_methods=(value)
|
|
54
|
-
write_attribute(:bypass_guards_for_methods, encode_json_array(value))
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
# Defensive accessors — if `ai_db_migrate` hasn't been run yet, the status
|
|
58
|
-
# / approval columns may be missing on an older table. Return safe defaults
|
|
59
|
-
# instead of blowing up with NameError.
|
|
60
|
-
def status
|
|
61
|
-
has_attribute_status? ? read_attribute(:status) : STATUS_PROPOSED
|
|
38
|
+
# Parsed view of the raw markdown content. Memoized per-instance and cleared
|
|
39
|
+
# on assignment. Returns {} for invalid content so callers don't need to nil-guard.
|
|
40
|
+
def parsed
|
|
41
|
+
@parsed ||= (RailsConsoleAi::SkillLoader.parse(content.to_s) || {})
|
|
62
42
|
end
|
|
63
43
|
|
|
64
|
-
def
|
|
65
|
-
|
|
44
|
+
def content=(value)
|
|
45
|
+
@parsed = nil
|
|
46
|
+
super
|
|
66
47
|
end
|
|
67
48
|
|
|
68
|
-
def
|
|
69
|
-
|
|
70
|
-
end
|
|
49
|
+
def description; parsed['description']; end
|
|
50
|
+
def body; parsed['body']; end
|
|
51
|
+
def tags; Array(parsed['tags']); end
|
|
52
|
+
def bypass_guards_for_methods; Array(parsed['bypass_guards_for_methods']); end
|
|
71
53
|
|
|
72
54
|
def proposed?; status.to_s == STATUS_PROPOSED; end
|
|
73
55
|
def approved?; status.to_s == STATUS_APPROVED; end
|
|
74
56
|
|
|
57
|
+
# Atomically bump use_count + last_used_at without firing callbacks /
|
|
58
|
+
# validations / updated_at. Safe to call from concurrent AI tool calls.
|
|
59
|
+
def self.record_use!(id)
|
|
60
|
+
where(id: id).update_all([
|
|
61
|
+
'use_count = COALESCE(use_count, 0) + 1, last_used_at = ?',
|
|
62
|
+
Time.now.utc
|
|
63
|
+
])
|
|
64
|
+
true
|
|
65
|
+
rescue ::ActiveRecord::ActiveRecordError => e
|
|
66
|
+
RailsConsoleAi.logger.warn("RailsConsoleAi::Skill.record_use!(#{id.inspect}) failed: #{e.message}")
|
|
67
|
+
false
|
|
68
|
+
end
|
|
69
|
+
|
|
75
70
|
def to_hash
|
|
76
71
|
{
|
|
77
72
|
'id' => id,
|
|
@@ -80,6 +75,7 @@ module RailsConsoleAi
|
|
|
80
75
|
'body' => body,
|
|
81
76
|
'tags' => tags,
|
|
82
77
|
'bypass_guards_for_methods' => bypass_guards_for_methods,
|
|
78
|
+
'content' => content,
|
|
83
79
|
'status' => status,
|
|
84
80
|
'approved_by' => approved_by,
|
|
85
81
|
'approved_at' => approved_at,
|
|
@@ -90,66 +86,14 @@ module RailsConsoleAi
|
|
|
90
86
|
}
|
|
91
87
|
end
|
|
92
88
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
# Atomically bump use_count + last_used_at without firing callbacks /
|
|
98
|
-
# validations / updated_at. Safe to call from concurrent AI tool calls.
|
|
99
|
-
# No-op (returns false) if the table doesn't have the columns yet — that
|
|
100
|
-
# keeps older installs working until they run ai_db_migrate.
|
|
101
|
-
def self.record_use!(id)
|
|
102
|
-
return false unless connection.column_exists?(table_name, :use_count)
|
|
103
|
-
where(id: id).update_all([
|
|
104
|
-
'use_count = COALESCE(use_count, 0) + 1, last_used_at = ?',
|
|
105
|
-
Time.now.utc
|
|
106
|
-
])
|
|
107
|
-
true
|
|
108
|
-
rescue ::ActiveRecord::ActiveRecordError => e
|
|
109
|
-
RailsConsoleAi.logger.warn("RailsConsoleAi::Skill.record_use!(#{id.inspect}) failed: #{e.message}")
|
|
110
|
-
false
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
def use_count
|
|
114
|
-
has_attribute?(:use_count) ? (read_attribute(:use_count) || 0) : 0
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
def last_used_at
|
|
118
|
-
has_attribute?(:last_used_at) ? read_attribute(:last_used_at) : nil
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
def self.decode_json_array(raw)
|
|
122
|
-
return [] if raw.nil? || (raw.respond_to?(:empty?) && raw.empty?)
|
|
123
|
-
return raw if raw.is_a?(Array)
|
|
124
|
-
JSON.parse(raw)
|
|
125
|
-
rescue JSON::ParserError
|
|
126
|
-
[]
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
def self.encode_json_array(value)
|
|
130
|
-
JSON.dump(Array(value))
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
def decode_json_array(raw)
|
|
134
|
-
self.class.decode_json_array(raw)
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
def encode_json_array(value)
|
|
138
|
-
self.class.encode_json_array(value)
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
# Assigns attrs, saves, and records one SkillVersion snapshot of the post-save state.
|
|
142
|
-
# Every save produces exactly one version row, so the version log is a complete history
|
|
143
|
-
# including the current state (the most recent version mirrors `self`).
|
|
144
|
-
#
|
|
145
|
-
# If `preserve_approval` is false (the default), any change to a content attribute
|
|
146
|
-
# reverts the skill back to "proposed" and clears the approver. Pass true from the
|
|
147
|
-
# approve! flow so approval doesn't reset itself.
|
|
89
|
+
# Assigns attrs, saves, and records one SkillVersion snapshot.
|
|
90
|
+
# Any change to `content` reverts approval back to "proposed" unless
|
|
91
|
+
# `preserve_approval: true` is passed (approve! does this).
|
|
148
92
|
def update_with_version!(attrs, edited_by: nil, change_note: nil, preserve_approval: false)
|
|
149
93
|
transaction do
|
|
150
94
|
assign_attributes(attrs)
|
|
151
95
|
|
|
152
|
-
if !preserve_approval && approved? &&
|
|
96
|
+
if !preserve_approval && approved? && changes.key?('content')
|
|
153
97
|
self.status = STATUS_PROPOSED
|
|
154
98
|
self.approved_by = nil
|
|
155
99
|
self.approved_at = nil
|
|
@@ -157,22 +101,17 @@ module RailsConsoleAi
|
|
|
157
101
|
|
|
158
102
|
save!
|
|
159
103
|
RailsConsoleAi::SkillVersion.create!(
|
|
160
|
-
skill_id:
|
|
161
|
-
name:
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
status: status,
|
|
167
|
-
edited_by: edited_by,
|
|
168
|
-
change_note: change_note
|
|
104
|
+
skill_id: id,
|
|
105
|
+
name: name,
|
|
106
|
+
content: content,
|
|
107
|
+
status: status,
|
|
108
|
+
edited_by: edited_by,
|
|
109
|
+
change_note: change_note
|
|
169
110
|
)
|
|
170
111
|
end
|
|
171
112
|
self
|
|
172
113
|
end
|
|
173
114
|
|
|
174
|
-
# Marks the current head as approved. Logs a version row with the approver name
|
|
175
|
-
# so the audit trail captures the approval moment.
|
|
176
115
|
def approve!(approved_by:)
|
|
177
116
|
raise ArgumentError, 'approved_by is required' if approved_by.to_s.strip.empty?
|
|
178
117
|
|
|
@@ -190,9 +129,20 @@ module RailsConsoleAi
|
|
|
190
129
|
|
|
191
130
|
private
|
|
192
131
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
132
|
+
def sync_name_from_content
|
|
133
|
+
return if content.to_s.strip.empty?
|
|
134
|
+
parsed_name = parsed['name'].to_s.strip
|
|
135
|
+
self.name = parsed_name unless parsed_name.empty?
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def content_parses
|
|
139
|
+
return if content.to_s.strip.empty? # presence validator handles blank
|
|
140
|
+
hash = RailsConsoleAi::SkillLoader.parse(content.to_s)
|
|
141
|
+
if hash.nil?
|
|
142
|
+
errors.add(:content, "could not be parsed — expected YAML frontmatter between `---` lines followed by a markdown body")
|
|
143
|
+
elsif hash['name'].to_s.strip.empty?
|
|
144
|
+
errors.add(:content, "frontmatter is missing a `name:` field")
|
|
145
|
+
end
|
|
196
146
|
end
|
|
197
147
|
end
|
|
198
148
|
end
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
require '
|
|
1
|
+
require 'rails_console_ai/skill_loader'
|
|
2
2
|
|
|
3
3
|
module RailsConsoleAi
|
|
4
4
|
class SkillVersion < ActiveRecord::Base
|
|
@@ -21,34 +21,13 @@ module RailsConsoleAi
|
|
|
21
21
|
end
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
-
def
|
|
25
|
-
|
|
24
|
+
def parsed
|
|
25
|
+
@parsed ||= (RailsConsoleAi::SkillLoader.parse(content.to_s) || {})
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
def
|
|
29
|
-
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def bypass_guards_for_methods
|
|
33
|
-
decode_json_array(read_attribute(:bypass_guards_for_methods))
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def bypass_guards_for_methods=(value)
|
|
37
|
-
write_attribute(:bypass_guards_for_methods, encode_json_array(value))
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
private
|
|
41
|
-
|
|
42
|
-
def decode_json_array(raw)
|
|
43
|
-
return [] if raw.nil? || (raw.respond_to?(:empty?) && raw.empty?)
|
|
44
|
-
return raw if raw.is_a?(Array)
|
|
45
|
-
JSON.parse(raw)
|
|
46
|
-
rescue JSON::ParserError
|
|
47
|
-
[]
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def encode_json_array(value)
|
|
51
|
-
JSON.dump(Array(value))
|
|
52
|
-
end
|
|
28
|
+
def description; parsed['description']; end
|
|
29
|
+
def body; parsed['body']; end
|
|
30
|
+
def tags; Array(parsed['tags']); end
|
|
31
|
+
def bypass_guards_for_methods; Array(parsed['bypass_guards_for_methods']); end
|
|
53
32
|
end
|
|
54
33
|
end
|
|
@@ -1,47 +1,23 @@
|
|
|
1
1
|
<% if @agent.persisted? && @agent.respond_to?(:approved?) && @agent.approved? %>
|
|
2
2
|
<div class="flash flash-alert" style="margin-bottom:16px;">
|
|
3
|
-
Heads up: this agent is currently <strong>approved</strong>. Editing it will revert its status to <strong>proposed</strong
|
|
3
|
+
Heads up: this agent is currently <strong>approved</strong>. Editing it will revert its status to <strong>proposed</strong>.
|
|
4
4
|
</div>
|
|
5
5
|
<% elsif !@agent.persisted? %>
|
|
6
6
|
<div class="flash flash-notice" style="margin-bottom:16px;">
|
|
7
|
-
New DB agents start as <strong>proposed</strong>. After saving, an approver (likely you) needs to click the Approve button
|
|
7
|
+
New DB agents start as <strong>proposed</strong>. After saving, an approver (likely you) needs to click the Approve button before delegate_task can invoke it.
|
|
8
8
|
</div>
|
|
9
9
|
<% end %>
|
|
10
10
|
|
|
11
11
|
<%= form_with model: @agent, url: form_url, method: form_method, local: true do |f| %>
|
|
12
12
|
<div class="form-card">
|
|
13
13
|
<div class="form-row">
|
|
14
|
-
<label>
|
|
15
|
-
<%= f.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
</div>
|
|
22
|
-
|
|
23
|
-
<div class="form-row">
|
|
24
|
-
<label>Body / system instructions (markdown)</label>
|
|
25
|
-
<%= f.text_area :body, value: @agent.body, rows: 18 %>
|
|
26
|
-
<div class="hint">Persona + strategy + rules. Be specific about what tools/approaches the sub-agent should use and what format its summary should take.</div>
|
|
27
|
-
</div>
|
|
28
|
-
|
|
29
|
-
<div class="form-row">
|
|
30
|
-
<label>Max rounds (optional)</label>
|
|
31
|
-
<%= f.number_field :max_rounds, value: @agent.max_rounds, min: 1, max: 100 %>
|
|
32
|
-
<div class="hint">Maximum tool-loop iterations for the sub-agent. Defaults to the global <code>config.sub_agent_max_rounds</code>. Smaller = tighter scope, fewer tokens.</div>
|
|
33
|
-
</div>
|
|
34
|
-
|
|
35
|
-
<div class="form-row">
|
|
36
|
-
<label>Model (optional)</label>
|
|
37
|
-
<%= f.text_field :model, value: @agent.model, placeholder: 'e.g. claude-haiku-4-5' %>
|
|
38
|
-
<div class="hint">Model override. Leave blank to use the global model. Useful for cheap/fast agents.</div>
|
|
39
|
-
</div>
|
|
40
|
-
|
|
41
|
-
<div class="form-row">
|
|
42
|
-
<label>Tool whitelist (one per line, optional)</label>
|
|
43
|
-
<%= f.text_area :tools, value: Array(@agent.tools).join("\n"), rows: 4 %>
|
|
44
|
-
<div class="hint">Tool names the sub-agent is allowed to call (e.g. <code>execute_code</code>, <code>search_code</code>, <code>read_file</code>). Leave blank to use the default tool set. Adding tools here can broaden what the sub-agent can do — this is why DB agents need approval.</div>
|
|
14
|
+
<label>Markdown (frontmatter + body)</label>
|
|
15
|
+
<%= f.text_area :content, value: @agent.content, rows: 28, required: true, style: 'font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 13px;' %>
|
|
16
|
+
<div class="hint">
|
|
17
|
+
YAML frontmatter between <code>---</code> lines, then the agent persona / instructions. Required:
|
|
18
|
+
<code>name</code>, <code>description</code>. Optional: <code>max_rounds</code>, <code>model</code>,
|
|
19
|
+
<code>tools</code> (whitelist; empty/omitted = default set).
|
|
20
|
+
</div>
|
|
45
21
|
</div>
|
|
46
22
|
|
|
47
23
|
<hr style="margin: 20px 0; border: 0; border-top: 1px solid #eee;">
|
|
@@ -49,7 +25,6 @@
|
|
|
49
25
|
<div class="form-row">
|
|
50
26
|
<label>Edited by</label>
|
|
51
27
|
<input type="text" name="edited_by" value="<%= params[:edited_by] %>" placeholder="Your name or handle">
|
|
52
|
-
<div class="hint">Recorded with this version. Free text — the gem does not assume authentication beyond the admin login.</div>
|
|
53
28
|
</div>
|
|
54
29
|
|
|
55
30
|
<div class="form-row">
|
|
@@ -5,11 +5,7 @@
|
|
|
5
5
|
Version #<%= @from.id %> (<%= @from.created_at.strftime('%Y-%m-%d %H:%M') %>) → <%= @to_label %>
|
|
6
6
|
</p>
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
<%= render_text_diff(@from.body, @to_body, left_label: "v##{@from.id}", right_label: @to_label) %>
|
|
10
|
-
|
|
11
|
-
<h3 style="margin:24px 0 6px;">Tool whitelist</h3>
|
|
12
|
-
<%= render_json_diff(Array(@from.tools), @to_tools, left_label: "v##{@from.id}", right_label: @to_label) %>
|
|
8
|
+
<%= render_text_diff(@from.content, @to_content, left_label: "v##{@from.id}", right_label: @to_label) %>
|
|
13
9
|
|
|
14
10
|
<div class="btn-bar" style="margin-top:24px;">
|
|
15
11
|
<% if @to.nil? %>
|
|
@@ -1,22 +1,6 @@
|
|
|
1
1
|
<%= link_to '← All agents', agents_path, class: 'back-link' %>
|
|
2
2
|
<h2 style="margin-bottom:16px;">New agent</h2>
|
|
3
3
|
|
|
4
|
-
<%= form_with url: import_agents_path, method: :post, local: true do %>
|
|
5
|
-
<div class="form-card" style="background:#f8f9fa;">
|
|
6
|
-
<div class="form-row" style="margin-bottom:8px;">
|
|
7
|
-
<label>Paste a .md file (optional)</label>
|
|
8
|
-
<textarea name="content" rows="14" placeholder="--- name: My agent description: What it specializes in max_rounds: 10 model: claude-haiku-4-5 tools: [execute_code, search_code] --- Persona, strategy, rules …"></textarea>
|
|
9
|
-
<div class="hint">
|
|
10
|
-
Paste the full contents of a Markdown file (YAML frontmatter + body) — same
|
|
11
|
-
format as <code>.rails_console_ai/agents/*.md</code> or the built-in agents
|
|
12
|
-
shipped with the gem. Useful for moving an existing on-disk agent into the
|
|
13
|
-
versioned DB store.
|
|
14
|
-
</div>
|
|
15
|
-
</div>
|
|
16
|
-
<button type="submit" class="btn btn-secondary">Parse pasted content ↓</button>
|
|
17
|
-
</div>
|
|
18
|
-
<% end %>
|
|
19
|
-
|
|
20
4
|
<%= render 'form',
|
|
21
5
|
form_url: agents_path,
|
|
22
6
|
form_method: :post,
|
|
@@ -1,19 +1,12 @@
|
|
|
1
1
|
<%= form_with model: @memory, url: form_url, method: form_method, local: true do |f| %>
|
|
2
2
|
<div class="form-card">
|
|
3
3
|
<div class="form-row">
|
|
4
|
-
<label>
|
|
5
|
-
<%= f.
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
<%= f.text_area :description, value: @memory.description, rows: 14 %>
|
|
11
|
-
<div class="hint">The fact or pattern you're persisting. Markdown is fine.</div>
|
|
12
|
-
</div>
|
|
13
|
-
|
|
14
|
-
<div class="form-row">
|
|
15
|
-
<label>Tags (comma-separated)</label>
|
|
16
|
-
<%= f.text_field :tags, value: Array(@memory.tags).join(', '), placeholder: 'database, sharding' %>
|
|
4
|
+
<label>Markdown (frontmatter + body)</label>
|
|
5
|
+
<%= f.text_area :content, value: @memory.content, rows: 22, required: true, style: 'font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 13px;' %>
|
|
6
|
+
<div class="hint">
|
|
7
|
+
YAML frontmatter between <code>---</code> lines (required: <code>name</code>; optional: <code>tags</code>),
|
|
8
|
+
then the memory body — the fact or pattern you're persisting.
|
|
9
|
+
</div>
|
|
17
10
|
</div>
|
|
18
11
|
|
|
19
12
|
<hr style="margin: 20px 0; border: 0; border-top: 1px solid #eee;">
|
|
@@ -5,11 +5,7 @@
|
|
|
5
5
|
Version #<%= @from.id %> (<%= @from.created_at.strftime('%Y-%m-%d %H:%M') %>) → <%= @to_label %>
|
|
6
6
|
</p>
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
<%= render_text_diff(@from.description, @to_description, left_label: "v##{@from.id}", right_label: @to_label) %>
|
|
10
|
-
|
|
11
|
-
<h3 style="margin:24px 0 6px;">Tags</h3>
|
|
12
|
-
<%= render_json_diff(Array(@from.tags), @to_tags, left_label: "v##{@from.id}", right_label: @to_label) %>
|
|
8
|
+
<%= render_text_diff(@from.content, @to_content, left_label: "v##{@from.id}", right_label: @to_label) %>
|
|
13
9
|
|
|
14
10
|
<div class="btn-bar" style="margin-top:24px;">
|
|
15
11
|
<% if @to.nil? %>
|
|
@@ -1,21 +1,6 @@
|
|
|
1
1
|
<%= link_to '← All memories', memories_path, class: 'back-link' %>
|
|
2
2
|
<h2 style="margin-bottom:16px;">New memory</h2>
|
|
3
3
|
|
|
4
|
-
<%= form_with url: import_memories_path, method: :post, local: true do %>
|
|
5
|
-
<div class="form-card" style="background:#f8f9fa;">
|
|
6
|
-
<div class="form-row" style="margin-bottom:8px;">
|
|
7
|
-
<label>Paste a .md file (optional)</label>
|
|
8
|
-
<textarea name="content" rows="12" placeholder="--- name: My memory tags: [database] --- The fact or pattern …"></textarea>
|
|
9
|
-
<div class="hint">
|
|
10
|
-
Paste the full contents of a Markdown file (YAML frontmatter + body) — same
|
|
11
|
-
format as <code>.rails_console_ai/memories/*.md</code>. The fields below will
|
|
12
|
-
be prefilled from the paste.
|
|
13
|
-
</div>
|
|
14
|
-
</div>
|
|
15
|
-
<button type="submit" class="btn btn-secondary">Parse pasted content ↓</button>
|
|
16
|
-
</div>
|
|
17
|
-
<% end %>
|
|
18
|
-
|
|
19
4
|
<%= render 'form',
|
|
20
5
|
form_url: memories_path,
|
|
21
6
|
form_method: :post,
|
|
@@ -11,36 +11,14 @@
|
|
|
11
11
|
<%= form_with model: @skill, url: form_url, method: form_method, local: true do |f| %>
|
|
12
12
|
<div class="form-card">
|
|
13
13
|
<div class="form-row">
|
|
14
|
-
<label>
|
|
15
|
-
<%= f.
|
|
16
|
-
</div>
|
|
17
|
-
|
|
18
|
-
<div class="form-row">
|
|
19
|
-
<label>Description</label>
|
|
20
|
-
<%= f.text_field :description, value: @skill.description, placeholder: 'One-line description of when to use this skill' %>
|
|
21
|
-
</div>
|
|
22
|
-
|
|
23
|
-
<div class="form-row">
|
|
24
|
-
<label>Body (markdown)</label>
|
|
25
|
-
<%= f.text_area :body, value: @skill.body, rows: 18 %>
|
|
26
|
-
<div class="hint">Include "## When to use", "## Recipe" (numbered steps with code blocks), "## Notes".</div>
|
|
27
|
-
</div>
|
|
28
|
-
|
|
29
|
-
<div class="form-row">
|
|
30
|
-
<label>Tags (comma-separated)</label>
|
|
31
|
-
<%= f.text_field :tags, value: Array(@skill.tags).join(', '), placeholder: 'booking-page, admin' %>
|
|
32
|
-
</div>
|
|
33
|
-
|
|
34
|
-
<div class="form-row">
|
|
35
|
-
<label>Bypass guards for methods (one per line)</label>
|
|
36
|
-
<%= f.text_area :bypass_guards_for_methods, value: Array(@skill.bypass_guards_for_methods).join("\n"), rows: 4 %>
|
|
14
|
+
<label>Markdown (frontmatter + body)</label>
|
|
15
|
+
<%= f.text_area :content, value: @skill.content, rows: 28, required: true, style: 'font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 13px;' %>
|
|
37
16
|
<div class="hint">
|
|
38
|
-
|
|
39
|
-
<
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
Namespaced classes work: <code>Acme::Booking#destroy</code>. The <code>!</code> is part of the match, so <code>save</code> and <code>save!</code> are different specs. While the skill is active, calls to these methods skip the configured safety guards (DB writes, HTTP mutations, email, etc.) <strong>for that tool-call only</strong>.
|
|
17
|
+
YAML frontmatter between <code>---</code> lines, then the markdown body. Required frontmatter:
|
|
18
|
+
<code>name</code>, <code>description</code>. Optional: <code>tags</code>,
|
|
19
|
+
<code>bypass_guards_for_methods</code> (entries shaped like <code>ClassName#method</code> or
|
|
20
|
+
<code>ClassName.method</code> — while the skill is active, calls to these methods skip the
|
|
21
|
+
configured safety guards for that tool-call only).
|
|
44
22
|
</div>
|
|
45
23
|
</div>
|
|
46
24
|
|
|
@@ -5,14 +5,7 @@
|
|
|
5
5
|
Version #<%= @from.id %> (<%= @from.created_at.strftime('%Y-%m-%d %H:%M') %>) → <%= @to_label %>
|
|
6
6
|
</p>
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
<%= render_text_diff(@from.body, @to_body, left_label: "v##{@from.id}", right_label: @to_label) %>
|
|
10
|
-
|
|
11
|
-
<h3 style="margin:24px 0 6px;">Tags</h3>
|
|
12
|
-
<%= render_json_diff(Array(@from.tags), @to_tags, left_label: "v##{@from.id}", right_label: @to_label) %>
|
|
13
|
-
|
|
14
|
-
<h3 style="margin:24px 0 6px;">Bypass guards</h3>
|
|
15
|
-
<%= render_json_diff(Array(@from.bypass_guards_for_methods), @to_bypass, left_label: "v##{@from.id}", right_label: @to_label) %>
|
|
8
|
+
<%= render_text_diff(@from.content, @to_content, left_label: "v##{@from.id}", right_label: @to_label) %>
|
|
16
9
|
|
|
17
10
|
<div class="btn-bar" style="margin-top:24px;">
|
|
18
11
|
<% if @to.nil? %>
|
|
@@ -1,23 +1,6 @@
|
|
|
1
1
|
<%= link_to '← All skills', skills_path, class: 'back-link' %>
|
|
2
2
|
<h2 style="margin-bottom:16px;">New skill</h2>
|
|
3
3
|
|
|
4
|
-
<%= form_with url: import_skills_path, method: :post, local: true do %>
|
|
5
|
-
<div class="form-card" style="background:#f8f9fa;">
|
|
6
|
-
<div class="form-row" style="margin-bottom:8px;">
|
|
7
|
-
<label>Paste a .md file (optional)</label>
|
|
8
|
-
<textarea name="content" rows="14" placeholder="--- name: My skill description: What it does tags: [admin] bypass_guards_for_methods: [] --- ## When to use … ## Recipe 1. …"></textarea>
|
|
9
|
-
<div class="hint">
|
|
10
|
-
Paste the full contents of a Markdown file (YAML frontmatter between
|
|
11
|
-
<code>---</code> lines, then a markdown body) — same format as
|
|
12
|
-
<code>.rails_console_ai/skills/*.md</code>. The fields below will be
|
|
13
|
-
prefilled from the paste. You can review and tweak before clicking
|
|
14
|
-
<strong>Create skill</strong>.
|
|
15
|
-
</div>
|
|
16
|
-
</div>
|
|
17
|
-
<button type="submit" class="btn btn-secondary">Parse pasted content ↓</button>
|
|
18
|
-
</div>
|
|
19
|
-
<% end %>
|
|
20
|
-
|
|
21
4
|
<%= render 'form',
|
|
22
5
|
form_url: skills_path,
|
|
23
6
|
form_method: :post,
|
data/config/routes.rb
CHANGED
|
@@ -8,7 +8,6 @@ RailsConsoleAi::Engine.routes.draw do
|
|
|
8
8
|
end
|
|
9
9
|
collection do
|
|
10
10
|
get :diff
|
|
11
|
-
post :import
|
|
12
11
|
end
|
|
13
12
|
resources :versions, only: [:index, :show], controller: 'skill_versions' do
|
|
14
13
|
member do
|
|
@@ -20,7 +19,6 @@ RailsConsoleAi::Engine.routes.draw do
|
|
|
20
19
|
resources :memories do
|
|
21
20
|
collection do
|
|
22
21
|
get :diff
|
|
23
|
-
post :import
|
|
24
22
|
end
|
|
25
23
|
resources :versions, only: [:index, :show], controller: 'memory_versions' do
|
|
26
24
|
member do
|
|
@@ -35,7 +33,6 @@ RailsConsoleAi::Engine.routes.draw do
|
|
|
35
33
|
end
|
|
36
34
|
collection do
|
|
37
35
|
get :diff
|
|
38
|
-
post :import
|
|
39
36
|
end
|
|
40
37
|
resources :versions, only: [:index, :show], controller: 'agent_versions' do
|
|
41
38
|
member do
|
|
@@ -159,16 +159,10 @@ module RailsConsoleAi
|
|
|
159
159
|
key = agent_key(name)
|
|
160
160
|
existing = load_agent_file(key)
|
|
161
161
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
frontmatter['max_rounds'] = max_rounds if max_rounds
|
|
167
|
-
frontmatter['model'] = model if model
|
|
168
|
-
tool_list = Array(tools)
|
|
169
|
-
frontmatter['tools'] = tool_list unless tool_list.empty?
|
|
170
|
-
|
|
171
|
-
content = "---\n#{YAML.dump(frontmatter).sub("---\n", '').strip}\n---\n\n#{body}\n"
|
|
162
|
+
content = self.class.dump(
|
|
163
|
+
name: name, description: description, body: body,
|
|
164
|
+
max_rounds: max_rounds, model: model, tools: tools
|
|
165
|
+
)
|
|
172
166
|
@storage.write(key, content)
|
|
173
167
|
|
|
174
168
|
path = @storage.respond_to?(:root_path) ? File.join(@storage.root_path, key) : key
|
|
@@ -210,5 +204,19 @@ module RailsConsoleAi
|
|
|
210
204
|
rescue Psych::SyntaxError
|
|
211
205
|
nil
|
|
212
206
|
end
|
|
207
|
+
|
|
208
|
+
# Inverse of parse: emit a canonical .md (frontmatter + body) string from
|
|
209
|
+
# structured attrs.
|
|
210
|
+
def self.dump(name:, description:, body:, max_rounds: nil, model: nil, tools: nil)
|
|
211
|
+
frontmatter = {
|
|
212
|
+
'name' => name,
|
|
213
|
+
'description' => description
|
|
214
|
+
}
|
|
215
|
+
frontmatter['max_rounds'] = max_rounds if max_rounds
|
|
216
|
+
frontmatter['model'] = model if model
|
|
217
|
+
tool_list = Array(tools)
|
|
218
|
+
frontmatter['tools'] = tool_list unless tool_list.empty?
|
|
219
|
+
"---\n#{YAML.dump(frontmatter).sub("---\n", '').strip}\n---\n\n#{body}\n"
|
|
220
|
+
end
|
|
213
221
|
end
|
|
214
222
|
end
|