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.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +46 -0
  3. data/README.md +65 -0
  4. data/app/controllers/rails_console_ai/agent_versions_controller.rb +29 -0
  5. data/app/controllers/rails_console_ai/agents_controller.rb +176 -0
  6. data/app/controllers/rails_console_ai/application_controller.rb +5 -0
  7. data/app/controllers/rails_console_ai/memories_controller.rb +136 -0
  8. data/app/controllers/rails_console_ai/memory_versions_controller.rb +29 -0
  9. data/app/controllers/rails_console_ai/skill_versions_controller.rb +29 -0
  10. data/app/controllers/rails_console_ai/skills_controller.rb +171 -0
  11. data/app/helpers/rails_console_ai/diff_helper.rb +114 -0
  12. data/app/models/rails_console_ai/agent.rb +143 -0
  13. data/app/models/rails_console_ai/agent_version.rb +34 -0
  14. data/app/models/rails_console_ai/memory.rb +103 -0
  15. data/app/models/rails_console_ai/memory_version.rb +31 -0
  16. data/app/models/rails_console_ai/session.rb +1 -1
  17. data/app/models/rails_console_ai/skill.rb +148 -0
  18. data/app/models/rails_console_ai/skill_version.rb +33 -0
  19. data/app/views/layouts/rails_console_ai/application.html.erb +78 -1
  20. data/app/views/rails_console_ai/agent_versions/index.html.erb +28 -0
  21. data/app/views/rails_console_ai/agent_versions/show.html.erb +25 -0
  22. data/app/views/rails_console_ai/agents/_form.html.erb +40 -0
  23. data/app/views/rails_console_ai/agents/diff.html.erb +15 -0
  24. data/app/views/rails_console_ai/agents/edit.html.erb +7 -0
  25. data/app/views/rails_console_ai/agents/index.html.erb +80 -0
  26. data/app/views/rails_console_ai/agents/new.html.erb +8 -0
  27. data/app/views/rails_console_ai/agents/show.html.erb +108 -0
  28. data/app/views/rails_console_ai/memories/_form.html.erb +29 -0
  29. data/app/views/rails_console_ai/memories/diff.html.erb +15 -0
  30. data/app/views/rails_console_ai/memories/edit.html.erb +7 -0
  31. data/app/views/rails_console_ai/memories/index.html.erb +67 -0
  32. data/app/views/rails_console_ai/memories/new.html.erb +8 -0
  33. data/app/views/rails_console_ai/memories/show.html.erb +65 -0
  34. data/app/views/rails_console_ai/memory_versions/index.html.erb +26 -0
  35. data/app/views/rails_console_ai/memory_versions/show.html.erb +21 -0
  36. data/app/views/rails_console_ai/skill_versions/index.html.erb +28 -0
  37. data/app/views/rails_console_ai/skill_versions/show.html.erb +23 -0
  38. data/app/views/rails_console_ai/skills/_form.html.erb +43 -0
  39. data/app/views/rails_console_ai/skills/diff.html.erb +15 -0
  40. data/app/views/rails_console_ai/skills/edit.html.erb +7 -0
  41. data/app/views/rails_console_ai/skills/index.html.erb +79 -0
  42. data/app/views/rails_console_ai/skills/new.html.erb +8 -0
  43. data/app/views/rails_console_ai/skills/show.html.erb +94 -0
  44. data/config/routes.rb +39 -0
  45. data/lib/rails_console_ai/agent_loader.rb +139 -43
  46. data/lib/rails_console_ai/agent_runner.rb +209 -0
  47. data/lib/rails_console_ai/channel/api.rb +139 -0
  48. data/lib/rails_console_ai/conversation_engine.rb +19 -13
  49. data/lib/rails_console_ai/session_logger.rb +10 -0
  50. data/lib/rails_console_ai/skill_loader.rb +130 -29
  51. data/lib/rails_console_ai/storage/database_storage.rb +195 -0
  52. data/lib/rails_console_ai/tools/memory_tools.rb +110 -32
  53. data/lib/rails_console_ai/tools/registry.rb +99 -8
  54. data/lib/rails_console_ai/version.rb +1 -1
  55. data/lib/rails_console_ai.rb +240 -0
  56. data/lib/tasks/rails_console_ai.rake +7 -0
  57. metadata +55 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 30c7955113bc3e4ce45989fcc0e93034185fbabc0e95acfa01df56ec397900b5
