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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '083adc4ffd8b69f950fbc4ab18f2ac056e8642c6946c8ba46e67e19e4299e0f2'
|
|
4
|
+
data.tar.gz: a01fef0fe45dfa157d638a7d4edb1ee5c8d9753be3a79956b3ebde220d3ee939
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 56b8fdcd0cf445d9480d4e56e252271241af017c8ca51e2a11af36d2158ce4a3a1fdf9670685f931b8cbd0994e6828e6bac08d10247e8bec1af8c66ad83c47f4
|
|
7
|
+
data.tar.gz: 6ccb72b8b8e9b157b28722d1e532d2abf5df8037f4ba4f1f3635bb37ad8a3441e48d832d903f6f92b6dce80106082215368dde15626eb0fc41d93da60100f261
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.31.0]
|
|
6
|
+
|
|
7
|
+
- Simplify DB-backed skills, sub-agents, and memories to a single content field per record, replacing the separate frontmatter/body columns and streamlining the models, controllers, and forms
|
|
8
|
+
- Add options for running agents
|
|
9
|
+
- Fix the `/script/release` script
|
|
10
|
+
|
|
5
11
|
## [0.30.0]
|
|
6
12
|
|
|
7
13
|
- Add background agent support — new `run_agent` mechanism to launch agents asynchronously alongside an HTTP API channel
|
data/README.md
CHANGED
|
@@ -521,6 +521,23 @@ RailsConsoleAi.get_agent_response(id)
|
|
|
521
521
|
|
|
522
522
|
`run_agent` enqueues a row in the sessions table with `mode='agent_api'` and `status='queued'`. A separate long-running rake task picks them up and runs each in its own thread using the same engine that powers `ai "..."` in the console.
|
|
523
523
|
|
|
524
|
+
### Per-run options
|
|
525
|
+
|
|
526
|
+
`run_agent` accepts two extra keyword arguments to tune individual runs:
|
|
527
|
+
|
|
528
|
+
```ruby
|
|
529
|
+
RailsConsoleAi.run_agent(
|
|
530
|
+
"Trace why nightly billing is double-charging some accounts",
|
|
531
|
+
use_thinking_model: true, # run on the thinking-tier model (e.g. Opus)
|
|
532
|
+
max_wall_clock_seconds: 1800 # hard kill after 30 minutes; pass nil for no cap
|
|
533
|
+
)
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
- `use_thinking_model:` (default `false`) — switches the run to `config.thinking_model` (or the provider default thinking model) for the duration of the agent. Useful for harder, multi-step problems.
|
|
537
|
+
- `max_wall_clock_seconds:` (default `600`) — hard ceiling on wall-clock time. If the run exceeds the cap, the worker thread is killed and the session is marked `status='failed'` with `error_message: "exceeded max_wall_clock_seconds (Ns)"`. Pass `nil` to opt out of any cap.
|
|
538
|
+
|
|
539
|
+
These (along with any future per-run options) are stored in a JSON `options` column on the session row, so they survive the handoff to the background runner.
|
|
540
|
+
|
|
524
541
|
### Running the background runner
|
|
525
542
|
|
|
526
543
|
```bash
|
|
@@ -535,7 +552,7 @@ Run it separately from your web server, alongside (or instead of) the Slack bot.
|
|
|
535
552
|
|
|
536
553
|
### Requirements
|
|
537
554
|
|
|
538
|
-
- `RailsConsoleAi.setup!` must have been run so the sessions table has the `status`, `result`, and `
|
|
555
|
+
- `RailsConsoleAi.setup!` must have been run so the sessions table has the `status`, `result`, `error_message`, and `options` columns. `ai_db_setup` (or `ai_db_migrate` on existing installs) handles this.
|
|
539
556
|
- `session_logging` must be enabled (it is by default).
|
|
540
557
|
|
|
541
558
|
### Behavior notes
|
|
@@ -13,14 +13,7 @@ module RailsConsoleAi
|
|
|
13
13
|
def restore
|
|
14
14
|
version = @agent.versions.find(params[:id])
|
|
15
15
|
@agent.update_with_version!(
|
|
16
|
-
{
|
|
17
|
-
name: version.name,
|
|
18
|
-
description: version.description,
|
|
19
|
-
body: version.body,
|
|
20
|
-
max_rounds: version.max_rounds,
|
|
21
|
-
model: version.model,
|
|
22
|
-
tools: Array(version.tools)
|
|
23
|
-
},
|
|
16
|
+
{ content: version.content },
|
|
24
17
|
edited_by: params[:edited_by].presence || 'web',
|
|
25
18
|
change_note: "Restored from version ##{version.id}"
|
|
26
19
|
)
|
|
@@ -32,14 +32,17 @@ module RailsConsoleAi
|
|
|
32
32
|
a['source'] == :builtin && a['name'].to_s.downcase == params[:from_builtin].to_s.downcase
|
|
33
33
|
}
|
|
34
34
|
if builtin
|
|
35
|
-
@agent.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
35
|
+
@agent.content = AgentLoader.dump(
|
|
36
|
+
name: builtin['name'],
|
|
37
|
+
description: builtin['description'],
|
|
38
|
+
body: builtin['body'],
|
|
39
|
+
max_rounds: builtin['max_rounds'],
|
|
40
|
+
model: builtin['model'],
|
|
41
|
+
tools: Array(builtin['tools'])
|
|
42
|
+
)
|
|
41
43
|
end
|
|
42
44
|
end
|
|
45
|
+
@agent.content ||= new_agent_template
|
|
43
46
|
end
|
|
44
47
|
|
|
45
48
|
def create
|
|
@@ -57,34 +60,6 @@ module RailsConsoleAi
|
|
|
57
60
|
end
|
|
58
61
|
end
|
|
59
62
|
|
|
60
|
-
# POST /agents/import — parse a pasted .md blob and re-render `new` with fields prefilled.
|
|
61
|
-
def import
|
|
62
|
-
content = params[:content].to_s
|
|
63
|
-
if content.strip.empty?
|
|
64
|
-
redirect_to new_agent_path, alert: 'Nothing to parse — paste the .md content into the box first.'
|
|
65
|
-
return
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
parsed = AgentLoader.parse(content)
|
|
69
|
-
if parsed.nil? || parsed['name'].to_s.strip.empty?
|
|
70
|
-
redirect_to new_agent_path,
|
|
71
|
-
alert: 'Could not parse. Expected YAML frontmatter (between `---` lines) with at least a `name` field, followed by the agent body.'
|
|
72
|
-
return
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
@agent = Agent.new(
|
|
76
|
-
name: parsed['name'],
|
|
77
|
-
description: parsed['description'],
|
|
78
|
-
body: parsed['body'],
|
|
79
|
-
max_rounds: parsed['max_rounds'],
|
|
80
|
-
model: parsed['model']
|
|
81
|
-
)
|
|
82
|
-
@agent.tools = Array(parsed['tools'])
|
|
83
|
-
|
|
84
|
-
flash.now[:notice] = "Parsed \"#{parsed['name']}\" from pasted content. Review the fields below and click Create agent to save to the DB."
|
|
85
|
-
render :new
|
|
86
|
-
end
|
|
87
|
-
|
|
88
63
|
def edit
|
|
89
64
|
redirect_to agents_path, alert: read_only_message and return unless @agent.is_a?(RailsConsoleAi::Agent)
|
|
90
65
|
end
|
|
@@ -136,9 +111,8 @@ module RailsConsoleAi
|
|
|
136
111
|
@agent = Agent.find(params[:agent_id])
|
|
137
112
|
@from = @agent.versions.find(params[:from])
|
|
138
113
|
@to = params[:to].present? ? @agent.versions.find(params[:to]) : nil
|
|
139
|
-
@to_label
|
|
140
|
-
@
|
|
141
|
-
@to_tools = @to ? Array(@to.tools) : Array(@agent.tools)
|
|
114
|
+
@to_label = @to ? "Version ##{@to.id}" : 'Current'
|
|
115
|
+
@to_content = @to ? @to.content : @agent.content
|
|
142
116
|
end
|
|
143
117
|
|
|
144
118
|
private
|
|
@@ -163,22 +137,25 @@ module RailsConsoleAi
|
|
|
163
137
|
end
|
|
164
138
|
|
|
165
139
|
def agent_params
|
|
166
|
-
{
|
|
167
|
-
name: params.require(:agent)[:name],
|
|
168
|
-
description: params[:agent][:description],
|
|
169
|
-
body: params[:agent][:body],
|
|
170
|
-
max_rounds: params[:agent][:max_rounds].presence&.to_i,
|
|
171
|
-
model: params[:agent][:model].presence,
|
|
172
|
-
tools: split_lines(params[:agent][:tools])
|
|
173
|
-
}
|
|
140
|
+
{ content: params.require(:agent)[:content].to_s }
|
|
174
141
|
end
|
|
175
142
|
|
|
176
143
|
def edited_by_param
|
|
177
144
|
params[:edited_by].presence || 'web'
|
|
178
145
|
end
|
|
179
146
|
|
|
180
|
-
def
|
|
181
|
-
|
|
147
|
+
def new_agent_template
|
|
148
|
+
<<~MD
|
|
149
|
+
---
|
|
150
|
+
name:
|
|
151
|
+
description:
|
|
152
|
+
max_rounds:
|
|
153
|
+
model:
|
|
154
|
+
tools: []
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
Persona, strategy, rules…
|
|
158
|
+
MD
|
|
182
159
|
end
|
|
183
160
|
|
|
184
161
|
def slugify(name)
|
|
@@ -25,32 +25,7 @@ module RailsConsoleAi
|
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
def new
|
|
28
|
-
@memory = Memory.new
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
# POST /memories/import — parse a pasted .md blob and re-render `new` with fields prefilled.
|
|
32
|
-
def import
|
|
33
|
-
content = params[:content].to_s
|
|
34
|
-
if content.strip.empty?
|
|
35
|
-
redirect_to new_memory_path, alert: 'Nothing to parse — paste the .md content into the box first.'
|
|
36
|
-
return
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
parsed = Tools::MemoryTools.parse(content)
|
|
40
|
-
if parsed.nil? || parsed['name'].to_s.strip.empty?
|
|
41
|
-
redirect_to new_memory_path,
|
|
42
|
-
alert: 'Could not parse. Expected YAML frontmatter (between `---` lines) with at least a `name` field, followed by the memory body.'
|
|
43
|
-
return
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
@memory = Memory.new(
|
|
47
|
-
name: parsed['name'],
|
|
48
|
-
description: parsed['description']
|
|
49
|
-
)
|
|
50
|
-
@memory.tags = Array(parsed['tags'])
|
|
51
|
-
|
|
52
|
-
flash.now[:notice] = "Parsed \"#{parsed['name']}\" from pasted content. Review the fields below and click Create memory to save to the DB."
|
|
53
|
-
render :new
|
|
28
|
+
@memory = Memory.new(content: new_memory_template)
|
|
54
29
|
end
|
|
55
30
|
|
|
56
31
|
def create
|
|
@@ -102,9 +77,8 @@ module RailsConsoleAi
|
|
|
102
77
|
@memory = Memory.find(params[:memory_id])
|
|
103
78
|
@from = @memory.versions.find(params[:from])
|
|
104
79
|
@to = params[:to].present? ? @memory.versions.find(params[:to]) : nil
|
|
105
|
-
@to_label
|
|
106
|
-
@
|
|
107
|
-
@to_tags = @to ? Array(@to.tags) : Array(@memory.tags)
|
|
80
|
+
@to_label = @to ? "Version ##{@to.id}" : 'Current'
|
|
81
|
+
@to_content = @to ? @to.content : @memory.content
|
|
108
82
|
end
|
|
109
83
|
|
|
110
84
|
private
|
|
@@ -130,19 +104,22 @@ module RailsConsoleAi
|
|
|
130
104
|
end
|
|
131
105
|
|
|
132
106
|
def memory_params
|
|
133
|
-
{
|
|
134
|
-
name: params.require(:memory)[:name],
|
|
135
|
-
description: params[:memory][:description],
|
|
136
|
-
tags: split_csv(params[:memory][:tags])
|
|
137
|
-
}
|
|
107
|
+
{ content: params.require(:memory)[:content].to_s }
|
|
138
108
|
end
|
|
139
109
|
|
|
140
110
|
def edited_by_param
|
|
141
111
|
params[:edited_by].presence || 'web'
|
|
142
112
|
end
|
|
143
113
|
|
|
144
|
-
def
|
|
145
|
-
|
|
114
|
+
def new_memory_template
|
|
115
|
+
<<~MD
|
|
116
|
+
---
|
|
117
|
+
name:
|
|
118
|
+
tags: []
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
The fact or pattern you're persisting.
|
|
122
|
+
MD
|
|
146
123
|
end
|
|
147
124
|
|
|
148
125
|
def slugify(name)
|
|
@@ -13,11 +13,7 @@ module RailsConsoleAi
|
|
|
13
13
|
def restore
|
|
14
14
|
version = @memory.versions.find(params[:id])
|
|
15
15
|
@memory.update_with_version!(
|
|
16
|
-
{
|
|
17
|
-
name: version.name,
|
|
18
|
-
description: version.description,
|
|
19
|
-
tags: Array(version.tags)
|
|
20
|
-
},
|
|
16
|
+
{ content: version.content },
|
|
21
17
|
edited_by: params[:edited_by].presence || 'web',
|
|
22
18
|
change_note: "Restored from version ##{version.id}"
|
|
23
19
|
)
|
|
@@ -13,13 +13,7 @@ module RailsConsoleAi
|
|
|
13
13
|
def restore
|
|
14
14
|
version = @skill.versions.find(params[:id])
|
|
15
15
|
@skill.update_with_version!(
|
|
16
|
-
{
|
|
17
|
-
name: version.name,
|
|
18
|
-
description: version.description,
|
|
19
|
-
body: version.body,
|
|
20
|
-
tags: Array(version.tags),
|
|
21
|
-
bypass_guards_for_methods: Array(version.bypass_guards_for_methods)
|
|
22
|
-
},
|
|
16
|
+
{ content: version.content },
|
|
23
17
|
edited_by: params[:edited_by].presence || 'web',
|
|
24
18
|
change_note: "Restored from version ##{version.id}"
|
|
25
19
|
)
|
|
@@ -26,37 +26,7 @@ module RailsConsoleAi
|
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
def new
|
|
29
|
-
@skill = Skill.new
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
# POST /skills/import — accepts a pasted .md blob in params[:content], parses
|
|
33
|
-
# YAML frontmatter + body, and re-renders `new` with the fields pre-populated.
|
|
34
|
-
# The user reviews + clicks Create skill to actually persist (normal proposed-
|
|
35
|
-
# status + version-row flow applies).
|
|
36
|
-
def import
|
|
37
|
-
content = params[:content].to_s
|
|
38
|
-
if content.strip.empty?
|
|
39
|
-
redirect_to new_skill_path, alert: 'Nothing to parse — paste the .md content into the box first.'
|
|
40
|
-
return
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
parsed = SkillLoader.parse(content)
|
|
44
|
-
if parsed.nil? || parsed['name'].to_s.strip.empty?
|
|
45
|
-
redirect_to new_skill_path,
|
|
46
|
-
alert: 'Could not parse. Expected YAML frontmatter (between `---` lines) with at least a `name` field, followed by a markdown body.'
|
|
47
|
-
return
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
@skill = Skill.new(
|
|
51
|
-
name: parsed['name'],
|
|
52
|
-
description: parsed['description'],
|
|
53
|
-
body: parsed['body']
|
|
54
|
-
)
|
|
55
|
-
@skill.tags = Array(parsed['tags'])
|
|
56
|
-
@skill.bypass_guards_for_methods = Array(parsed['bypass_guards_for_methods'])
|
|
57
|
-
|
|
58
|
-
flash.now[:notice] = "Parsed \"#{parsed['name']}\" from pasted content. Review the fields below and click Create skill to save to the DB."
|
|
59
|
-
render :new
|
|
29
|
+
@skill = Skill.new(content: new_skill_template)
|
|
60
30
|
end
|
|
61
31
|
|
|
62
32
|
def create
|
|
@@ -130,10 +100,8 @@ module RailsConsoleAi
|
|
|
130
100
|
@from = @skill.versions.find(params[:from])
|
|
131
101
|
@to = params[:to].present? ? @skill.versions.find(params[:to]) : nil
|
|
132
102
|
# If `to` is omitted, diff against the current skill.
|
|
133
|
-
@to_label
|
|
134
|
-
@
|
|
135
|
-
@to_tags = @to ? Array(@to.tags) : Array(@skill.tags)
|
|
136
|
-
@to_bypass = @to ? Array(@to.bypass_guards_for_methods) : Array(@skill.bypass_guards_for_methods)
|
|
103
|
+
@to_label = @to ? "Version ##{@to.id}" : 'Current'
|
|
104
|
+
@to_content = @to ? @to.content : @skill.content
|
|
137
105
|
end
|
|
138
106
|
|
|
139
107
|
private
|
|
@@ -165,25 +133,28 @@ module RailsConsoleAi
|
|
|
165
133
|
end
|
|
166
134
|
|
|
167
135
|
def skill_params
|
|
168
|
-
{
|
|
169
|
-
name: params.require(:skill)[:name],
|
|
170
|
-
description: params[:skill][:description],
|
|
171
|
-
body: params[:skill][:body],
|
|
172
|
-
tags: split_csv(params[:skill][:tags]),
|
|
173
|
-
bypass_guards_for_methods: split_lines(params[:skill][:bypass_guards_for_methods])
|
|
174
|
-
}
|
|
136
|
+
{ content: params.require(:skill)[:content].to_s }
|
|
175
137
|
end
|
|
176
138
|
|
|
177
139
|
def edited_by_param
|
|
178
140
|
params[:edited_by].presence || 'web'
|
|
179
141
|
end
|
|
180
142
|
|
|
181
|
-
def
|
|
182
|
-
|
|
183
|
-
|
|
143
|
+
def new_skill_template
|
|
144
|
+
<<~MD
|
|
145
|
+
---
|
|
146
|
+
name:
|
|
147
|
+
description:
|
|
148
|
+
tags: []
|
|
149
|
+
bypass_guards_for_methods: []
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## When to use
|
|
153
|
+
|
|
154
|
+
## Recipe
|
|
184
155
|
|
|
185
|
-
|
|
186
|
-
|
|
156
|
+
## Notes
|
|
157
|
+
MD
|
|
187
158
|
end
|
|
188
159
|
|
|
189
160
|
def slugify(name)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
require '
|
|
1
|
+
require 'rails_console_ai/agent_loader'
|
|
2
2
|
|
|
3
3
|
module RailsConsoleAi
|
|
4
4
|
class Agent < 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 agent 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 max_rounds model tools].freeze
|
|
15
|
-
|
|
16
11
|
has_many :versions,
|
|
17
12
|
-> { order(created_at: :desc) },
|
|
18
13
|
class_name: 'RailsConsoleAi::AgentVersion',
|
|
19
14
|
foreign_key: :agent_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,43 +35,25 @@ module RailsConsoleAi
|
|
|
36
35
|
end
|
|
37
36
|
end
|
|
38
37
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def tools
|
|
42
|
-
decode_json_array(read_attribute(:tools))
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def tools=(value)
|
|
46
|
-
write_attribute(:tools, encode_json_array(value))
|
|
38
|
+
def parsed
|
|
39
|
+
@parsed ||= (RailsConsoleAi::AgentLoader.parse(content.to_s) || {})
|
|
47
40
|
end
|
|
48
41
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
has_attribute_status? ? read_attribute(:status) : STATUS_PROPOSED
|
|
42
|
+
def content=(value)
|
|
43
|
+
@parsed = nil
|
|
44
|
+
super
|
|
53
45
|
end
|
|
54
46
|
|
|
55
|
-
def
|
|
56
|
-
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def
|
|
60
|
-
has_attribute?(:approved_at) ? read_attribute(:approved_at) : nil
|
|
61
|
-
end
|
|
47
|
+
def description; parsed['description']; end
|
|
48
|
+
def body; parsed['body']; end
|
|
49
|
+
def max_rounds; parsed['max_rounds']; end
|
|
50
|
+
def model; parsed['model']; end
|
|
51
|
+
def tools; Array(parsed['tools']); end
|
|
62
52
|
|
|
63
53
|
def proposed?; status.to_s == STATUS_PROPOSED; end
|
|
64
54
|
def approved?; status.to_s == STATUS_APPROVED; end
|
|
65
55
|
|
|
66
|
-
def use_count
|
|
67
|
-
has_attribute?(:use_count) ? (read_attribute(:use_count) || 0) : 0
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
def last_used_at
|
|
71
|
-
has_attribute?(:last_used_at) ? read_attribute(:last_used_at) : nil
|
|
72
|
-
end
|
|
73
|
-
|
|
74
56
|
def self.record_use!(id)
|
|
75
|
-
return false unless connection.column_exists?(table_name, :use_count)
|
|
76
57
|
where(id: id).update_all([
|
|
77
58
|
'use_count = COALESCE(use_count, 0) + 1, last_used_at = ?',
|
|
78
59
|
Time.now.utc
|
|
@@ -92,6 +73,7 @@ module RailsConsoleAi
|
|
|
92
73
|
'max_rounds' => max_rounds,
|
|
93
74
|
'model' => model,
|
|
94
75
|
'tools' => tools,
|
|
76
|
+
'content' => content,
|
|
95
77
|
'status' => status,
|
|
96
78
|
'approved_by' => approved_by,
|
|
97
79
|
'approved_at' => approved_at,
|
|
@@ -102,33 +84,11 @@ module RailsConsoleAi
|
|
|
102
84
|
}
|
|
103
85
|
end
|
|
104
86
|
|
|
105
|
-
def has_attribute_status?
|
|
106
|
-
has_attribute?(:status)
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
def self.decode_json_array(raw)
|
|
110
|
-
return [] if raw.nil? || (raw.respond_to?(:empty?) && raw.empty?)
|
|
111
|
-
return raw if raw.is_a?(Array)
|
|
112
|
-
JSON.parse(raw)
|
|
113
|
-
rescue JSON::ParserError
|
|
114
|
-
[]
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
def self.encode_json_array(value)
|
|
118
|
-
JSON.dump(Array(value))
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
def decode_json_array(raw); self.class.decode_json_array(raw); end
|
|
122
|
-
def encode_json_array(value); self.class.encode_json_array(value); end
|
|
123
|
-
|
|
124
|
-
# Assigns attrs, saves, and records one AgentVersion snapshot of the post-save state.
|
|
125
|
-
# If `preserve_approval` is false (the default), any change to a content attribute
|
|
126
|
-
# reverts the agent back to "proposed" and clears the approver.
|
|
127
87
|
def update_with_version!(attrs, edited_by: nil, change_note: nil, preserve_approval: false)
|
|
128
88
|
transaction do
|
|
129
89
|
assign_attributes(attrs)
|
|
130
90
|
|
|
131
|
-
if !preserve_approval && approved? &&
|
|
91
|
+
if !preserve_approval && approved? && changes.key?('content')
|
|
132
92
|
self.status = STATUS_PROPOSED
|
|
133
93
|
self.approved_by = nil
|
|
134
94
|
self.approved_at = nil
|
|
@@ -138,11 +98,7 @@ module RailsConsoleAi
|
|
|
138
98
|
RailsConsoleAi::AgentVersion.create!(
|
|
139
99
|
agent_id: id,
|
|
140
100
|
name: name,
|
|
141
|
-
|
|
142
|
-
body: body,
|
|
143
|
-
max_rounds: max_rounds,
|
|
144
|
-
model: model,
|
|
145
|
-
tools: tools,
|
|
101
|
+
content: content,
|
|
146
102
|
status: status,
|
|
147
103
|
edited_by: edited_by,
|
|
148
104
|
change_note: change_note
|
|
@@ -168,8 +124,20 @@ module RailsConsoleAi
|
|
|
168
124
|
|
|
169
125
|
private
|
|
170
126
|
|
|
171
|
-
def
|
|
172
|
-
|
|
127
|
+
def sync_name_from_content
|
|
128
|
+
return if content.to_s.strip.empty?
|
|
129
|
+
parsed_name = parsed['name'].to_s.strip
|
|
130
|
+
self.name = parsed_name unless parsed_name.empty?
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def content_parses
|
|
134
|
+
return if content.to_s.strip.empty?
|
|
135
|
+
hash = RailsConsoleAi::AgentLoader.parse(content.to_s)
|
|
136
|
+
if hash.nil?
|
|
137
|
+
errors.add(:content, "could not be parsed — expected YAML frontmatter between `---` lines followed by a markdown body")
|
|
138
|
+
elsif hash['name'].to_s.strip.empty?
|
|
139
|
+
errors.add(:content, "frontmatter is missing a `name:` field")
|
|
140
|
+
end
|
|
173
141
|
end
|
|
174
142
|
end
|
|
175
143
|
end
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
require '
|
|
1
|
+
require 'rails_console_ai/agent_loader'
|
|
2
2
|
|
|
3
3
|
module RailsConsoleAi
|
|
4
4
|
class AgentVersion < ActiveRecord::Base
|
|
@@ -21,26 +21,14 @@ module RailsConsoleAi
|
|
|
21
21
|
end
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
-
def
|
|
25
|
-
|
|
24
|
+
def parsed
|
|
25
|
+
@parsed ||= (RailsConsoleAi::AgentLoader.parse(content.to_s) || {})
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
def
|
|
29
|
-
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
|
|
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 body; parsed['body']; end
|
|
30
|
+
def max_rounds; parsed['max_rounds']; end
|
|
31
|
+
def model; parsed['model']; end
|
|
32
|
+
def tools; Array(parsed['tools']); end
|
|
45
33
|
end
|
|
46
34
|
end
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
require '
|
|
1
|
+
require 'rails_console_ai/tools/memory_tools'
|
|
2
2
|
|
|
3
3
|
module RailsConsoleAi
|
|
4
4
|
class Memory < ActiveRecord::Base
|
|
@@ -10,7 +10,11 @@ module RailsConsoleAi
|
|
|
10
10
|
foreign_key: :memory_id,
|
|
11
11
|
dependent: :nullify
|
|
12
12
|
|
|
13
|
+
validates :content, presence: true
|
|
13
14
|
validates :name, presence: true, uniqueness: { case_sensitive: false }
|
|
15
|
+
validate :content_parses
|
|
16
|
+
|
|
17
|
+
before_validation :sync_name_from_content
|
|
14
18
|
|
|
15
19
|
scope :alphabetical, -> { order(Arel.sql('LOWER(name)')) }
|
|
16
20
|
|
|
@@ -24,24 +28,21 @@ module RailsConsoleAi
|
|
|
24
28
|
end
|
|
25
29
|
end
|
|
26
30
|
|
|
27
|
-
def
|
|
28
|
-
|
|
31
|
+
def parsed
|
|
32
|
+
@parsed ||= (RailsConsoleAi::Tools::MemoryTools.parse(content.to_s) || {})
|
|
29
33
|
end
|
|
30
34
|
|
|
31
|
-
def
|
|
32
|
-
|
|
35
|
+
def content=(value)
|
|
36
|
+
@parsed = nil
|
|
37
|
+
super
|
|
33
38
|
end
|
|
34
39
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def last_used_at
|
|
40
|
-
has_attribute?(:last_used_at) ? read_attribute(:last_used_at) : nil
|
|
41
|
-
end
|
|
40
|
+
# Memories don't have a separate description vs body — the markdown body
|
|
41
|
+
# IS the memory. Parser exposes it under 'description'.
|
|
42
|
+
def description; parsed['description']; end
|
|
43
|
+
def tags; Array(parsed['tags']); end
|
|
42
44
|
|
|
43
45
|
def self.record_use!(id)
|
|
44
|
-
return false unless connection.column_exists?(table_name, :use_count)
|
|
45
46
|
where(id: id).update_all([
|
|
46
47
|
'use_count = COALESCE(use_count, 0) + 1, last_used_at = ?',
|
|
47
48
|
Time.now.utc
|
|
@@ -58,6 +59,7 @@ module RailsConsoleAi
|
|
|
58
59
|
'name' => name,
|
|
59
60
|
'description' => description,
|
|
60
61
|
'tags' => tags,
|
|
62
|
+
'content' => content,
|
|
61
63
|
'use_count' => use_count,
|
|
62
64
|
'last_used_at' => last_used_at,
|
|
63
65
|
'source' => :db,
|
|
@@ -72,8 +74,7 @@ module RailsConsoleAi
|
|
|
72
74
|
RailsConsoleAi::MemoryVersion.create!(
|
|
73
75
|
memory_id: id,
|
|
74
76
|
name: name,
|
|
75
|
-
|
|
76
|
-
tags: tags,
|
|
77
|
+
content: content,
|
|
77
78
|
edited_by: edited_by,
|
|
78
79
|
change_note: change_note
|
|
79
80
|
)
|
|
@@ -83,16 +84,20 @@ module RailsConsoleAi
|
|
|
83
84
|
|
|
84
85
|
private
|
|
85
86
|
|
|
86
|
-
def
|
|
87
|
-
return
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
rescue JSON::ParserError
|
|
91
|
-
[]
|
|
87
|
+
def sync_name_from_content
|
|
88
|
+
return if content.to_s.strip.empty?
|
|
89
|
+
parsed_name = parsed['name'].to_s.strip
|
|
90
|
+
self.name = parsed_name unless parsed_name.empty?
|
|
92
91
|
end
|
|
93
92
|
|
|
94
|
-
def
|
|
95
|
-
|
|
93
|
+
def content_parses
|
|
94
|
+
return if content.to_s.strip.empty?
|
|
95
|
+
hash = RailsConsoleAi::Tools::MemoryTools.parse(content.to_s)
|
|
96
|
+
if hash.nil?
|
|
97
|
+
errors.add(:content, "could not be parsed — expected YAML frontmatter between `---` lines followed by a markdown body")
|
|
98
|
+
elsif hash['name'].to_s.strip.empty?
|
|
99
|
+
errors.add(:content, "frontmatter is missing a `name:` field")
|
|
100
|
+
end
|
|
96
101
|
end
|
|
97
102
|
end
|
|
98
103
|
end
|