llmemory 0.2.2 → 0.2.4

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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +65 -1
  3. data/lib/llmemory/cli/commands/stats.rb +5 -0
  4. data/lib/llmemory/configuration.rb +22 -2
  5. data/lib/llmemory/crypto/cipher.rb +147 -0
  6. data/lib/llmemory/crypto/field_helpers.rb +110 -0
  7. data/lib/llmemory/instrumentation.rb +4 -2
  8. data/lib/llmemory/llm/anthropic.rb +10 -4
  9. data/lib/llmemory/llm/base.rb +42 -0
  10. data/lib/llmemory/llm/openai.rb +29 -13
  11. data/lib/llmemory/llm/response.rb +18 -0
  12. data/lib/llmemory/llm/tracking_client.rb +61 -0
  13. data/lib/llmemory/llm/usage.rb +31 -0
  14. data/lib/llmemory/llm/usage_ledger.rb +118 -0
  15. data/lib/llmemory/llm/usage_recorder.rb +37 -0
  16. data/lib/llmemory/llm.rb +5 -0
  17. data/lib/llmemory/long_term/episodic/memory.rb +16 -4
  18. data/lib/llmemory/long_term/episodic/storage.rb +11 -4
  19. data/lib/llmemory/long_term/episodic/storages/active_record_storage.rb +19 -6
  20. data/lib/llmemory/long_term/episodic/storages/database_storage.rb +25 -3
  21. data/lib/llmemory/long_term/episodic/storages/file_storage.rb +22 -5
  22. data/lib/llmemory/long_term/file_based/storage.rb +11 -4
  23. data/lib/llmemory/long_term/file_based/storages/active_record_storage.rb +16 -10
  24. data/lib/llmemory/long_term/file_based/storages/database_storage.rb +24 -8
  25. data/lib/llmemory/long_term/file_based/storages/file_storage.rb +28 -14
  26. data/lib/llmemory/long_term/graph_based/memory.rb +17 -3
  27. data/lib/llmemory/long_term/graph_based/storage.rb +3 -2
  28. data/lib/llmemory/long_term/graph_based/storages/active_record_storage.rb +47 -21
  29. data/lib/llmemory/long_term/procedural/memory.rb +16 -4
  30. data/lib/llmemory/long_term/procedural/storage.rb +11 -4
  31. data/lib/llmemory/long_term/procedural/storages/active_record_storage.rb +33 -13
  32. data/lib/llmemory/long_term/procedural/storages/database_storage.rb +25 -4
  33. data/lib/llmemory/long_term/procedural/storages/file_storage.rb +23 -6
  34. data/lib/llmemory/mcp/tools/memory_stats.rb +13 -0
  35. data/lib/llmemory/memory.rb +66 -15
  36. data/lib/llmemory/short_term/checkpoint.rb +5 -2
  37. data/lib/llmemory/short_term/stores/active_record_store.rb +12 -10
  38. data/lib/llmemory/short_term/stores/memory_store.rb +1 -1
  39. data/lib/llmemory/short_term/stores/postgres_store.rb +11 -5
  40. data/lib/llmemory/short_term/stores/redis_store.rb +7 -5
  41. data/lib/llmemory/short_term/stores.rb +7 -6
  42. data/lib/llmemory/vector_store/active_record_store.rb +30 -3
  43. data/lib/llmemory/vector_store/memory_store.rb +29 -3
  44. data/lib/llmemory/vector_store/openai_embeddings.rb +23 -2
  45. data/lib/llmemory/vector_store.rb +4 -3
  46. data/lib/llmemory/version.rb +1 -1
  47. data/lib/llmemory.rb +2 -0
  48. metadata +8 -1
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Llmemory
4
+ module LLM
5
+ class Usage
6
+ attr_reader :input_tokens, :output_tokens, :total_tokens
7
+
8
+ def initialize(input_tokens:, output_tokens:, total_tokens: nil)
9
+ @input_tokens = input_tokens.to_i
10
+ @output_tokens = output_tokens.to_i
11
+ @total_tokens = total_tokens.nil? ? (@input_tokens + @output_tokens) : total_tokens.to_i
12
+ end
13
+
14
+ def self.zero
15
+ new(input_tokens: 0, output_tokens: 0, total_tokens: 0)
16
+ end
17
+
18
+ def +(other)
19
+ self.class.new(
20
+ input_tokens: @input_tokens + other.input_tokens,
21
+ output_tokens: @output_tokens + other.output_tokens,
22
+ total_tokens: @total_tokens + other.total_tokens
23
+ )
24
+ end
25
+
26
+ def to_h
27
+ { input_tokens: @input_tokens, output_tokens: @output_tokens, total_tokens: @total_tokens }
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+ require_relative "../short_term/stores"
5
+
6
+ module Llmemory
7
+ module LLM
8
+ # Cumulative LLM token usage per user, persisted in the short-term store
9
+ # under a pseudo-session key (same pattern as ForgetLog).
10
+ class UsageLedger
11
+ SESSION_KEY = "__llm_usage__"
12
+
13
+ def initialize(store: nil)
14
+ @store = store || ShortTerm::Stores.build
15
+ end
16
+
17
+ def record(user_id, usage, operation:)
18
+ state = load_raw(user_id)
19
+ case operation.to_sym
20
+ when :invoke
21
+ bucket = symbolize_bucket(state[:invoke] || state["invoke"])
22
+ state = state.merge(
23
+ invoke: {
24
+ input_tokens: bucket[:input_tokens] + usage.input_tokens,
25
+ output_tokens: bucket[:output_tokens] + usage.output_tokens,
26
+ total_tokens: bucket[:total_tokens] + usage.total_tokens,
27
+ calls: bucket[:calls] + 1
28
+ }
29
+ )
30
+ when :embed
31
+ bucket = symbolize_bucket(state[:embed] || state["embed"], embed: true)
32
+ state = state.merge(
33
+ embed: {
34
+ total_tokens: bucket[:total_tokens] + usage.total_tokens,
35
+ calls: bucket[:calls] + 1
36
+ }
37
+ )
38
+ else
39
+ return totals(user_id)
40
+ end
41
+ state[:updated_at] = Time.now.iso8601
42
+ @store.save(user_id, SESSION_KEY, stringify(state))
43
+ totals(user_id)
44
+ end
45
+
46
+ def totals(user_id)
47
+ normalize(load_raw(user_id))
48
+ end
49
+
50
+ def reset!(user_id)
51
+ empty = default_state
52
+ @store.save(user_id, SESSION_KEY, stringify(empty))
53
+ empty
54
+ end
55
+
56
+ def self.format_text(totals)
57
+ inv = totals[:invoke]
58
+ emb = totals[:embed]
59
+ lines = [
60
+ "LLM TOKEN USAGE:",
61
+ " Chat/completions: #{inv[:total_tokens]} total (#{inv[:input_tokens]} in, #{inv[:output_tokens]} out, #{inv[:calls]} calls)",
62
+ " Embeddings: #{emb[:total_tokens]} total (#{emb[:calls]} calls)"
63
+ ]
64
+ lines << " Last updated: #{totals[:updated_at]}" if totals[:updated_at]
65
+ lines.join("\n")
66
+ end
67
+
68
+ private
69
+
70
+ def load_raw(user_id)
71
+ state = @store.load(user_id, SESSION_KEY)
72
+ return default_state unless state.is_a?(Hash)
73
+ normalize(state)
74
+ end
75
+
76
+ def default_state
77
+ {
78
+ invoke: { input_tokens: 0, output_tokens: 0, total_tokens: 0, calls: 0 },
79
+ embed: { total_tokens: 0, calls: 0 },
80
+ updated_at: nil
81
+ }
82
+ end
83
+
84
+ def normalize(state)
85
+ invoke = symbolize_bucket(state[:invoke] || state["invoke"])
86
+ embed = symbolize_bucket(state[:embed] || state["embed"], embed: true)
87
+ {
88
+ invoke: invoke,
89
+ embed: embed,
90
+ updated_at: state[:updated_at] || state["updated_at"]
91
+ }
92
+ end
93
+
94
+ def symbolize_bucket(bucket, embed: false)
95
+ bucket = {} unless bucket.is_a?(Hash)
96
+ if embed
97
+ {
98
+ total_tokens: (bucket[:total_tokens] || bucket["total_tokens"] || 0).to_i,
99
+ calls: (bucket[:calls] || bucket["calls"] || 0).to_i
100
+ }
101
+ else
102
+ {
103
+ input_tokens: (bucket[:input_tokens] || bucket["input_tokens"] || 0).to_i,
104
+ output_tokens: (bucket[:output_tokens] || bucket["output_tokens"] || 0).to_i,
105
+ total_tokens: (bucket[:total_tokens] || bucket["total_tokens"] || 0).to_i,
106
+ calls: (bucket[:calls] || bucket["calls"] || 0).to_i
107
+ }
108
+ end
109
+ end
110
+
111
+ def stringify(state)
112
+ state.transform_keys(&:to_s).transform_values do |v|
113
+ v.is_a?(Hash) ? v.transform_keys(&:to_s) : v
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "usage_ledger"
4
+
5
+ module Llmemory
6
+ module LLM
7
+ module UsageRecorder
8
+ module_function
9
+
10
+ def record(user_id:, usage:, operation:, store: nil)
11
+ return if user_id.nil? || user_id.to_s.empty?
12
+ return if usage.nil?
13
+
14
+ UsageLedger.new(store: store).record(user_id, usage, operation: operation)
15
+ end
16
+
17
+ def record_embed_from_store(user_id:, vector_store:, store: nil)
18
+ usage = embed_usage_from(vector_store)
19
+ return unless usage
20
+
21
+ record(user_id: user_id, usage: usage, operation: :embed, store: store)
22
+ end
23
+
24
+ def embed_usage_from(vector_store)
25
+ return nil unless vector_store
26
+
27
+ if vector_store.respond_to?(:last_usage)
28
+ usage = vector_store.last_usage
29
+ return usage unless usage.nil?
30
+ end
31
+
32
+ provider = vector_store.instance_variable_get(:@embedding_provider) if vector_store.instance_variable_defined?(:@embedding_provider)
33
+ provider&.last_usage if provider&.respond_to?(:last_usage)
34
+ end
35
+ end
36
+ end
37
+ end
data/lib/llmemory/llm.rb CHANGED
@@ -1,6 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "llm/base"
4
+ require_relative "llm/usage"
5
+ require_relative "llm/response"
6
+ require_relative "llm/usage_ledger"
7
+ require_relative "llm/usage_recorder"
8
+ require_relative "llm/tracking_client"
4
9
  require_relative "llm/openai"