4
- data.tar.gz: 87b0399d973e448404b234820067ed2f20d4148f78613963904c9e288a17d87a
3
+ metadata.gz: '083adc4ffd8b69f950fbc4ab18f2ac056e8642c6946c8ba46e67e19e4299e0f2'
4
+ data.tar.gz: a01fef0fe45dfa157d638a7d4edb1ee5c8d9753be3a79956b3ebde220d3ee939
5
5
  SHA512:
6
- metadata.gz: ad1091c711239e286408f0133378c3597c83075b3f9435a40f43bf2df4fa279cf459cf3af84c58027842d2da4c8bccc5d11bfbec616c6e3ec1212784ad59b764
7
- data.tar.gz: 61ea9cc3afd50b35ec3d0740b723a076175731d1289c4a98ee5727aaec78c993abb0b78637cb6ac7ef4b550b81b473ff287d83c32405c8fbd345dbf05b0b3b72
6
+ metadata.gz: 56b8fdcd0cf445d9480d4e56e252271241af017c8ca51e2a11af36d2158ce4a3a1fdf9670685f931b8cbd0994e6828e6bac08d10247e8bec1af8c66ad83c47f4
7
+ data.tar.gz: 6ccb72b8b8e9b157b28722d1e532d2abf5df8037f4ba4f1f3635bb37ad8a3441e48d832d903f6f92b6dce80106082215368dde15626eb0fc41d93da60100f261
data/CHANGELOG.md CHANGED
@@ -2,6 +2,52 @@
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
+
11
+ ## [0.30.0]
12
+
13
+ - Add background agent support — new `run_agent` mechanism to launch agents asynchronously alongside an HTTP API channel
14
+ - Store skills and memories in the database with versioning, side-by-side diff, and restore, in addition to on-disk files
15
+ - Store sub-agents in the database with the same versioned workflow as skills
16
+ - Require human approval before the AI can use DB-backed skills or sub-agents; editing reverts them to proposed
17
+ - Track per-record usage (`use_count`, `last_used_at`) for DB-backed skills, memories, and sub-agents, surfaced in the web UI
18
+ - Add web UI sections for skills, memories, and agents with list / view / create / edit / delete / approve / version history
19
+ - Add `save_agent`, `delete_agent` AI tools and a `target` parameter on `save_skill` / `save_memory` for DB vs file
20
+ - Add a "Paste a .md file" import box on new skill / memory / agent pages that prefills the form from frontmatter
21
+ - Make `RailsConsoleAi.migrate!` re-run column probes on upgrades so new columns are added on existing installs
22
+ - Harden `Skill` / `Agent` model accessors to return safe defaults when newly added columns are not yet present
23
+ - Fix built-in agent `.md` files with UTF-8 characters failing to load under US-ASCII locales
24
+
25
+ ## [Unreleased]
26
+
27
+ - New skill / memory / agent pages now have a "Paste a .md file" textarea at the top. Paste the contents of a Markdown file with YAML frontmatter (same format used by `.rails_console_ai/{skills,memories,agents}/*.md` and the gem's built-in agents), click "Parse pasted content ↓", and the form fields below are prefilled from the parse. You still click Create to actually save, so the normal proposed-status + version-row flow applies — and you can tweak the parsed content before saving. Useful for moving an existing on-disk record into the versioned DB store
28
+ - `RailsConsoleAi::SkillLoader.parse`, `RailsConsoleAi::AgentLoader.parse`, and `RailsConsoleAi::Tools::MemoryTools.parse` are now public class methods. They return a parsed frontmatter+body hash, or `nil` on malformed input
29
+
30
+ - Usage tracking for DB-backed skills, memories, and agents. New `use_count` and `last_used_at` columns are bumped atomically (one SQL `UPDATE … SET use_count = use_count + 1, last_used_at = NOW()`, no callbacks, no `updated_at` change) when:
31
+ - `activate_skill` resolves a DB skill
32
+ - `recall_memory` resolves a DB memory, or `recall_memories` returns a DB memory in its result set
33
+ - `delegate_task` resolves a DB sub-agent
34
+ File / built-in records have no DB row so they're not tracked (the web UI shows `—` for them). System-prompt summary inclusion does **not** count — counters only move when the AI actively invokes/loads the record. Surfaced in the index tables (with a "Sort: most used" toggle) and on each show page (Times activated / Times recalled / Times invoked, plus Last used)
35
+ - Bugfix: `RailsConsoleAi.migrate!` now always re-runs the `setup_*_tables!` methods so column-add probes execute on upgrades. Previously, when the base table already existed, the outer `unless table_exists?` guard skipped column probes entirely, leaving new columns un-added and causing `NameError: undefined local variable or method 'status'` from `Skill#proposed?` on host apps that hadn't fully migrated
36
+ - Bugfix: `Skill` / `Agent` model accessors for `status` / `approved_by` / `approved_at` / `use_count` / `last_used_at` are now defensive — they return safe defaults via `has_attribute?` rather than raising NameError when the column hasn't been added yet. The `status` inclusion validation is also gated on the column being present
37
+
38
+ - Sub-agents can now be stored in the database, versioned, and approved through the web UI — same workflow as skills. DB-backed agents start as `proposed` and are invisible to `delegate_task` until a human clicks Approve at `/rails_console_ai/agents`; editing an approved agent reverts it to proposed. Built-in (gem-shipped) and file-based agents are pre-approved and continue working unchanged
39
+ - New AI tools `save_agent` and `delete_agent` — the AI can now draft a sub-agent definition (lands as proposed, awaits human approval) and request deletion. `delegate_task` now emits a specific "awaiting human approval" error when the AI references a proposed DB agent
40
+ - New web UI section at `/rails_console_ai/agents` with three-source listing (DB / FILE / BUILTIN badges) plus the full skills-style CRUD + version history + diff + restore + approve. Built-in agents are read-only with a "Create DB override" link that prefills the new-agent form
41
+ - `ai_db_setup` / `ai_db_migrate` now also create `rails_console_ai_agents` and `rails_console_ai_agent_versions` tables idempotently
42
+ - Fix: built-in agent .md files containing UTF-8 (em-dashes, smart quotes) now load correctly. `safe_load_builtin_agents` was using `File.read`'s locale-default encoding, which silently swallowed `Encoding::CompatibilityError` on US-ASCII locales
43
+
44
+ - Skills and memories can now be stored in the database (in addition to the existing on-disk `.rails_console_ai/skills` and `.rails_console_ai/memories` files). DB-backed records are versioned — every save creates a `SkillVersion` / `MemoryVersion` row with `edited_by` and an optional change note
45
+ - DB-backed skills start in a **proposed** state and cannot be activated by the AI until a human approves them in the web UI. Editing an approved skill reverts it to proposed. File-backed skills are unaffected (already git-tracked, considered pre-approved). Memories are not gated
46
+ - `SkillLoader#load_all_skills` and `MemoryTools#load_all_memories` now return the union of DB and file records (DB wins on name collision). `SkillLoader#find_skill` / `skill_summaries` filter out proposed skills so the AI never sees them
47
+ - `save_skill` / `save_memory` AI tools accept an optional `target` parameter (`"db"` default, `"file"` to write to disk) plus `change_note`. When the AI saves a DB skill, the tool response tells it the skill is awaiting human approval
48
+ - New web UI sections at `/rails_console_ai/skills` and `/rails_console_ai/memories` provide list, view, create, edit, delete, version history, side-by-side diff, and restore. Skills also have an Approve button and PROPOSED / APPROVED badges. File-sourced records are surfaced but read-only in the UI
49
+ - `ai_db_setup` / `ai_db_migrate` now create the new `rails_console_ai_skills`, `rails_console_ai_skill_versions`, `rails_console_ai_memories`, and `rails_console_ai_memory_versions` tables idempotently, plus the `status` / `approved_by` / `approved_at` columns on skills
50
+
5
51
  ## [0.29.0]
6
52
 
7
53
  - Allow steering Slack conversations mid-run by sending follow-up messages that are folded in as user guidance at the next tool-loop boundary
data/README.md CHANGED
@@ -504,6 +504,71 @@ This starts a long-running process (run it separately from your web server). The
504
504
  - **Channels** — the bot only responds when @mentioned. @mention it in any channel message or thread to start a session. The person who first @mentions the bot owns the session — only they can continue the conversation, and they must @mention the bot on each message. Exception: when the bot asks a question, the owner can reply without @mentioning.
505
505
  - **Joining threads** — when @mentioned mid-thread, the bot reads the thread history for context so it understands what's already been discussed.
506
506
 
507
+ ## Background Agents
508
+
509
+ Fire off agent runs from your application code and pick up the result later. Useful when you want to delegate a question (or a multi-step task) to the AI from a controller action, a job, a webhook handler, or any place where blocking on an LLM round-trip is undesirable.
510
+
511
+ ```ruby
512
+ id = RailsConsoleAi.run_agent("How many users signed up yesterday?", name: 'daily-stats', user_name: 'cron')
513
+ # => 4821 (Integer session id, returned immediately)
514
+
515
+ RailsConsoleAi.check_agent(id)
516
+ # => 'queued' | 'running' | 'ready' | 'failed' | nil
517
+
518
+ RailsConsoleAi.get_agent_response(id)
519
+ # => { status: 'ready', result: "1,432 users signed up yesterday.\n", error: nil }
520
+ ```
521
+
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
+
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
+
541
+ ### Running the background runner
542
+
543
+ ```bash
544
+ bundle exec rake rails_console_ai:agents
545
+ # AGENT_CONCURRENCY=3 by default; bump it for more parallelism
546
+ AGENT_CONCURRENCY=8 bundle exec rake rails_console_ai:agents
547
+ ```
548
+
549
+ The background runner polls every 2 seconds, claims queued rows atomically (only one worker wins a row), and updates each row to `status='ready'` with the result, or `status='failed'` with an error message. SIGINT/SIGTERM triggers a graceful drain — up to 60 seconds for in-flight jobs, then any stragglers are marked `failed`.
550
+
551
+ Run it separately from your web server, alongside (or instead of) the Slack bot. If the process crashes mid-job, rows stuck in `status='running'` are left as-is — no built-in reaper yet.
552
+
553
+ ### Requirements
554
+
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.
556
+ - `session_logging` must be enabled (it is by default).
557
+
558
+ ### Behavior notes
559
+
560
+ - The agent runs with `Channel::Api`, a non-interactive channel — `prompt` returns `''` and `confirm` auto-yes, so the agent never blocks waiting for human input. Safety guards are always on (`supports_danger?` is `false`), and `bypass_guards_for_methods` still applies as usual.
561
+ - `result` is composed from the agent's prose answer plus the inspected return value of any code it executed:
562
+
563
+ ```
564
+ Let me query the users table for signups from yesterday.
565
+
566
+ Result: 1432
567
+ ```
568
+
569
+ If the agent answers without running code, you get just the prose. The raw code, its stdout, and the raw return value also live on the session row as `code_executed` / `code_output` / `code_result` if you need them.
570
+ - The queue row and the result row are the same row, so the existing session viewer at `/rails_console_ai` shows background runs alongside REPL and Slack sessions.
571
+
507
572
  ## Requirements
508
573
 
509
574
  Ruby >= 2.5, Rails >= 5.0, Faraday >= 1.0. For Bedrock: `aws-sdk-bedrockruntime` (loaded lazily, not a hard dependency).
@@ -0,0 +1,29 @@
1
+ module RailsConsoleAi
2
+ class AgentVersionsController < ApplicationController
3
+ before_action :load_agent
4
+
5
+ def index
6
+ @versions = @agent.versions
7
+ end
8
+
9
+ def show
10
+ @version = @agent.versions.find(params[:id])
11
+ end
12
+
13
+ def restore
14
+ version = @agent.versions.find(params[:id])
15
+ @agent.update_with_version!(
16
+ { content: version.content },
17
+ edited_by: params[:edited_by].presence || 'web',
18
+ change_note: "Restored from version ##{version.id}"
19
+ )
20
+ redirect_to agent_path(@agent), notice: "Restored version ##{version.id}."
21
+ end
22
+
23
+ private
24
+
25
+ def load_agent
26
+ @agent = Agent.find(params[:agent_id])
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,176 @@
1
+ require 'rails_console_ai/agent_loader'
2
+
3
+ module RailsConsoleAi
4
+ class AgentsController < ApplicationController
5
+ before_action :load_agent, only: [:show, :edit, :update, :destroy, :approve]
6
+
7
+ def index
8
+ @agents = AgentLoader.new.load_all_agents
9
+ @q = params[:q].to_s.strip
10
+ unless @q.empty?
11
+ needle = @q.downcase
12
+ @agents = @agents.select { |a|
13
+ [a['name'], a['description'], Array(a['tools']).join(' ')].compact.join(' ').downcase.include?(needle)
14
+ }
15
+ end
16
+
17
+ @sort = params[:sort].to_s
18
+ if @sort == 'used'
19
+ @agents = @agents.sort_by { |a| [-(a['use_count'].to_i), a['name'].to_s.downcase] }
20
+ end
21
+ end
22
+
23
+ def show
24
+ @versions = @agent.versions if @agent.is_a?(RailsConsoleAi::Agent)
25
+ end
26
+
27
+ def new
28
+ # Allow prefilling from a built-in (the "Create override" action on the show page).
29
+ @agent = Agent.new
30
+ if params[:from_builtin].present?
31
+ builtin = AgentLoader.new.load_all_agents.find { |a|
32
+ a['source'] == :builtin && a['name'].to_s.downcase == params[:from_builtin].to_s.downcase
33
+ }
34
+ if builtin
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
+ )
43
+ end
44
+ end
45
+ @agent.content ||= new_agent_template
46
+ end
47
+
48
+ def create
49
+ @agent = Agent.new
50
+ begin
51
+ @agent.update_with_version!(
52
+ agent_params,
53
+ edited_by: edited_by_param,
54
+ change_note: params[:change_note].presence
55
+ )
56
+ redirect_to agent_path(@agent), notice: 'Agent created.'
57
+ rescue ActiveRecord::RecordInvalid => e
58
+ flash.now[:alert] = e.message
59
+ render :new
60
+ end
61
+ end
62
+
63
+ def edit
64
+ redirect_to agents_path, alert: read_only_message and return unless @agent.is_a?(RailsConsoleAi::Agent)
65
+ end
66
+
67
+ def update
68
+ redirect_to agents_path, alert: read_only_message and return unless @agent.is_a?(RailsConsoleAi::Agent)
69
+
70
+ begin
71
+ @agent.update_with_version!(
72
+ agent_params,
73
+ edited_by: edited_by_param,
74
+ change_note: params[:change_note].presence
75
+ )
76
+ redirect_to agent_path(@agent), notice: 'Agent updated.'
77
+ rescue ActiveRecord::RecordInvalid => e
78
+ flash.now[:alert] = e.message
79
+ render :edit
80
+ end
81
+ end
82
+
83
+ def destroy
84
+ if @agent.is_a?(RailsConsoleAi::Agent)
85
+ @agent.destroy
86
+ redirect_to agents_path, notice: 'Agent deleted. Past versions remain in history.'
87
+ else
88
+ redirect_to agents_path, alert: read_only_message
89
+ end
90
+ end
91
+
92
+ def approve
93
+ redirect_to agents_path, alert: read_only_message and return unless @agent.is_a?(RailsConsoleAi::Agent)
94
+
95
+ approver = params[:approved_by].presence || 'web'
96
+
97
+ if @agent.approved?
98
+ redirect_to agent_path(@agent), notice: 'Agent is already approved.'
99
+ return
100
+ end
101
+
102
+ begin
103
+ @agent.approve!(approved_by: approver)
104
+ redirect_to agent_path(@agent), notice: "Approved by #{approver}. The AI can now invoke this agent via delegate_task."
105
+ rescue ArgumentError, ActiveRecord::RecordInvalid => e
106
+ redirect_to agent_path(@agent), alert: "Could not approve: #{e.message}"
107
+ end
108
+ end
109
+
110
+ def diff
111
+ @agent = Agent.find(params[:agent_id])
112
+ @from = @agent.versions.find(params[:from])
113
+ @to = params[:to].present? ? @agent.versions.find(params[:to]) : nil
114
+ @to_label = @to ? "Version ##{@to.id}" : 'Current'
115
+ @to_content = @to ? @to.content : @agent.content
116
+ end
117
+
118
+ private
119
+
120
+ def load_agent
121
+ if params[:id].to_s =~ /\A\d+\z/
122
+ @agent = Agent.find(params[:id])
123
+ return
124
+ end
125
+
126
+ # Non-numeric :id — prefer the AR record if a DB row matches by name or slug.
127
+ ar = Agent.where('LOWER(name) = ?', params[:id].to_s.downcase).first
128
+ ar ||= Agent.all.find { |a| slugify(a.name) == params[:id] }
129
+ if ar
130
+ @agent = ar
131
+ return
132
+ end
133
+
134
+ all = AgentLoader.new.load_all_agents
135
+ @agent = all.find { |a| slugify(a['name']) == params[:id] || a['name'] == params[:id] }
136
+ raise ActiveRecord::RecordNotFound, "Agent not found: #{params[:id]}" unless @agent
137
+ end
138
+
139
+ def agent_params
140
+ { content: params.require(:agent)[:content].to_s }
141
+ end
142
+
143
+ def edited_by_param
144
+ params[:edited_by].presence || 'web'
145
+ end
146
+
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
159
+ end
160
+
161
+ def slugify(name)
162
+ name.to_s.downcase.strip
163
+ .gsub(/[^a-z0-9\s-]/, '')
164
+ .gsub(/[\s]+/, '-')
165
+ .gsub(/-+/, '-')
166
+ end
167
+
168
+ def read_only_message
169
+ if @agent.is_a?(Hash) && @agent['source'] == :builtin
170
+ 'This is a built-in agent shipped with the gem. To customize it, create a same-named DB agent override.'
171
+ else
172
+ 'This agent lives on disk under .rails_console_ai/agents/. Edit the file directly to change it.'
173
+ end
174
+ end
175
+ end
176
+ end
@@ -1,5 +1,10 @@
1
1
  module RailsConsoleAi
