rails_console_ai 0.29.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 +46 -0
- data/README.md +65 -0
- data/app/controllers/rails_console_ai/agent_versions_controller.rb +29 -0
- data/app/controllers/rails_console_ai/agents_controller.rb +176 -0
- data/app/controllers/rails_console_ai/application_controller.rb +5 -0
- data/app/controllers/rails_console_ai/memories_controller.rb +136 -0
- data/app/controllers/rails_console_ai/memory_versions_controller.rb +29 -0
- data/app/controllers/rails_console_ai/skill_versions_controller.rb +29 -0
- data/app/controllers/rails_console_ai/skills_controller.rb +171 -0
- data/app/helpers/rails_console_ai/diff_helper.rb +114 -0
- data/app/models/rails_console_ai/agent.rb +143 -0
- data/app/models/rails_console_ai/agent_version.rb +34 -0
- data/app/models/rails_console_ai/memory.rb +103 -0
- data/app/models/rails_console_ai/memory_version.rb +31 -0
- data/app/models/rails_console_ai/session.rb +1 -1
- data/app/models/rails_console_ai/skill.rb +148 -0
- data/app/models/rails_console_ai/skill_version.rb +33 -0
- data/app/views/layouts/rails_console_ai/application.html.erb +78 -1
- data/app/views/rails_console_ai/agent_versions/index.html.erb +28 -0
- data/app/views/rails_console_ai/agent_versions/show.html.erb +25 -0
- data/app/views/rails_console_ai/agents/_form.html.erb +40 -0
- data/app/views/rails_console_ai/agents/diff.html.erb +15 -0
- data/app/views/rails_console_ai/agents/edit.html.erb +7 -0
- data/app/views/rails_console_ai/agents/index.html.erb +80 -0
- data/app/views/rails_console_ai/agents/new.html.erb +8 -0
- data/app/views/rails_console_ai/agents/show.html.erb +108 -0
- data/app/views/rails_console_ai/memories/_form.html.erb +29 -0
- data/app/views/rails_console_ai/memories/diff.html.erb +15 -0
- data/app/views/rails_console_ai/memories/edit.html.erb +7 -0
- data/app/views/rails_console_ai/memories/index.html.erb +67 -0
- data/app/views/rails_console_ai/memories/new.html.erb +8 -0
- data/app/views/rails_console_ai/memories/show.html.erb +65 -0
- data/app/views/rails_console_ai/memory_versions/index.html.erb +26 -0
- data/app/views/rails_console_ai/memory_versions/show.html.erb +21 -0
- data/app/views/rails_console_ai/skill_versions/index.html.erb +28 -0
- data/app/views/rails_console_ai/skill_versions/show.html.erb +23 -0
- data/app/views/rails_console_ai/skills/_form.html.erb +43 -0
- data/app/views/rails_console_ai/skills/diff.html.erb +15 -0
- data/app/views/rails_console_ai/skills/edit.html.erb +7 -0
- data/app/views/rails_console_ai/skills/index.html.erb +79 -0
- data/app/views/rails_console_ai/skills/new.html.erb +8 -0
- data/app/views/rails_console_ai/skills/show.html.erb +94 -0
- data/config/routes.rb +39 -0
- data/lib/rails_console_ai/agent_loader.rb +139 -43
- data/lib/rails_console_ai/agent_runner.rb +209 -0
- data/lib/rails_console_ai/channel/api.rb +139 -0
- data/lib/rails_console_ai/conversation_engine.rb +19 -13
- data/lib/rails_console_ai/session_logger.rb +10 -0
- data/lib/rails_console_ai/skill_loader.rb +130 -29
- data/lib/rails_console_ai/storage/database_storage.rb +195 -0
- data/lib/rails_console_ai/tools/memory_tools.rb +110 -32
- data/lib/rails_console_ai/tools/registry.rb +99 -8
- data/lib/rails_console_ai/version.rb +1 -1
- data/lib/rails_console_ai.rb +240 -0
- data/lib/tasks/rails_console_ai.rake +7 -0
- metadata +55 -1
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
require 'rails_console_ai/skill_loader'
|
|
2
|
+
|
|
3
|
+
module RailsConsoleAi
|
|
4
|
+
class Skill < ActiveRecord::Base
|
|
5
|
+
self.table_name = 'rails_console_ai_skills'
|
|
6
|
+
|
|
7
|
+
STATUS_PROPOSED = 'proposed'.freeze
|
|
8
|
+
STATUS_APPROVED = 'approved'.freeze
|
|
9
|
+
STATUSES = [STATUS_PROPOSED, STATUS_APPROVED].freeze
|
|
10
|
+
|
|
11
|
+
has_many :versions,
|
|
12
|
+
-> { order(created_at: :desc) },
|
|
13
|
+
class_name: 'RailsConsoleAi::SkillVersion',
|
|
14
|
+
foreign_key: :skill_id,
|
|
15
|
+
dependent: :nullify
|
|
16
|
+
|
|
17
|
+
validates :content, presence: true
|
|
18
|
+
validates :name, presence: true, uniqueness: { case_sensitive: false }
|
|
19
|
+
validates :status, inclusion: { in: STATUSES }
|
|
20
|
+
validate :content_parses
|
|
21
|
+
|
|
22
|
+
before_validation :sync_name_from_content
|
|
23
|
+
|
|
24
|
+
scope :alphabetical, -> { order(Arel.sql('LOWER(name)')) }
|
|
25
|
+
scope :approved, -> { where(status: STATUS_APPROVED) }
|
|
26
|
+
scope :proposed, -> { where(status: STATUS_PROPOSED) }
|
|
27
|
+
|
|
28
|
+
def self.connection
|
|
29
|
+
klass = RailsConsoleAi.configuration.connection_class
|
|
30
|
+
if klass
|
|
31
|
+
klass = Object.const_get(klass) if klass.is_a?(String)
|
|
32
|
+
klass.connection
|
|
33
|
+
else
|
|
34
|
+
super
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
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) || {})
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def content=(value)
|
|
45
|
+
@parsed = nil
|
|
46
|
+
super
|
|
47
|
+
end
|
|
48
|
+
|
|
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
|
|
53
|
+
|
|
54
|
+
def proposed?; status.to_s == STATUS_PROPOSED; end
|
|
55
|
+
def approved?; status.to_s == STATUS_APPROVED; end
|
|
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
|
+
|
|
70
|
+
def to_hash
|
|
71
|
+
{
|
|
72
|
+
'id' => id,
|
|
73
|
+
'name' => name,
|
|
74
|
+
'description' => description,
|
|
75
|
+
'body' => body,
|
|
76
|
+
'tags' => tags,
|
|
77
|
+
'bypass_guards_for_methods' => bypass_guards_for_methods,
|
|
78
|
+
'content' => content,
|
|
79
|
+
'status' => status,
|
|
80
|
+
'approved_by' => approved_by,
|
|
81
|
+
'approved_at' => approved_at,
|
|
82
|
+
'use_count' => use_count,
|
|
83
|
+
'last_used_at' => last_used_at,
|
|
84
|
+
'source' => :db,
|
|
85
|
+
'updated_at' => updated_at
|
|
86
|
+
}
|
|
87
|
+
end
|
|
88
|
+
|
|
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).
|
|
92
|
+
def update_with_version!(attrs, edited_by: nil, change_note: nil, preserve_approval: false)
|
|
93
|
+
transaction do
|
|
94
|
+
assign_attributes(attrs)
|
|
95
|
+
|
|
96
|
+
if !preserve_approval && approved? && changes.key?('content')
|
|
97
|
+
self.status = STATUS_PROPOSED
|
|
98
|
+
self.approved_by = nil
|
|
99
|
+
self.approved_at = nil
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
save!
|
|
103
|
+
RailsConsoleAi::SkillVersion.create!(
|
|
104
|
+
skill_id: id,
|
|
105
|
+
name: name,
|
|
106
|
+
content: content,
|
|
107
|
+
status: status,
|
|
108
|
+
edited_by: edited_by,
|
|
109
|
+
change_note: change_note
|
|
110
|
+
)
|
|
111
|
+
end
|
|
112
|
+
self
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def approve!(approved_by:)
|
|
116
|
+
raise ArgumentError, 'approved_by is required' if approved_by.to_s.strip.empty?
|
|
117
|
+
|
|
118
|
+
update_with_version!(
|
|
119
|
+
{
|
|
120
|
+
status: STATUS_APPROVED,
|
|
121
|
+
approved_by: approved_by,
|
|
122
|
+
approved_at: Time.now.utc
|
|
123
|
+
},
|
|
124
|
+
edited_by: approved_by,
|
|
125
|
+
change_note: "Approved by #{approved_by}",
|
|
126
|
+
preserve_approval: true
|
|
127
|
+
)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
private
|
|
131
|
+
|
|
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
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
require 'rails_console_ai/skill_loader'
|
|
2
|
+
|
|
3
|
+
module RailsConsoleAi
|
|
4
|
+
class SkillVersion < ActiveRecord::Base
|
|
5
|
+
self.table_name = 'rails_console_ai_skill_versions'
|
|
6
|
+
|
|
7
|
+
belongs_to :skill,
|
|
8
|
+
class_name: 'RailsConsoleAi::Skill',
|
|
9
|
+
foreign_key: :skill_id,
|
|
10
|
+
optional: true
|
|
11
|
+
|
|
12
|
+
scope :recent, -> { order(created_at: :desc) }
|
|
13
|
+
|
|
14
|
+
def self.connection
|
|
15
|
+
klass = RailsConsoleAi.configuration.connection_class
|
|
16
|
+
if klass
|
|
17
|
+
klass = Object.const_get(klass) if klass.is_a?(String)
|
|
18
|
+
klass.connection
|
|
19
|
+
else
|
|
20
|
+
super
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def parsed
|
|
25
|
+
@parsed ||= (RailsConsoleAi::SkillLoader.parse(content.to_s) || {})
|
|
26
|
+
end
|
|
27
|
+
|
|
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
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -70,14 +70,91 @@
|
|
|
70
70
|
.pagination a:hover { background: #f8f9fa; text-decoration: none; }
|
|
71
71
|
.pagination .disabled { padding: 6px 14px; font-size: 13px; color: #ccc; }
|
|
72
72
|
.pagination .page-info { font-size: 13px; color: #888; }
|
|
73
|
+
.nav-links a { margin-left: 16px; }
|
|
74
|
+
.btn {
|
|
75
|
+
display: inline-block; padding: 7px 14px; border-radius: 4px;
|
|
76
|
+
background: #4a6fa5; color: #fff !important; border: 0;
|
|
77
|
+
font-size: 13px; cursor: pointer; text-decoration: none;
|
|
78
|
+
}
|
|
79
|
+
.btn:hover { background: #3a5a85; text-decoration: none; }
|
|
80
|
+
.btn-secondary { background: #6c757d; }
|
|
81
|
+
.btn-secondary:hover { background: #5a6268; }
|
|
82
|
+
.btn-danger { background: #dc3545; }
|
|
83
|
+
.btn-danger:hover { background: #c82333; }
|
|
84
|
+
.btn-bar { display: flex; gap: 8px; margin: 16px 0; flex-wrap: wrap; }
|
|
85
|
+
.source-db { background: #cce5ff; color: #004085; }
|
|
86
|
+
.source-file { background: #e2e3e5; color: #383d41; }
|
|
87
|
+
.source-builtin { background: #e0d4ff; color: #4a2d8c; }
|
|
88
|
+
.status-approved { background: #d4edda; color: #155724; }
|
|
89
|
+
.status-proposed { background: #fff3cd; color: #856404; }
|
|
90
|
+
.form-row { margin-bottom: 14px; }
|
|
91
|
+
.form-row label {
|
|
92
|
+
display: block; font-size: 12px; font-weight: 600;
|
|
93
|
+
color: #555; text-transform: uppercase; margin-bottom: 4px;
|
|
94
|
+
}
|
|
95
|
+
.form-row input[type="text"], .form-row textarea {
|
|
96
|
+
width: 100%; padding: 8px 10px; font-size: 14px;
|
|
97
|
+
border: 1px solid #ccc; border-radius: 4px;
|
|
98
|
+
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
|
|
99
|
+
}
|
|
100
|
+
.form-row textarea {
|
|
101
|
+
font-family: "SF Mono", "Monaco", "Menlo", "Consolas", monospace;
|
|
102
|
+
font-size: 13px;
|
|
103
|
+
}
|
|
104
|
+
.form-row .hint { font-size: 12px; color: #888; margin-top: 4px; }
|
|
105
|
+
.form-card {
|
|
106
|
+
background: #fff; border-radius: 8px; padding: 24px;
|
|
107
|
+
box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 24px;
|
|
108
|
+
}
|
|
109
|
+
.flash { padding: 12px 16px; border-radius: 4px; margin-bottom: 16px; font-size: 14px; }
|
|
110
|
+
.flash-notice { background: #d4edda; color: #155724; }
|
|
111
|
+
.flash-alert { background: #f8d7da; color: #721c24; }
|
|
112
|
+
.markdown-body {
|
|
113
|
+
background: #fff; border-radius: 8px; padding: 20px;
|
|
114
|
+
box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 16px;
|
|
115
|
+
white-space: pre-wrap; font-family: "SF Mono", monospace; font-size: 13px;
|
|
116
|
+
}
|
|
117
|
+
.versions-list { background: #fff; border-radius: 8px; padding: 0; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
|
|
118
|
+
.versions-list .ver-row {
|
|
119
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
120
|
+
padding: 10px 16px; border-bottom: 1px solid #f0f0f0; font-size: 13px;
|
|
121
|
+
}
|
|
122
|
+
.versions-list .ver-row:last-child { border-bottom: 0; }
|
|
123
|
+
.versions-list .ver-meta { color: #888; font-size: 12px; }
|
|
124
|
+
.diff-table {
|
|
125
|
+
width: 100%; border-collapse: collapse; background: #fff;
|
|
126
|
+
border-radius: 8px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
|
127
|
+
}
|
|
128
|
+
.diff-table th { background: #f8f9fa; padding: 8px 12px; text-align: left; font-size: 12px; }
|
|
129
|
+
.diff-table td {
|
|
130
|
+
vertical-align: top; padding: 0; width: 50%; border-bottom: 1px solid #f0f0f0;
|
|
131
|
+
}
|
|
132
|
+
.diff-table pre {
|
|
133
|
+
margin: 0; padding: 4px 12px; font-family: "SF Mono", monospace;
|
|
134
|
+
font-size: 12px; white-space: pre-wrap; word-break: break-word;
|
|
135
|
+
}
|
|
136
|
+
.diff-add { background: #d4edda; }
|
|
137
|
+
.diff-del { background: #f8d7da; }
|
|
138
|
+
.tag {
|
|
139
|
+
display: inline-block; padding: 2px 8px; margin-right: 4px;
|
|
140
|
+
background: #e9ecef; color: #495057; border-radius: 10px; font-size: 11px;
|
|
141
|
+
}
|
|
142
|
+
.inline-form { display: inline; }
|
|
73
143
|
</style>
|
|
74
144
|
</head>
|
|
75
145
|
<body>
|
|
76
146
|
<div class="header">
|
|
77
147
|
<h1>RailsConsoleAi Admin</h1>
|
|
78
|
-
<
|
|
148
|
+
<div class="nav-links">
|
|
149
|
+
<a href="<%= rails_console_ai.root_path %>">Sessions</a>
|
|
150
|
+
<a href="<%= rails_console_ai.skills_path %>">Skills</a>
|
|
151
|
+
<a href="<%= rails_console_ai.memories_path %>">Memories</a>
|
|
152
|
+
<a href="<%= rails_console_ai.agents_path %>">Agents</a>
|
|
153
|
+
</div>
|
|
79
154
|
</div>
|
|
80
155
|
<div class="container">
|
|
156
|
+
<% if flash[:notice] %><div class="flash flash-notice"><%= flash[:notice] %></div><% end %>
|
|
157
|
+
<% if flash[:alert] %><div class="flash flash-alert"><%= flash[:alert] %></div><% end %>
|
|
81
158
|
<%= yield %>
|
|
82
159
|
</div>
|
|
83
160
|
</body>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<%= link_to '← Back to agent', agent_path(@agent), class: 'back-link' %>
|
|
2
|
+
|
|
3
|
+
<h2 style="margin-bottom:16px;">Versions — <%= @agent.name %></h2>
|
|
4
|
+
|
|
5
|
+
<% if @versions.empty? %>
|
|
6
|
+
<div class="meta-card"><p class="text-muted">No versions recorded yet.</p></div>
|
|
7
|
+
<% else %>
|
|
8
|
+
<div class="versions-list">
|
|
9
|
+
<% @versions.each do |v| %>
|
|
10
|
+
<div class="ver-row">
|
|
11
|
+
<div>
|
|
12
|
+
<strong>v<%= v.id %></strong>
|
|
13
|
+
<span class="ver-meta">— <%= v.created_at.strftime('%Y-%m-%d %H:%M:%S') %> by <%= v.edited_by || 'unknown' %></span>
|
|
14
|
+
<% if v.status == 'approved' %><span class="badge status-approved">APPROVED</span>
|
|
15
|
+
<% elsif v.status == 'proposed' %><span class="badge status-proposed">PROPOSED</span><% end %>
|
|
16
|
+
<% if v.change_note.present? %><div class="ver-meta"><%= v.change_note %></div><% end %>
|
|
17
|
+
</div>
|
|
18
|
+
<div>
|
|
19
|
+
<%= link_to 'View', agent_version_path(@agent, v) %>
|
|
20
|
+
·
|
|
21
|
+
<%= link_to 'Diff vs current', diff_agents_path(agent_id: @agent.id, from: v.id) %>
|
|
22
|
+
·
|
|
23
|
+
<%= button_to 'Restore', restore_agent_version_path(@agent, v), method: :post, class: 'btn btn-secondary', form: { style: 'display:inline' }, data: { confirm: "Restore version ##{v.id}? Restoring reverts the agent to PROPOSED and a new version row will be created." } %>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
<% end %>
|
|
27
|
+
</div>
|
|
28
|
+
<% end %>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<%= link_to '← All versions', agent_versions_path(@agent), class: 'back-link' %>
|
|
2
|
+
|
|
3
|
+
<div class="meta-card">
|
|
4
|
+
<h2 style="margin-bottom:8px;"><%= @version.name %> <span class="badge source-db">v<%= @version.id %></span></h2>
|
|
5
|
+
<p class="text-muted"><%= @version.description %></p>
|
|
6
|
+
<div class="meta-grid" style="margin-top:16px;">
|
|
7
|
+
<div class="meta-item"><label>Edited by</label><span><%= @version.edited_by || '—' %></span></div>
|
|
8
|
+
<div class="meta-item"><label>At</label><span><%= @version.created_at.strftime('%Y-%m-%d %H:%M:%S') %></span></div>
|
|
9
|
+
<div class="meta-item"><label>Status</label><span><%= @version.status || '—' %></span></div>
|
|
10
|
+
<div class="meta-item"><label>Model</label><span><%= @version.model || '(default)' %></span></div>
|
|
11
|
+
<div class="meta-item"><label>Max rounds</label><span><%= @version.max_rounds || '(default)' %></span></div>
|
|
12
|
+
<div class="meta-item"><label>Tools</label><span><%= Array(@version.tools).join(', ').presence || '(default set)' %></span></div>
|
|
13
|
+
</div>
|
|
14
|
+
<% if @version.change_note.present? %>
|
|
15
|
+
<p style="margin-top:12px;"><strong>Change note:</strong> <%= @version.change_note %></p>
|
|
16
|
+
<% end %>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<div class="btn-bar">
|
|
20
|
+
<%= button_to "Restore this version", restore_agent_version_path(@agent, @version), method: :post, class: 'btn btn-danger', data: { confirm: "Restore version ##{@version.id}? Current agent will be overwritten and status reverted to PROPOSED." } %>
|
|
21
|
+
<%= link_to 'Diff vs current', diff_agents_path(agent_id: @agent.id, from: @version.id), class: 'btn btn-secondary' %>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<h3 style="margin-top:24px; margin-bottom:8px;">Body / instructions</h3>
|
|
25
|
+
<div class="markdown-body"><%= @version.body %></div>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<% if @agent.persisted? && @agent.respond_to?(:approved?) && @agent.approved? %>
|
|
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>.
|
|
4
|
+
</div>
|
|
5
|
+
<% elsif !@agent.persisted? %>
|
|
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 before delegate_task can invoke it.
|
|
8
|
+
</div>
|
|
9
|
+
<% end %>
|
|
10
|
+
|
|
11
|
+
<%= form_with model: @agent, url: form_url, method: form_method, local: true do |f| %>
|
|
12
|
+
<div class="form-card">
|
|
13
|
+
<div class="form-row">
|
|
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>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<hr style="margin: 20px 0; border: 0; border-top: 1px solid #eee;">
|
|
24
|
+
|
|
25
|
+
<div class="form-row">
|
|
26
|
+
<label>Edited by</label>
|
|
27
|
+
<input type="text" name="edited_by" value="<%= params[:edited_by] %>" placeholder="Your name or handle">
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<div class="form-row">
|
|
31
|
+
<label>Change note (optional)</label>
|
|
32
|
+
<input type="text" name="change_note" value="" placeholder="What did you change and why?">
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<div class="btn-bar">
|
|
37
|
+
<%= f.submit submit_label, class: 'btn' %>
|
|
38
|
+
<%= link_to 'Cancel', cancel_path, class: 'btn btn-secondary' %>
|
|
39
|
+
</div>
|
|
40
|
+
<% end %>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<%= link_to '← Back to agent', agent_path(@agent), class: 'back-link' %>
|
|
2
|
+
|
|
3
|
+
<h2 style="margin-bottom:8px;">Diff — <%= @agent.name %></h2>
|
|
4
|
+
<p class="text-muted" style="margin-bottom:16px;">
|
|
5
|
+
Version #<%= @from.id %> (<%= @from.created_at.strftime('%Y-%m-%d %H:%M') %>) → <%= @to_label %>
|
|
6
|
+
</p>
|
|
7
|
+
|
|
8
|
+
<%= render_text_diff(@from.content, @to_content, left_label: "v##{@from.id}", right_label: @to_label) %>
|
|
9
|
+
|
|
10
|
+
<div class="btn-bar" style="margin-top:24px;">
|
|
11
|
+
<% if @to.nil? %>
|
|
12
|
+
<%= button_to "Restore v##{@from.id}", restore_agent_version_path(@agent, @from), method: :post, class: 'btn btn-danger', data: { confirm: 'Restoring will overwrite the current agent (a new version row is created) and revert status to PROPOSED.' } %>
|
|
13
|
+
<% end %>
|
|
14
|
+
<%= link_to 'Back', agent_path(@agent), class: 'btn btn-secondary' %>
|
|
15
|
+
</div>
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
<div class="btn-bar">
|
|
2
|
+
<%= link_to 'New agent', new_agent_path, class: 'btn' %>
|
|
3
|
+
<% if @sort == 'used' %>
|
|
4
|
+
<%= link_to 'Sort: alphabetical', agents_path(q: @q), class: 'btn btn-secondary' %>
|
|
5
|
+
<% else %>
|
|
6
|
+
<%= link_to 'Sort: most used', agents_path(q: @q, sort: 'used'), class: 'btn btn-secondary' %>
|
|
7
|
+
<% end %>
|
|
8
|
+
<form method="get" action="<%= agents_path %>" style="margin-left:auto;">
|
|
9
|
+
<input type="text" name="q" value="<%= @q %>" placeholder="Search agents…" style="padding:6px 10px; border:1px solid #ccc; border-radius:4px;">
|
|
10
|
+
<% if @sort.present? %><input type="hidden" name="sort" value="<%= @sort %>"><% end %>
|
|
11
|
+
</form>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
<% proposed_count = @agents.count { |a| a['source'] == :db && a['status'] != 'approved' } %>
|
|
15
|
+
<% if proposed_count > 0 %>
|
|
16
|
+
<div class="flash flash-alert" style="margin-bottom:16px;">
|
|
17
|
+
<strong><%= proposed_count %></strong> agent<%= proposed_count == 1 ? '' : 's' %> awaiting approval. The AI cannot invoke them via delegate_task until a human approves.
|
|
18
|
+
</div>
|
|
19
|
+
<% end %>
|
|
20
|
+
|
|
21
|
+
<% if @agents.empty? %>
|
|
22
|
+
<div class="meta-card">
|
|
23
|
+
<p class="text-muted">No agents found. <%= link_to 'Create one', new_agent_path %> or drop a Markdown file in <code>.rails_console_ai/agents/</code>.</p>
|
|
24
|
+
</div>
|
|
25
|
+
<% else %>
|
|
26
|
+
<table>
|
|
27
|
+
<thead>
|
|
28
|
+
<tr>
|
|
29
|
+
<th>Name</th>
|
|
30
|
+
<th>Description</th>
|
|
31
|
+
<th>Model</th>
|
|
32
|
+
<th>Source / Status</th>
|
|
33
|
+
<th title="Number of times delegate_task invoked this agent (DB only)">Uses</th>
|
|
34
|
+
<th>Last used</th>
|
|
35
|
+
<th></th>
|
|
36
|
+
</tr>
|
|
37
|
+
</thead>
|
|
38
|
+
<tbody>
|
|
39
|
+
<% @agents.each do |a| %>
|
|
40
|
+
<% link_id = a['id'] || a['name'] %>
|
|
41
|
+
<tr>
|
|
42
|
+
<td><strong><%= link_to a['name'], agent_path(link_id) %></strong></td>
|
|
43
|
+
<td class="query-cell"><%= a['description'] %></td>
|
|
44
|
+
<td class="mono"><%= a['model'] || '—' %></td>
|
|
45
|
+
<td>
|
|
46
|
+
<% case a['source'] %>
|
|
47
|
+
<% when :db %>
|
|
48
|
+
<span class="badge source-db">DB</span>
|
|
49
|
+
<% if a['status'] == 'approved' %>
|
|
50
|
+
<span class="badge status-approved">APPROVED</span>
|
|
51
|
+
<% else %>
|
|
52
|
+
<span class="badge status-proposed">PROPOSED</span>
|
|
53
|
+
<% end %>
|
|
54
|
+
<% when :file %>
|
|
55
|
+
<span class="badge source-file">FILE</span>
|
|
56
|
+
<% when :builtin %>
|
|
57
|
+
<span class="badge source-builtin">BUILTIN</span>
|
|
58
|
+
<% end %>
|
|
59
|
+
</td>
|
|
60
|
+
<td>
|
|
61
|
+
<% if a['source'] == :db %>
|
|
62
|
+
<%= a['use_count'] || 0 %>
|
|
63
|
+
<% else %>
|
|
64
|
+
<span class="text-muted">—</span>
|
|
65
|
+
<% end %>
|
|
66
|
+
</td>
|
|
67
|
+
<td class="text-muted" style="font-size:12px;">
|
|
68
|
+
<%= a['last_used_at']&.strftime('%Y-%m-%d %H:%M') || '—' %>
|
|
69
|
+
</td>
|
|
70
|
+
<td>
|
|
71
|
+
<%= link_to 'View', agent_path(link_id) %>
|
|
72
|
+
<% if a['source'] == :db %>
|
|
73
|
+
· <%= link_to 'Edit', edit_agent_path(a['id']) %>
|
|
74
|
+
<% end %>
|
|
75
|
+
</td>
|
|
76
|
+
</tr>
|
|
77
|
+
<% end %>
|
|
78
|
+
</tbody>
|
|
79
|
+
</table>
|
|
80
|
+
<% end %>
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
<%
|
|
2
|
+
is_db = @agent.is_a?(RailsConsoleAi::Agent)
|
|
3
|
+
is_builtin = !is_db && @agent['source'] == :builtin
|
|
4
|
+
is_file = !is_db && @agent['source'] == :file
|
|
5
|
+
|
|
6
|
+
name = is_db ? @agent.name : @agent['name']
|
|
7
|
+
description = is_db ? @agent.description : @agent['description']
|
|
8
|
+
body = is_db ? @agent.body : @agent['body']
|
|
9
|
+
max_rounds = is_db ? @agent.max_rounds : @agent['max_rounds']
|
|
10
|
+
model = is_db ? @agent.model : @agent['model']
|
|
11
|
+
tools = is_db ? Array(@agent.tools) : Array(@agent['tools'])
|
|
12
|
+
file_key = is_db ? nil : @agent['file_key']
|
|
13
|
+
%>
|
|
14
|
+
|
|
15
|
+
<%= link_to '← All agents', agents_path, class: 'back-link' %>
|
|
16
|
+
|
|
17
|
+
<% if is_db && !@agent.approved? %>
|
|
18
|
+
<div class="flash flash-alert" style="margin-bottom:16px;">
|
|
19
|
+
<strong>Awaiting approval.</strong> The AI cannot invoke this agent via delegate_task until a human approves it.
|
|
20
|
+
Review the body and tools list below, then click <strong>Approve</strong>.
|
|
21
|
+
</div>
|
|
22
|
+
<% end %>
|
|
23
|
+
|
|
24
|
+
<% if is_builtin %>
|
|
25
|
+
<div class="flash flash-notice" style="margin-bottom:16px;">
|
|
26
|
+
This is a <strong>built-in agent</strong> shipped with the gem. It's read-only here.
|
|
27
|
+
To customize it, <%= link_to 'create a same-named DB override', new_agent_path(from_builtin: name) %> — your DB agent will shadow this one.
|
|
28
|
+
</div>
|
|
29
|
+
<% end %>
|
|
30
|
+
|
|
31
|
+
<div class="meta-card">
|
|
32
|
+
<h2 style="margin-bottom:8px;">
|
|
33
|
+
<%= name %>
|
|
34
|
+
<% if is_db %>
|
|
35
|
+
<span class="badge source-db">DB</span>
|
|
36
|
+
<% if @agent.approved? %>
|
|
37
|
+
<span class="badge status-approved">APPROVED</span>
|
|
38
|
+
<% else %>
|
|
39
|
+
<span class="badge status-proposed">PROPOSED</span>
|
|
40
|
+
<% end %>
|
|
41
|
+
<% elsif is_builtin %>
|
|
42
|
+
<span class="badge source-builtin">BUILTIN</span>
|
|
43
|
+
<% else %>
|
|
44
|
+
<span class="badge source-file">FILE</span>
|
|
45
|
+
<% end %>
|
|
46
|
+
</h2>
|
|
47
|
+
<p class="text-muted"><%= description %></p>
|
|
48
|
+
<div class="meta-grid" style="margin-top:16px;">
|
|
49
|
+
<div class="meta-item"><label>Model</label><span><%= model || '(default)' %></span></div>
|
|
50
|
+
<div class="meta-item"><label>Max rounds</label><span><%= max_rounds || '(default)' %></span></div>
|
|
51
|
+
<div class="meta-item"><label>Tool whitelist</label><span><%= tools.empty? ? '(default set)' : tools.join(', ') %></span></div>
|
|
52
|
+
<% if is_db %>
|
|
53
|
+
<div class="meta-item"><label>Updated</label><span><%= @agent.updated_at.strftime('%Y-%m-%d %H:%M') %></span></div>
|
|
54
|
+
<div class="meta-item"><label>Versions</label><span><%= @agent.versions.count %></span></div>
|
|
55
|
+
<div class="meta-item"><label>Times invoked</label><span><%= @agent.use_count %></span></div>
|
|
56
|
+
<div class="meta-item"><label>Last invoked</label><span><%= @agent.last_used_at&.strftime('%Y-%m-%d %H:%M') || 'never' %></span></div>
|
|
57
|
+
<% if @agent.approved? %>
|
|
58
|
+
<div class="meta-item"><label>Approved by</label><span><%= @agent.approved_by || '—' %></span></div>
|
|
59
|
+
<div class="meta-item"><label>Approved at</label><span><%= @agent.approved_at&.strftime('%Y-%m-%d %H:%M') || '—' %></span></div>
|
|
60
|
+
<% end %>
|
|
61
|
+
<% else %>
|
|
62
|
+
<div class="meta-item"><label>Path</label><span class="mono"><%= file_key %></span></div>
|
|
63
|
+
<% end %>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<div class="btn-bar">
|
|
68
|
+
<% if is_db %>
|
|
69
|
+
<% unless @agent.approved? %>
|
|
70
|
+
<%= form_with url: approve_agent_path(@agent), method: :post, local: true, style: 'display:inline' do %>
|
|
71
|
+
<input type="text" name="approved_by" placeholder="Your name (required)" style="padding:7px 10px; border:1px solid #ccc; border-radius:4px; margin-right:6px;">
|
|
72
|
+
<button type="submit" class="btn btn-danger" data-confirm="Approve this agent? The AI will then be able to invoke it via delegate_task with the configured tool whitelist.">Approve</button>
|
|
73
|
+
<% end %>
|
|
74
|
+
<% end %>
|
|
75
|
+
<%= link_to 'Edit', edit_agent_path(@agent), class: 'btn' %>
|
|
76
|
+
<%= link_to 'Versions', agent_versions_path(@agent), class: 'btn btn-secondary' %>
|
|
77
|
+
<%= button_to 'Delete', agent_path(@agent), method: :delete, class: 'btn btn-danger', form: { style: 'display:inline' }, data: { confirm: 'Delete this agent? Past versions remain in history.' } %>
|
|
78
|
+
<% elsif is_builtin %>
|
|
79
|
+
<%= link_to 'Create DB override', new_agent_path(from_builtin: name), class: 'btn' %>
|
|
80
|
+
<% end %>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<h3 style="margin-bottom:8px;">Body / instructions</h3>
|
|
84
|
+
<div class="markdown-body"><%= body %></div>
|
|
85
|
+
|
|
86
|
+
<% if is_db && @versions && @versions.any? %>
|
|
87
|
+
<h3 style="margin:24px 0 8px;">Recent versions</h3>
|
|
88
|
+
<div class="versions-list">
|
|
89
|
+
<% @versions.first(5).each do |v| %>
|
|
90
|
+
<div class="ver-row">
|
|
91
|
+
<div>
|
|
92
|
+
<strong>v<%= v.id %></strong>
|
|
93
|
+
<span class="ver-meta">— <%= v.created_at.strftime('%Y-%m-%d %H:%M') %> by <%= v.edited_by || 'unknown' %></span>
|
|
94
|
+
<% if v.status == 'approved' %><span class="badge status-approved">APPROVED</span><% end %>
|
|
95
|
+
<% if v.change_note.present? %><div class="ver-meta"><%= v.change_note %></div><% end %>
|
|
96
|
+
</div>
|
|
97
|
+
<div>
|
|
98
|
+
<%= link_to 'View', agent_version_path(@agent, v) %>
|
|
99
|
+
·
|
|
100
|
+
<%= link_to 'Diff vs current', diff_agents_path(agent_id: @agent.id, from: v.id) %>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
<% end %>
|
|
104
|
+
</div>
|
|
105
|
+
<% if @versions.size > 5 %>
|
|
106
|
+
<p style="margin-top:8px;"><%= link_to "See all #{@versions.size} versions →", agent_versions_path(@agent) %></p>
|
|
107
|
+
<% end %>
|
|
108
|
+
<% end %>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<%= form_with model: @memory, url: form_url, method: form_method, local: true do |f| %>
|
|
2
|
+
<div class="form-card">
|
|
3
|
+
<div class="form-row">
|
|
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>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<hr style="margin: 20px 0; border: 0; border-top: 1px solid #eee;">
|
|
13
|
+
|
|
14
|
+
<div class="form-row">
|
|
15
|
+
<label>Edited by</label>
|
|
16
|
+
<input type="text" name="edited_by" value="<%= params[:edited_by] %>" placeholder="Your name or handle">
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<div class="form-row">
|
|
20
|
+
<label>Change note (optional)</label>
|
|
21
|
+
<input type="text" name="change_note" value="" placeholder="What did you change and why?">
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<div class="btn-bar">
|
|
26
|
+
<%= f.submit submit_label, class: 'btn' %>
|
|
27
|
+
<%= link_to 'Cancel', cancel_path, class: 'btn btn-secondary' %>
|
|
28
|
+
</div>
|
|
29
|
+
<% end %>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<%= link_to '← Back to memory', memory_path(@memory), class: 'back-link' %>
|
|
2
|
+
|
|
3
|
+
<h2 style="margin-bottom:8px;">Diff — <%= @memory.name %></h2>
|
|
4
|
+
<p class="text-muted" style="margin-bottom:16px;">
|
|
5
|
+
Version #<%= @from.id %> (<%= @from.created_at.strftime('%Y-%m-%d %H:%M') %>) → <%= @to_label %>
|
|
6
|
+
</p>
|
|
7
|
+
|
|
8
|
+
<%= render_text_diff(@from.content, @to_content, left_label: "v##{@from.id}", right_label: @to_label) %>
|
|
9
|
+
|
|
10
|
+
<div class="btn-bar" style="margin-top:24px;">
|
|
11
|
+
<% if @to.nil? %>
|
|
12
|
+
<%= button_to "Restore v##{@from.id}", restore_memory_version_path(@memory, @from), method: :post, class: 'btn btn-danger', data: { confirm: 'Restoring will overwrite the current memory (a new version row is created).' } %>
|
|
13
|
+
<% end %>
|
|
14
|
+
<%= link_to 'Back', memory_path(@memory), class: 'btn btn-secondary' %>
|
|
15
|
+
</div>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<%= link_to '← Back to memory', memory_path(@memory), class: 'back-link' %>
|
|
2
|
+
<h2 style="margin-bottom:16px;">Edit memory</h2>
|
|
3
|
+
<%= render 'form',
|
|
4
|
+
form_url: memory_path(@memory),
|
|
5
|
+
form_method: :patch,
|
|
6
|
+
submit_label: 'Save changes',
|
|
7
|
+
cancel_path: memory_path(@memory) %>
|