5
10
  require_relative "llm/anthropic"
6
11
 
@@ -21,9 +21,10 @@ module Llmemory
21
21
 
22
22
  attr_reader :user_id, :storage
23
23
 
24
- def initialize(user_id:, storage: nil, vector_store: nil)
24
+ def initialize(user_id:, storage: nil, vector_store: nil, cipher: nil)
25
25
  @user_id = user_id
26
- @storage = storage || Storages.build
26
+ @cipher = cipher || Llmemory.build_cipher
27
+ @storage = storage || Storages.build(cipher: @cipher)
27
28
  @vector_store = vector_store
28
29
  @vector_explicit = !vector_store.nil?
29
30
  end
@@ -125,7 +126,7 @@ module Llmemory
125
126
  if @vector_explicit
126
127
  @vector_store
127
128
  elsif Llmemory.configuration.episodic_vector_enabled
128
- @vector_store ||= Llmemory::VectorStore.build(source_type: "episode")
129
+ @vector_store ||= Llmemory::VectorStore.build(source_type: "episode", cipher: @cipher)
129
130
  end
130
131
  end
131
132
 
@@ -134,6 +135,7 @@ module Llmemory
134
135
  vs = vector_store
135
136
  return if vs.nil? || text.to_s.strip.empty?
136
137
  embedding = vs.embed(text)