2
2
  class ApplicationController < ActionController::Base
3
+ # Pin to the engine's layout. Without this, Rails layout auto-resolution can
4
+ # fall back to the host app's `layouts/application` (which usually references
5
+ # host-only helpers like `logout_url`).
6
+ layout 'rails_console_ai/application'
7
+
3
8
  protect_from_forgery with: :exception
4
9
 
5
10
  before_action :rails_console_ai_authenticate!
@@ -0,0 +1,136 @@
1
+ require 'rails_console_ai/tools/memory_tools'
2
+
3
+ module RailsConsoleAi
4
+ class MemoriesController < ApplicationController
5
+ before_action :load_memory, only: [:show, :edit, :update, :destroy]
6
+
7
+ def index
8
+ @memories = Tools::MemoryTools.new.load_all_memories
9
+ @q = params[:q].to_s.strip
10
+ unless @q.empty?
11
+ needle = @q.downcase
12
+ @memories = @memories.select { |m|
13
+ [m['name'], m['description'], Array(m['tags']).join(' ')].compact.join(' ').downcase.include?(needle)
14
+ }
15
+ end
16
+
17
+ @sort = params[:sort].to_s
18
+ if @sort == 'used'
19
+ @memories = @memories.sort_by { |m| [-(m['use_count'].to_i), m['name'].to_s.downcase] }
20
+ end
21
+ end
22
+
23
+ def show
24
+ @versions = @memory.versions if @memory.is_a?(RailsConsoleAi::Memory)
25
+ end
26
+
27
+ def new
28
+ @memory = Memory.new(content: new_memory_template)
29
+ end
30
+
31
+ def create
32
+ @memory = Memory.new
33
+ attrs = memory_params
34
+ begin
35
+ @memory.update_with_version!(
36
+ attrs,
37
+ edited_by: edited_by_param,
38
+ change_note: params[:change_note].presence
39
+ )
40
+ redirect_to memory_path(@memory), notice: 'Memory created.'
41
+ rescue ActiveRecord::RecordInvalid => e
42
+ flash.now[:alert] = e.message
43
+ render :new
44
+ end
45
+ end
46
+
47
+ def edit
48
+ redirect_to memories_path, alert: file_memory_message and return unless @memory.is_a?(RailsConsoleAi::Memory)
49
+ end
50
+
51
+ def update
52
+ redirect_to memories_path, alert: file_memory_message and return unless @memory.is_a?(RailsConsoleAi::Memory)
53
+
54
+ begin
55
+ @memory.update_with_version!(
56
+ memory_params,
57
+ edited_by: edited_by_param,
58
+ change_note: params[:change_note].presence
59
+ )
60
+ redirect_to memory_path(@memory), notice: 'Memory updated.'
61
+ rescue ActiveRecord::RecordInvalid => e
62
+ flash.now[:alert] = e.message
63
+ render :edit
64
+ end
65
+ end
66
+
67
+ def destroy
68
+ if @memory.is_a?(RailsConsoleAi::Memory)
69
+ @memory.destroy
70
+ redirect_to memories_path, notice: 'Memory deleted. Past versions remain in history.'
71
+ else
72
+ redirect_to memories_path, alert: file_memory_message
73
+ end
74
+ end
75
+
76
+ def diff
77
+ @memory = Memory.find(params[:memory_id])
78
+ @from = @memory.versions.find(params[:from])
79
+ @to = params[:to].present? ? @memory.versions.find(params[:to]) : nil
80
+ @to_label = @to ? "Version ##{@to.id}" : 'Current'
81
+ @to_content = @to ? @to.content : @memory.content
82
+ end
83
+
84
+ private
85
+
86
+ def load_memory
87
+ if params[:id].to_s =~ /\A\d+\z/
88
+ @memory = Memory.find(params[:id])
89
+ return
90
+ end
91
+
92
+ # Non-numeric :id — prefer the AR record if a DB row matches by name or slug,
93
+ # so write actions (update/destroy) get the AR object, not a read-only Hash.
94
+ ar = Memory.where('LOWER(name) = ?', params[:id].to_s.downcase).first
95
+ ar ||= Memory.all.find { |m| slugify(m.name) == params[:id] }
96
+ if ar
97
+ @memory = ar
98
+ return
99
+ end
100
+
101
+ all = Tools::MemoryTools.new.load_all_memories
102
+ @memory = all.find { |m| slugify(m['name']) == params[:id] || m['name'] == params[:id] }
103
+ raise ActiveRecord::RecordNotFound, "Memory not found: #{params[:id]}" unless @memory
104
+ end
105
+
106
+ def memory_params
107
+ { content: params.require(:memory)[:content].to_s }
108
+ end
109
+
110
+ def edited_by_param
111
+ params[:edited_by].presence || 'web'
112
+ end
113
+
114
+ def new_memory_template
115
+ <<~MD
116
+ ---
117
+ name:
118
+ tags: []
119
+ ---
120
+
121
+ The fact or pattern you're persisting.
122
+ MD
123
+ end
124
+
125
+ def slugify(name)
126
+ name.to_s.downcase.strip
127
+ .gsub(/[^a-z0-9\s-]/, '')
128
+ .gsub(/[\s]+/, '-')
129
+ .gsub(/-+/, '-')
130
+ end
131
+
132
+ def file_memory_message
133
+ 'This memory lives on disk under .rails_console_ai/memories/. Edit the file directly to change it.'
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,29 @@
1
+ module RailsConsoleAi
2
+ class MemoryVersionsController < ApplicationController
3
+ before_action :load_memory
4
+
5
+ def index
6
+ @versions = @memory.versions
7
+ end
8
+
9
+ def show
10
+ @version = @memory.versions.find(params[:id])
11
+ end
12
+
13
+ def restore
14
+ version = @memory.versions.find(params[:id])
15
+ @memory.update_with_version!(
16
+ { content: version.content },
17
+ edited_by: params[:edited_by].presence || 'web',
18
+ change_note: "Restored from version ##{version.id}"
19
+ )
20
+ redirect_to memory_path(@memory), notice: "Restored version ##{version.id}."
21
+ end
22
+
23
+ private
24
+
25
+ def load_memory
26
+ @memory = Memory.find(params[:memory_id])
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,29 @@
1
+ module RailsConsoleAi
2
+ class SkillVersionsController < ApplicationController
3
+ before_action :load_skill
4
+
5
+ def index
6
+ @versions = @skill.versions
7
+ end
8
+
9
+ def show
10
+ @version = @skill.versions.find(params[:id])
11
+ end
12
+
13
+ def restore
14
+ version = @skill.versions.find(params[:id])
15
+ @skill.update_with_version!(
16
+ { content: version.content },
17
+ edited_by: params[:edited_by].presence || 'web',
18
+ change_note: "Restored from version ##{version.id}"
19
+ )
20
+ redirect_to skill_path(@skill), notice: "Restored version ##{version.id}."
21
+ end
22
+
23
+ private
24
+
25
+ def load_skill
26
+ @skill = Skill.find(params[:skill_id])
27
+ end
28
+ end
29
+ end