Class: RailsConsoleAi::Tools::MemoryTools

Inherits:
Object
  • Object
show all
Defined in:
lib/rails_console_ai/tools/memory_tools.rb

Constant Summary collapse

MEMORIES_DIR =
'memories'

Class Method Summary collapse

Instance Method Summary collapse

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

.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).



205
206
207
208
209
210
211
212
# File 'lib/rails_console_ai/tools/memory_tools.rb', line 205

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



55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/rails_console_ai/tools/memory_tools.rb', line 55

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.message})."
end

#load_all_memoriesObject



130
131
132
133
134
135
136
# File 'lib/rails_console_ai/tools/memory_tools.rb', line 130

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_summariesObject



119
120
121
122
123
124
125
126
127
128
# File 'lib/rails_console_ai/tools/memory_tools.rb', line 119

def memory_summaries
  memories = load_all_memories
  return nil if memories.empty?

  memories.map { |m|
    tags = Array(m['tags'])
    tag_str = tags.empty? ? '' : " [#{tags.join(', ')}]"
    "- #{m['name']}#{tag_str}"
  }
end

#recall_memories(query: nil, tag: nil) ⇒ Object



85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'lib/rails_console_ai/tools/memory_tools.rb', line 85

def recall_memories(query: nil, tag: nil)
  memories = load_all_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



74
75
76
77
78
79
80
81
82
83
# File 'lib/rails_console_ai/tools/memory_tools.rb', line 74

def recall_memory(name:)
  memory = load_all_memories.find { |m| m['name'].to_s.downcase == name.to_s.downcase }
  return "No memory found: \"#{name}\"" unless memory

  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
# 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: 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: tags,
      edited_by: edited_by || 'ai', change_note: change_note
    )
    if was_new
      "Memory saved (db): \"#{record.name}\" (id=#{record.id})"
    else
      "Memory updated (db): \"#{record.name}\" (id=#{record.id})"
    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.message}). Add this manually to .rails_console_ai/#{memory_key(name)}:\n" \
    "---\nname: #{name}\ntags: #{Array(tags).inspect}\n---\n\n#{description}"
  else
    "FAILED to save (#{e.message})."
  end
rescue ::ActiveRecord::RecordInvalid => e
  "FAILED to save (#{e.message})."
end