138
+ record_embed_usage(vs)
137
139
  return unless embedding
138
140
  vs.store(id: id, embedding: embedding, metadata: { text: text, created_at: Time.now }, user_id: @user_id)
139
141
  rescue StandardError
@@ -141,7 +143,9 @@ module Llmemory
141
143
  end
142
144
 
143
145
  def vector_candidates(query, top_k, vs)
144
- vs.search_by_text(query.to_s, top_k: top_k, user_id: @user_id).filter_map do |r|
146
+ results = vs.search_by_text(query.to_s, top_k: top_k, user_id: @user_id)
147
+ record_embed_usage(vs)
148
+ results.filter_map do |r|
145
149
  raw = @storage.get_episode(@user_id, r[:id] || r["id"])
146
150
  raw && candidate_for(raw, (r[:score] || r["score"] || 1.0).to_f)
147
151
  end
@@ -182,6 +186,14 @@ module Llmemory
182
186
  return nil if actions.empty?
183
187
  "Episode with #{normalized.size} step(s): #{actions.join(' -> ')}"
184
188
  end
189
+
190
+ def record_embed_usage(vector_store)
191
+ Llmemory::LLM::UsageRecorder.record_embed_from_store(
192
+ user_id: @user_id,
193
+ vector_store: vector_store,
194
+ store: Llmemory::ShortTerm::Stores.build(cipher: @cipher)
195
+ )
196
+ end
185
197
  end
