Class: RailsConsoleAi::Tools::MemoryTools
- Inherits:
-
Object
- Object
- RailsConsoleAi::Tools::MemoryTools
- Defined in:
- lib/rails_console_ai/tools/memory_tools.rb
Constant Summary collapse
- MEMORIES_DIR =
'memories'
Class Method Summary collapse
-
.dump(name:, description:, tags: []) ⇒ Object
Inverse of parse: emit a canonical .md string.
-
.parse(content) ⇒ Object
Public: parse a raw .md (YAML frontmatter + body) string into a hash.
Instance Method Summary collapse
- #delete_memory(name:) ⇒ Object
-
#initialize(storage = nil) ⇒ MemoryTools
constructor
A new instance of MemoryTools.
-
#load_activatable_memories ⇒ Object
Memories the AI is allowed to see / recall: approved DB memories + all file memories.
-
#load_all_memories ⇒ Object
Includes proposed (unapproved) DB memories — they show up in the admin UI with a "PROPOSED" badge.
- #memory_summaries ⇒ Object
- #recall_memories(query: nil, tag: nil) ⇒ Object
- #recall_memory(name:) ⇒ Object
-
#save_memory(name:, description:, tags: [], target: :db, edited_by: nil, change_note: nil) ⇒ Object
target: :db (default) | :file Falls back to :file (with a notice in the return string) if DB tables aren't set up.
Constructor Details
#initialize(storage = nil) ⇒ MemoryTools
Returns a new instance of MemoryTools.
9 10 11 |
# File 'lib/rails_console_ai/tools/memory_tools.rb', line 9 def initialize(storage = nil) @storage = storage || RailsConsoleAi.storage end |
Class Method Details
.dump(name:, description:, tags: []) ⇒ Object
Inverse of parse: emit a canonical .md string. The DB store uses this minimal form (name + tags only in frontmatter); the file store layers created_at/updated_at on top via save_memory_to_file.
240 241 242 243 |
# File 'lib/rails_console_ai/tools/memory_tools.rb', line 240 def self.dump(name:, description:, tags: []) frontmatter = { 'name' => name, 'tags' => Array() } "---\n#{YAML.dump(frontmatter).sub("---\n", '').strip}\n---\n\n#{description}\n" end |
.parse(content) ⇒ Object
Public: parse a raw .md (YAML frontmatter + body) string into a hash. For memories, the body is stored under the 'description' key (memories don't have a separate description vs body — the markdown IS the memory).
228 229 230 231 232 233 234 235 |
# File 'lib/rails_console_ai/tools/memory_tools.rb', line 228 def self.parse(content) return nil unless content =~ /\A---\s*\n(.*?\n)---\s*\n(.*)/m frontmatter = YAML.safe_load($1, permitted_classes: [Time, Date]) || {} description = $2.strip frontmatter.merge('description' => description) rescue Psych::SyntaxError nil end |
Instance Method Details
#delete_memory(name:) ⇒ Object
60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
# File 'lib/rails_console_ai/tools/memory_tools.rb', line 60 def delete_memory(name:) if Storage::DatabaseStorage.delete_memory_by_name(name) return "Memory deleted (db): \"#{name}\"" end key = memory_key(name) unless @storage.exists?(key) found_key = find_memory_key_by_name(name) return "No memory found: \"#{name}\"" unless found_key key = found_key end memory = load_memory_file(key) @storage.delete(key) "Memory deleted: \"#{memory ? memory['name'] : name}\"" rescue Storage::StorageError => e "FAILED to delete memory (#{e.})." end |
#load_activatable_memories ⇒ Object
Memories the AI is allowed to see / recall: approved DB memories + all file memories. File memories are considered pre-approved because they're git-tracked.
157 158 159 |
# File 'lib/rails_console_ai/tools/memory_tools.rb', line 157 def load_activatable_memories load_all_memories.reject { |m| m['source'] == :db && m['status'] != 'approved' } end |
#load_all_memories ⇒ Object
Includes proposed (unapproved) DB memories — they show up in the admin UI with a "PROPOSED" badge. The AI-facing surface (#memory_summaries, #recall_memory, #recall_memories) filters them out via #load_activatable_memories, so an unapproved memory can never be recalled.
147 148 149 150 151 152 153 |
# File 'lib/rails_console_ai/tools/memory_tools.rb', line 147 def load_all_memories db = Storage::DatabaseStorage.all_memories file = load_all_file_memories names = db.map { |m| m['name'].to_s.downcase } file.reject! { |m| names.include?(m['name'].to_s.downcase) } (db + file).sort_by { |m| m['name'].to_s.downcase } end |
#memory_summaries ⇒ Object
132 133 134 135 136 137 138 139 140 141 |
# File 'lib/rails_console_ai/tools/memory_tools.rb', line 132 def memory_summaries memories = load_activatable_memories return nil if memories.empty? memories.map { |m| = Array(m['tags']) tag_str = .empty? ? '' : " [#{.join(', ')}]" "- #{m['name']}#{tag_str}" } end |
#recall_memories(query: nil, tag: nil) ⇒ Object
98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 |
# File 'lib/rails_console_ai/tools/memory_tools.rb', line 98 def recall_memories(query: nil, tag: nil) memories = load_activatable_memories return "No memories stored yet." if memories.empty? results = memories if tag && !tag.empty? results = results.select { |m| Array(m['tags']).any? { |t| t.downcase.include?(tag.downcase) } } end if query && !query.empty? words = query.downcase.split(/\s+/) results = results.select { |m| searchable = [ m['name'].to_s.downcase, m['description'].to_s.downcase, Array(m['tags']).map(&:downcase).join(' ') ].join(' ') words.all? { |w| searchable.include?(w) } } end return "No memories matching your search." if results.empty? # Every memory in the result set was loaded into the AI's context. results.each { |m| record_use(m) } results.map { |m| line = "**#{m['name']}**\n#{m['description']}" line += "\nTags: #{m['tags'].join(', ')}" if m['tags'] && !m['tags'].empty? line }.join("\n\n---\n\n") end |
#recall_memory(name:) ⇒ Object
79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 |
# File 'lib/rails_console_ai/tools/memory_tools.rb', line 79 def recall_memory(name:) memory = load_activatable_memories.find { |m| m['name'].to_s.downcase == name.to_s.downcase } unless memory # Distinguish "doesn't exist" from "exists but isn't approved yet". proposed = load_all_memories.find { |m| m['name'].to_s.downcase == name.to_s.downcase } if proposed && proposed['source'] == :db && proposed['status'] != 'approved' return "Memory \"#{name}\" exists but is awaiting human approval and cannot be recalled yet. " \ "Ask the user to approve it in the web UI at /rails_console_ai/memories." end return "No memory found: \"#{name}\"" end record_use(memory) line = "**#{memory['name']}**\n#{memory['description']}" line += "\nTags: #{memory['tags'].join(', ')}" if memory['tags'] && !memory['tags'].empty? line end |
#save_memory(name:, description:, tags: [], target: :db, edited_by: nil, change_note: nil) ⇒ Object
target: :db (default) | :file Falls back to :file (with a notice in the return string) if DB tables aren't set up.
15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
# File 'lib/rails_console_ai/tools/memory_tools.rb', line 15 def save_memory(name:, description:, tags: [], target: :db, edited_by: nil, change_note: nil) target = (target || :db).to_sym db_fell_back = false if target == :db && !Storage::DatabaseStorage.memories_available? target = :file db_fell_back = true end if target == :file result = save_memory_to_file(name: name, description: description, tags: ) if db_fell_back result += "\nNOTE: DB storage was requested but the rails_console_ai_memories table does not exist. " \ "Run `ai_db_setup` in your Rails console to enable the versioned DB store. " \ "Saved to a file instead." end result else record, was_new = Storage::DatabaseStorage.save_memory( name: name, description: description, tags: , edited_by: edited_by || 'ai', change_note: change_note ) status_note = if record.respond_to?(:proposed?) && record.proposed? ' — status: PROPOSED. A human must approve it at /rails_console_ai/memories before you can recall it.' else '' end if was_new "Memory saved (db): \"#{record.name}\" (id=#{record.id})#{status_note}" else "Memory updated (db): \"#{record.name}\" (id=#{record.id})#{status_note}" end end rescue Storage::StorageError => e if target == :file # Preserve the original behaviour: include a hint with the raw frontmatter # so the user (or AI) can paste it manually when the filesystem is read-only. "FAILED to save (#{e.}). Add this manually to .rails_console_ai/#{memory_key(name)}:\n" \ "---\nname: #{name}\ntags: #{Array().inspect}\n---\n\n#{description}" else "FAILED to save (#{e.})." end rescue ::ActiveRecord::RecordInvalid => e "FAILED to save (#{e.})." end |