186
198
  end
187
199
  end
@@ -12,17 +12,24 @@ module Llmemory
12
12
  Storage = Storages::MemoryStorage
13
13
 
14
14
  module Storages
15
- def self.build(store: nil, base_path: nil, database_url: nil)
15
+ def self.build(store: nil, base_path: nil, database_url: nil, cipher: nil)
16
+ resolved_cipher = cipher || Llmemory.build_cipher
16
17
  case (store || Llmemory.configuration.long_term_store).to_s.to_sym
17
18
  when :memory
18
19
  MemoryStorage.new
19
20
  when :file
20
- FileStorage.new(base_path: base_path || Llmemory.configuration.long_term_storage_path)
21
+ FileStorage.new(
22
+ base_path: base_path || Llmemory.configuration.long_term_storage_path,
23
+ cipher: resolved_cipher
24
+ )
21
25
  when :postgres, :database
22
- DatabaseStorage.new(database_url: database_url || Llmemory.configuration.database_url)
26
+ DatabaseStorage.new(
27
+ database_url: database_url || Llmemory.configuration.database_url,
28
+ cipher: resolved_cipher
29
+ )
23
30
  when :active_record, :activerecord
24
31
  require_relative "storages/active_record_storage"
25
- ActiveRecordStorage.new
32
+ ActiveRecordStorage.new(cipher: resolved_cipher)
26
33
  else
27
34
  MemoryStorage.new
28
35
  end
@@ -4,6 +4,7 @@ require "json"
4
4
  require "securerandom"
5
5
  require "time"
6
6
  require_relative "base"
7
+ require_relative "../../../crypto/field_helpers"
7
8
 
8
9
  module Llmemory
9
10
  module LongTerm
@@ -13,7 +14,10 @@ module Llmemory
13
14
  # AR auto-deserializes jsonb to a Hash (string keys), which Episode.from_h
14
15
  # handles. Mirrors the file-based ActiveRecordStorage pattern.
15
16
  class ActiveRecordStorage < Base
16
- def initialize
17
+ include Llmemory::Crypto::FieldHelpers
18
+
19
+ def initialize(cipher: nil)
20
+ @cipher = cipher || Llmemory.build_cipher
17
21
  self.class.load_models!
18
22
  end
19
23
 
@@ -30,8 +34,8 @@ module Llmemory
30
34
  data["created_at"] ||= Time.now.utc.iso8601
31
35
  rec = LlmemoryEpisode.find_or_initialize_by(id: id)
32
36
  rec.user_id = user_id
33
- rec.data = data
34
- rec.search_text = searchable_text(data)
37
+ rec.data = cipher.enabled? ? enc_json(data) : data
38
+ rec.search_text = enc(searchable_text(data))
35
39
  rec.created_at ||= Time.current
36
40
  rec.save!
37
41
  id
@@ -39,19 +43,21 @@ module Llmemory
39
43
 
40
44
  def get_episode(user_id, id)
41
45
  rec = LlmemoryEpisode.find_by(user_id: user_id, id: id)
42
- rec&.data
46
+ return nil unless rec
47
+
48
+ decode_data(rec.data)
43
49
  end
44
50
 
45
51
  def list_episodes(user_id, limit: nil, offset: nil)
46
52
  scope = LlmemoryEpisode.where(user_id: user_id, archived_at: nil).order(created_at: :desc)
47
53
  scope = scope.limit(limit) if limit && limit.to_i.positive?
48
54
  scope = scope.offset(offset) if offset && offset.to_i.positive?
49
- scope.map(&:data)
55
+ scope.map { |r| decode_data(r.data) }
50
56
  end
51
57
 
52
58
  def search_episodes(user_id, query)
53
59
  token_scope(LlmemoryEpisode.where(user_id: user_id, archived_at: nil), "search_text", query)
54
- .order(created_at: :desc).map(&:data)
60
+ .order(created_at: :desc).map { |r| decode_data(r.data) }
55
61
  end
56
62
 
57
63
  def count_episodes(user_id)
@@ -96,6 +102,13 @@ module Llmemory
96
102
  end
97
103
  parts.compact.join("\n")
98
104
  end
105
+
106
+ def decode_data(raw)
107
+ return raw.transform_keys(&:to_sym) if raw.is_a?(Hash)
108
+ return dec_json(raw) if raw.is_a?(String) && cipher.encrypted?(raw)
109
+
110
+ raw
111
+ end
99
112
  end
100
113
  end
101
114
  end
@@ -4,6 +4,7 @@ require "json"
4
4
  require "securerandom"
5
5
  require "time"
6
6
  require_relative "base"
7
+ require_relative "../../../crypto/field_helpers"
7
8
 
8
9
  module Llmemory
9
10
  module LongTerm
@@ -13,9 +14,12 @@ module Llmemory
13
14
  # (plus id/user_id/created_at and a denormalized search_text for keyword
14
15
  # search), mirroring the file-based DatabaseStorage pattern.
15
16
  class DatabaseStorage < Base
16
- def initialize(database_url: nil)
17
+ include Llmemory::Crypto::FieldHelpers
18
+
19
+ def initialize(database_url: nil, cipher: nil)
17
20
  @database_url = database_url || Llmemory.configuration.database_url
18
21
  @connection = nil
22
+ @cipher = cipher || Llmemory.build_cipher
19
23
  end
20
24
 
21
25
  def save_episode(user_id, episode)
@@ -23,11 +27,12 @@ module Llmemory
23
27
  id = episode[:id] || episode["id"] || "ep_#{SecureRandom.hex(8)}"
24
28
  data = symbolize(episode).merge(id: id, user_id: user_id)
25
29
  data[:created_at] ||= Time.now.utc.iso8601
30
+ search = searchable_text(data)
26
31
  conn.exec_params(
27
32
  "INSERT INTO llmemory_episodes (id, user_id, data, search_text, created_at) " \
28
33
  "VALUES ($1, $2, $3::jsonb, $4, $5) " \
29
34
  "ON CONFLICT (id) DO UPDATE SET data = $3::jsonb, search_text = $4",
30
- [id, user_id, JSON.generate(data), searchable_text(data), created_at_value(data)]
35
+ [id, user_id, store_data(data), enc(search), created_at_value(data)]
31
36
  )
32
37
  id
33
38
  end
@@ -124,11 +129,28 @@ module Llmemory
124
129
  end
125
130
 
126
131
  def parse_data(value)
127
- JSON.parse(value.to_s, symbolize_names: true)
132
+ if value.is_a?(Hash)
133
+ return value.transform_keys(&:to_sym)
134
+ end
135
+
136
+ str = value.to_s
137
+ if cipher.encrypted?(str)
138
+ cipher.decrypt_json(str)
139
+ else
140
+ JSON.parse(str, symbolize_names: true)
141
+ end
128
142
  rescue JSON::ParserError
129
143
  {}
130
144
  end
131
145
 
146
+ def store_data(data)
147
+ if cipher.enabled?
148
+ JSON.generate(enc_json(data))
149
+ else
150
+ JSON.generate(data)
151
+ end
152
+ end
153
+
132
154
  def symbolize(hash)
133
155
  hash.each_with_object({}) { |(k, v), acc| acc[k.to_sym] = v }
134
156
  end
@@ -4,22 +4,26 @@ require "fileutils"
4
4
  require "json"
5
5
  require "time"
6
6
  require_relative "base"
7
+ require_relative "../../../crypto/field_helpers"
7
8
 
8
9
  module Llmemory
9
10
  module LongTerm
10
11
  module Episodic
11
12
  module Storages
12
13
  class FileStorage < Base
13
- def initialize(base_path: nil)
14
+ include Llmemory::Crypto::FieldHelpers
15
+
16
+ def initialize(base_path: nil, cipher: nil)
14
17
  @base_path = base_path || Llmemory.configuration.long_term_storage_path || "./llmemory_data"
15
18
  @base_path = File.expand_path(@base_path)
19
+ @cipher = cipher || Llmemory.build_cipher
16
20
  end
17
21
 
18
22
  def save_episode(user_id, episode)
19
23
  id = episode[:id] || episode["id"] || "ep_#{next_seq(user_id)}"
20
24
  data = stringify_for_json(episode).merge("id" => id, "user_id" => user_id)
21
25
  data["created_at"] ||= Time.now.iso8601
22
- File.write(episode_path(user_id, id), JSON.generate(data))
26
+ write_episode_file(episode_path(user_id, id), data)
23
27
  id
24
28
  end
25
29
 
@@ -57,10 +61,10 @@ module Llmemory
57
61
  Array(ids).map(&:to_s).count do |id|
58
62
  path = episode_path(user_id, id)
59
63
  next false unless File.file?(path)
60
- data = JSON.parse(File.read(path))
64
+ data = load_episode_raw(path)
61
65
  next false if data["archived_at"]
62
66
  data["archived_at"] = Time.now.iso8601
63
- File.write(path, JSON.generate(data))
67
+ write_episode_file(path, data)
64
68
  true
65
69
  end
66
70
  end
@@ -89,13 +93,26 @@ module Llmemory
89
93
  end
90
94
 
91
95
  def load_episode(path)
92
- data = JSON.parse(File.read(path), symbolize_names: true)
96
+ data = load_episode_raw(path)
97
+ return nil unless data
98
+
93
99
  data[:created_at] = parse_time(data[:created_at])
94
100
  data
95
101
  rescue JSON::ParserError
96
102
  nil
97
103
  end
98
104
 
105
+ def load_episode_raw(path)
106
+ raw = File.read(path)
107
+ json = cipher.enabled? && cipher.encrypted?(raw) ? cipher.decrypt(raw) : raw
108
+ JSON.parse(json, symbolize_names: true)
109
+ end
110
+
111
+ def write_episode_file(path, data)
112
+ payload = JSON.generate(data)
113
+ File.write(path, cipher.enabled? ? cipher.encrypt(payload) : payload)
114
+ end
115
+
99
116
  def episode_text(episode)
100
117
  parts = [episode[:summary], episode[:outcome]]
101
118
  Array(episode[:steps]).each do |s|
@@ -14,17 +14,24 @@ module Llmemory
14
14
  Storage = Storages::MemoryStorage
15
15
 
16
16
  module Storages
17
- def self.build(store: nil, base_path: nil, database_url: nil)
17
+ def self.build(store: nil, base_path: nil, database_url: nil, cipher: nil)
18
+ resolved_cipher = cipher || Llmemory.build_cipher
18
19
  case (store || Llmemory.configuration.long_term_store).to_s.to_sym
19
20
  when :memory
20
21
  MemoryStorage.new
21
22
  when :file
22
- FileStorage.new(base_path: base_path || Llmemory.configuration.long_term_storage_path)
23
+ FileStorage.new(
24
+ base_path: base_path || Llmemory.configuration.long_term_storage_path,
25
+ cipher: resolved_cipher
26
+ )
23
27
  when :postgres, :database
24
- DatabaseStorage.new(database_url: database_url || Llmemory.configuration.database_url)
28
+ DatabaseStorage.new(
29
+ database_url: database_url || Llmemory.configuration.database_url,
30
+ cipher: resolved_cipher
31
+ )
25
32
  when :active_record, :activerecord
26
33
  require_relative "storages/active_record_storage"
27
- ActiveRecordStorage.new
34
+ ActiveRecordStorage.new(cipher: resolved_cipher)
28
35
  else
29
36
  MemoryStorage.new
30
37
  end
@@ -2,13 +2,17 @@
2
2
 
3
3
  require "securerandom"
4
4
  require_relative "base"
5
+ require_relative "../../../crypto/field_helpers"
5
6
 
6
7
  module Llmemory
7
8
  module LongTerm
8
9
  module FileBased
9
10
  module Storages
10
11
  class ActiveRecordStorage < Base
11
- def initialize
12
+ include Llmemory::Crypto::FieldHelpers
13
+
14
+ def initialize(cipher: nil)
15
+ @cipher = cipher || Llmemory.build_cipher
12
16
  self.class.load_models!
13
17
  end
14
18
 
@@ -24,7 +28,7 @@ module Llmemory
24
28
  LlmemoryResource.create!(
25
29
  id: id,
26
30
  user_id: user_id,
27
- text: text,
31
+ text: enc(text),
28
32
  created_at: Time.current
29
33
  )
30
34
  id
@@ -36,24 +40,26 @@ module Llmemory
36
40
  id: id,
37
41
  user_id: user_id,
38
42
  category: category,
39
- content: content,
43
+ content: enc(content),
40
44
  source_resource_id: source_resource_id,
41
45
  created_at: Time.current
42
46
  }
43
47
  attrs[:importance] = importance if LlmemoryItem.column_names.include?("importance")
44
- attrs[:provenance] = provenance if provenance && LlmemoryItem.column_names.include?("provenance")
48
+ if provenance && LlmemoryItem.column_names.include?("provenance")
49
+ attrs[:provenance] = cipher.enabled? ? enc_json(provenance) : provenance
50
+ end
45
51
  LlmemoryItem.create!(attrs)
46
52
  id
47
53
  end
48
54
 
49
55
  def load_category(user_id, category_name)
50
56
  rec = LlmemoryCategory.find_by(user_id: user_id, category_name: category_name)
51
- rec ? rec.content.to_s : ""
57
+ rec ? dec(rec.content.to_s) : ""
52
58
  end
53
59
 
54
60
  def save_category(user_id, category_name, content)
55
61
  rec = LlmemoryCategory.find_or_initialize_by(user_id: user_id, category_name: category_name)
56
- rec.content = content
62
+ rec.content = enc(content)
57
63
  rec.updated_at = Time.current
58
64
  rec.save!
59
65
  true
@@ -101,7 +107,7 @@ module Llmemory
101
107
  id: "item_#{SecureRandom.hex(8)}",
102
108
  user_id: user_id,
103
109
  category: merged_item[:category],
104
- content: merged_item[:content],
110
+ content: enc(merged_item[:content]),
105
111
  source_resource_id: merged_item[:source_resource_id],
106
112
  created_at: created_at
107
113
  }
@@ -194,19 +200,19 @@ module Llmemory
194
200
  h = {
195
201
  id: r.id,
196
202
  category: r.category,
197
- content: r.content,
203
+ content: dec(r.content),
198
204
  source_resource_id: r.source_resource_id,
199
205
  created_at: r.created_at
200
206
  }
201
207
  h[:importance] = r.respond_to?(:importance) ? (r.importance || 0.7).to_f : 0.7
202
- h[:provenance] = r.provenance if r.respond_to?(:provenance)
208
+ h[:provenance] = parse_provenance(r.provenance) if r.respond_to?(:provenance)
203
209
  h
204
210
  end
205
211
 
206
212
  def row_to_resource(r)
207
213
  {
208
214
  id: r.id,
209
- text: r.text,
215
+ text: dec(r.text),
210
216
  created_at: r.created_at
211
217
  }
212
218
  end