llmemory 0.2.2 → 0.2.3

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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +31 -0
  3. data/lib/llmemory/configuration.rb +22 -2
  4. data/lib/llmemory/crypto/cipher.rb +147 -0
  5. data/lib/llmemory/crypto/field_helpers.rb +110 -0
  6. data/lib/llmemory/llm/anthropic.rb +2 -1
  7. data/lib/llmemory/llm/openai.rb +2 -1
  8. data/lib/llmemory/long_term/episodic/memory.rb +4 -3
  9. data/lib/llmemory/long_term/episodic/storage.rb +11 -4
  10. data/lib/llmemory/long_term/episodic/storages/active_record_storage.rb +19 -6
  11. data/lib/llmemory/long_term/episodic/storages/database_storage.rb +25 -3
  12. data/lib/llmemory/long_term/episodic/storages/file_storage.rb +22 -5
  13. data/lib/llmemory/long_term/file_based/storage.rb +11 -4
  14. data/lib/llmemory/long_term/file_based/storages/active_record_storage.rb +16 -10
  15. data/lib/llmemory/long_term/file_based/storages/database_storage.rb +24 -8
  16. data/lib/llmemory/long_term/file_based/storages/file_storage.rb +28 -14
  17. data/lib/llmemory/long_term/graph_based/memory.rb +4 -3
  18. data/lib/llmemory/long_term/graph_based/storage.rb +3 -2
  19. data/lib/llmemory/long_term/graph_based/storages/active_record_storage.rb +47 -21
  20. data/lib/llmemory/long_term/procedural/memory.rb +4 -3
  21. data/lib/llmemory/long_term/procedural/storage.rb +11 -4
  22. data/lib/llmemory/long_term/procedural/storages/active_record_storage.rb +33 -13
  23. data/lib/llmemory/long_term/procedural/storages/database_storage.rb +25 -4
  24. data/lib/llmemory/long_term/procedural/storages/file_storage.rb +23 -6
  25. data/lib/llmemory/memory.rb +40 -8
  26. data/lib/llmemory/short_term/checkpoint.rb +3 -2
  27. data/lib/llmemory/short_term/stores/active_record_store.rb +12 -10
  28. data/lib/llmemory/short_term/stores/memory_store.rb +1 -1
  29. data/lib/llmemory/short_term/stores/postgres_store.rb +11 -5
  30. data/lib/llmemory/short_term/stores/redis_store.rb +7 -5
  31. data/lib/llmemory/short_term/stores.rb +7 -6
  32. data/lib/llmemory/vector_store/active_record_store.rb +24 -3
  33. data/lib/llmemory/vector_store/memory_store.rb +23 -3
  34. data/lib/llmemory/vector_store.rb +4 -3
  35. data/lib/llmemory/version.rb +1 -1
  36. data/lib/llmemory.rb +2 -0
  37. metadata +3 -1
@@ -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), mirroring
14
15
  # 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_skill(user_id, skill)
@@ -27,7 +31,7 @@ module Llmemory
27
31
  "INSERT INTO llmemory_skills (id, user_id, data, search_text, created_at) " \
28
32
  "VALUES ($1, $2, $3::jsonb, $4, $5) " \
29
33
  "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)]
34
+ [id, user_id, store_data(data), enc(searchable_text(data)), created_at_value(data)]
31
35
  )
32
36
  id
33
37
  end
@@ -72,7 +76,7 @@ module Llmemory
72
76
  data[:updated_at] = Time.now.utc.iso8601
73
77
  conn.exec_params(
74
78
  "UPDATE llmemory_skills SET data = $3::jsonb, search_text = $4 WHERE user_id = $1 AND id = $2",
75
- [user_id, skill_id, JSON.generate(data), searchable_text(data)]
79
+ [user_id, skill_id, store_data(data), enc(searchable_text(data))]
76
80
  )
77
81
  data
78
82
  end
@@ -144,11 +148,28 @@ module Llmemory
144
148
  end
145
149
 
146
150
  def parse_data(value)
147
- JSON.parse(value.to_s, symbolize_names: true)
151
+ if value.is_a?(Hash)
152
+ return value.transform_keys(&:to_sym)
153
+ end
154
+
155
+ str = value.to_s
156
+ if cipher.encrypted?(str)
157
+ cipher.decrypt_json(str)
158
+ else
159
+ JSON.parse(str, symbolize_names: true)
160
+ end
148
161
  rescue JSON::ParserError
149
162
  {}
150
163
  end
151
164
 
165
+ def store_data(data)
166
+ if cipher.enabled?
167
+ JSON.generate(enc_json(data))
168
+ else
169
+ JSON.generate(data)
170
+ end
171
+ end
172
+
152
173
  def symbolize(hash)
153
174
  hash.each_with_object({}) { |(k, v), acc| acc[k.to_sym] = v }
154
175
  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 Procedural
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_skill(user_id, skill)
19
23
  id = skill[:id] || skill["id"] || "skill_#{next_seq(user_id)}"
20
24
  data = stringify_for_json(skill).merge("id" => id, "user_id" => user_id)
21
25
  data["created_at"] ||= Time.now.iso8601(6)
22
- File.write(skill_path(user_id, id), JSON.generate(data))
26
+ write_skill_file(skill_path(user_id, id), data)
23
27
  id
24
28
  end
25
29
 
@@ -50,7 +54,7 @@ module Llmemory
50
54
  key = success ? :success_count : :failure_count
51
55
  skill[key] = (skill[key] || 0).to_i + 1
52
56
  skill[:updated_at] = Time.now.iso8601(6)
53
- File.write(skill_path(user_id, skill_id), JSON.generate(stringify_for_json(skill)))
57
+ write_skill_file(skill_path(user_id, skill_id), stringify_for_json(skill))
54
58
  skill
55
59
  end
56
60
 
@@ -71,10 +75,10 @@ module Llmemory
71
75
  Array(ids).map(&:to_s).count do |id|
72
76
  path = skill_path(user_id, id)
73
77
  next false unless File.file?(path)
74
- data = JSON.parse(File.read(path))
78
+ data = load_skill_raw(path)
75
79
  next false if data["archived_at"]
76
80
  data["archived_at"] = Time.now.iso8601
77
- File.write(path, JSON.generate(data))
81
+ write_skill_file(path, data)
78
82
  true
79
83
  end
80
84
  end
@@ -103,7 +107,9 @@ module Llmemory
103
107
  end
104
108
 
105
109
  def load_skill(path)
106
- data = JSON.parse(File.read(path), symbolize_names: true)
110
+ data = load_skill_raw(path)
111
+ return nil unless data
112
+
107
113
  data[:created_at] = parse_time(data[:created_at])
108
114
  data[:updated_at] = parse_time(data[:updated_at]) if data[:updated_at]
109
115
  data
@@ -111,6 +117,17 @@ module Llmemory
111
117
  nil
112
118
  end
113
119
 
120
+ def load_skill_raw(path)
121
+ raw = File.read(path)
122
+ json = cipher.enabled? && cipher.encrypted?(raw) ? cipher.decrypt(raw) : raw
123
+ JSON.parse(json, symbolize_names: true)
124
+ end
125
+
126
+ def write_skill_file(path, data)
127
+ payload = JSON.generate(data)
128
+ File.write(path, cipher.enabled? ? cipher.encrypt(payload) : payload)
129
+ end
130
+
114
131
  def skill_text(skill)
115
132
  [skill[:name], skill[:description], skill[:body]].compact.join("\n")
116
133
  end
@@ -10,34 +10,57 @@ module Llmemory
10
10
  DEFAULT_SESSION_ID = "default"
11
11
  STATE_KEY_MESSAGES = :messages
12
12
 
13
- def initialize(user_id:, session_id: DEFAULT_SESSION_ID, checkpoint: nil, long_term: nil, long_term_type: nil, retrieval_engine: nil, working_memory: nil, episodic: nil, procedural: nil, api_key: nil)
13
+ def initialize(user_id:, session_id: DEFAULT_SESSION_ID, checkpoint: nil, long_term: nil, long_term_type: nil, retrieval_engine: nil, working_memory: nil, episodic: nil, procedural: nil, api_key: nil, encryption_key: :inherit)
14
14
  @user_id = user_id
15
15
  @session_id = session_id
16
- @checkpoint = checkpoint || ShortTerm::Checkpoint.new(user_id: user_id, session_id: session_id)
16
+ resolved_key = encryption_key == :inherit ? nil : encryption_key
17
+ @cipher = Llmemory.build_cipher(resolved_key)
18
+ @checkpoint = checkpoint || ShortTerm::Checkpoint.new(
19
+ user_id: user_id,
20
+ session_id: session_id,
21
+ cipher: @cipher
22
+ )
17
23
  @working_memory = working_memory
18
24
  @episodic = episodic
19
25
  @procedural = procedural
20
26
  @llm = api_key.to_s.empty? ? nil : Llmemory::LLM.client(api_key: api_key)
21
27
  type = long_term_type || Llmemory.configuration.long_term_type || :file_based
22
28
  @long_term = long_term || build_long_term(type)
23
- @retrieval_engine = retrieval_engine || Retrieval::Engine.new(@long_term, llm: @llm)
29
+ short_term_store = build_short_term_store(@cipher)
30
+ @retrieval_engine = retrieval_engine || Retrieval::Engine.new(
31
+ @long_term,
32
+ llm: @llm,
33
+ feedback: Retrieval::FeedbackStore.new(store: short_term_store)
34
+ )
24
35
  end
25
36
 
26
37
  # Structured working memory for this session (CoALA working memory),
27
38
  # parallel to the message checkpoint. Lazily built.
28
39
  def working_memory
29
- @working_memory ||= WorkingMemory.new(user_id: @user_id, session_id: @session_id)
40
+ @working_memory ||= WorkingMemory.new(
41
+ user_id: @user_id,
42
+ session_id: @session_id,
43
+ store: build_short_term_store(@cipher)
44
+ )
30
45
  end
31
46
 
32
47
  # Episodic long-term memory (CoALA): records and retrieves agent trajectories.
33
48
  # Additive — coexists with the semantic store (file/graph). Lazily built.
34
49
  def episodic
35
- @episodic ||= LongTerm::Episodic::Memory.new(user_id: @user_id)
50
+ @episodic ||= LongTerm::Episodic::Memory.new(
51
+ user_id: @user_id,
52
+ storage: LongTerm::Episodic::Storages.build(cipher: @cipher),
53
+ cipher: @cipher
54
+ )
36
55
  end
37
56
 
38
57
  # Procedural long-term memory (Voyager-style skill library). Lazily built.
39
58
  def procedural
40
- @procedural ||= LongTerm::Procedural::Memory.new(user_id: @user_id)
59
+ @procedural ||= LongTerm::Procedural::Memory.new(
60
+ user_id: @user_id,
61
+ storage: LongTerm::Procedural::Storages.build(cipher: @cipher),
62
+ cipher: @cipher
63
+ )
41
64
  end
42
65
 
43
66
  # Reflects over recent episodes and writes distilled insights to the
@@ -321,14 +344,23 @@ module Llmemory
321
344
  when :graph_based
322
345
  LongTerm::GraphBased::Memory.new(
323
346
  user_id: @user_id,
324
- storage: LongTerm::GraphBased::Storages.build,
347
+ storage: LongTerm::GraphBased::Storages.build(cipher: @cipher),
348
+ cipher: @cipher,
325
349
  **llm_opts
326
350
  )
327
351
  else
328
- LongTerm::FileBased::Memory.new(user_id: @user_id, storage: LongTerm::FileBased::Storages.build, **llm_opts)
352
+ LongTerm::FileBased::Memory.new(
353
+ user_id: @user_id,
354
+ storage: LongTerm::FileBased::Storages.build(cipher: @cipher),
355
+ **llm_opts
356
+ )
329
357
  end
330
358
  end
331
359
 
360
+ def build_short_term_store(cipher)
361
+ ShortTerm::Stores.build(cipher: cipher)
362
+ end
363
+
332
364
  def save_state(messages:, last_flush_at: nil, last_compact_at: nil)
333
365
  state = { STATE_KEY_MESSAGES => messages, last_activity_at: Time.now }
334
366
  state[:last_flush_at] = last_flush_at if last_flush_at
@@ -7,9 +7,10 @@ module Llmemory
7
7
  class Checkpoint
8
8
  DEFAULT_SESSION_ID = "default"
9
9
 
10
- def initialize(user_id:, session_id: DEFAULT_SESSION_ID, store: nil)
10
+ def initialize(user_id:, session_id: DEFAULT_SESSION_ID, store: nil, cipher: nil)
11
11
  @user_id = user_id
12
12
  @session_id = session_id
13
+ @cipher = cipher
13
14
  @store = store || build_store
14
15
  end
15
16
 
@@ -28,7 +29,7 @@ module Llmemory
28
29
  private
29
30
 
30
31
  def build_store
31
- Stores.build
32
+ Stores.build(cipher: @cipher)
32
33
  end
33
34
  end
34
35
  end
@@ -1,12 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "base"
4
+ require_relative "../../crypto/field_helpers"
4
5
 
5
6
  module Llmemory
6
7
  module ShortTerm
7
8
  module Stores
8
9
  class ActiveRecordStore < Base
9
- def initialize
10
+ include Llmemory::Crypto::FieldHelpers
11
+
12
+ def initialize(cipher: nil)
13
+ @cipher = cipher || Llmemory.build_cipher
10
14
  self.class.load_model!
11
15
  end
12
16
 
@@ -22,7 +26,7 @@ module Llmemory
22
26
  user_id: user_id,
23
27
  session_id: session_id
24
28
  )
25
- record.state = state
29
+ record.state = cipher.enabled? ? serialize_state(state) : state
26
30
  record.updated_at = Time.current
27
31
  record.save!
28
32
  true
@@ -34,8 +38,13 @@ module Llmemory
34
38
  session_id: session_id
35
39
  )
36
40
  return nil unless record
41
+
37
42
  raw = record.state
38
- raw.is_a?(Hash) ? raw.transform_keys(&:to_sym) : deserialize(raw)
43
+ if raw.is_a?(Hash)
44
+ raw.transform_keys(&:to_sym)
45
+ else
46
+ deserialize_state(raw)
47
+ end
39
48
  end
40
49
 
41
50
  def delete(user_id, session_id)
@@ -53,13 +62,6 @@ module Llmemory
53
62
  def list_sessions(user_id:)
54
63
  Llmemory::ShortTerm::Stores::ActiveRecordCheckpoint.where(user_id: user_id).pluck(:session_id)
55
64
  end
56
-
57
- private
58
-
59
- def deserialize(data)
60
- return data if data.is_a?(Hash)
61
- JSON.parse(data.to_s, symbolize_names: true)
62
- end
63
65
  end
64
66
  end
65
67
  end
@@ -6,7 +6,7 @@ module Llmemory
6
6
  module ShortTerm
7
7
  module Stores
8
8
  class MemoryStore < Base
9
- def initialize
9
+ def initialize(cipher: nil)
10
10
  @store = {}
11
11
  end
12
12
 
@@ -1,14 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "base"
4
+ require_relative "../../crypto/field_helpers"
4
5
 
5
6
  module Llmemory
6
7
  module ShortTerm
7
8
  module Stores
8
9
  class PostgresStore < Base
9
- def initialize(database_url: nil)
10
+ include Llmemory::Crypto::FieldHelpers
11
+
12
+ def initialize(database_url: nil, cipher: nil)
10
13
  @database_url = database_url || Llmemory.configuration.database_url
11
14
  @connection = nil
15
+ @cipher = cipher || Llmemory.build_cipher
12
16
  end
13
17
 
14
18
  def save(user_id, session_id, state)
@@ -81,13 +85,15 @@ module Llmemory
81
85
  end
82
86
 
83
87
  def serialize(state)
84
- require "json"
85
- JSON.generate(state)
88
+ payload = serialize_state(state)
89
+ cipher.enabled? ? JSON.generate(payload) : payload
86
90
  end
87
91
 
88
92
  def deserialize(data)
89
- require "json"
90
- JSON.parse(data, symbolize_names: true)
93
+ if data.is_a?(String) && !cipher.encrypted?(data)
94
+ data = JSON.parse(data)
95
+ end
96
+ deserialize_state(data)
91
97
  end
92
98
  end
93
99
  end
@@ -1,14 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "base"
4
+ require_relative "../../crypto/field_helpers"
4
5
 
5
6
  module Llmemory
6
7
  module ShortTerm
7
8
  module Stores
8
9
  class RedisStore < Base
9
- def initialize(redis_url: nil)
10
+ include Llmemory::Crypto::FieldHelpers
11
+
12
+ def initialize(redis_url: nil, cipher: nil)
10
13
  @redis_url = redis_url || Llmemory.configuration.redis_url
11
14
  @redis = nil
15
+ @cipher = cipher || Llmemory.build_cipher
12
16
  end
13
17
 
14
18
  def save(user_id, session_id, state)
@@ -50,13 +54,11 @@ module Llmemory
50
54
  end
51
55
 
52
56
  def serialize(state)
53
- require "json"
54
- JSON.generate(state)
57
+ serialize_state(state)
55
58
  end
56
59
 
57
60
  def deserialize(data)
58
- require "json"
59
- JSON.parse(data, symbolize_names: true)
61
+ deserialize_state(data)
60
62
  end
61
63
  end
62
64
  end
@@ -10,16 +10,17 @@ module Llmemory
10
10
  module Stores
11
11
  # Single source of truth for selecting a short-term store backend.
12
12
  # Shared by Checkpoint, SessionLifecycle and WorkingMemory.
13
- def self.build(store_type = nil)
13
+ def self.build(store_type = nil, cipher: nil)
14
+ resolved_cipher = cipher || Llmemory.build_cipher
14
15
  case (store_type || Llmemory.configuration.short_term_store).to_sym
15
- when :memory then MemoryStore.new
16
- when :redis then RedisStore.new
17
- when :postgres then PostgresStore.new
16
+ when :memory then MemoryStore.new(cipher: resolved_cipher)
17
+ when :redis then RedisStore.new(cipher: resolved_cipher)
18
+ when :postgres then PostgresStore.new(cipher: resolved_cipher)
18
19
  when :active_record, :activerecord
19
20
  require_relative "stores/active_record_store"
20
- ActiveRecordStore.new
21
+ ActiveRecordStore.new(cipher: resolved_cipher)
21
22
  else
22
- MemoryStore.new
23
+ MemoryStore.new(cipher: resolved_cipher)
23
24
  end
24
25
  end
25
26
  end
@@ -7,10 +7,11 @@ module Llmemory
7
7
  # Persists embeddings in llmemory_embeddings (pgvector).
8
8
  # Use when long_term_store is :active_record so hybrid search finds persisted embeddings.
9
9
  class ActiveRecordStore < Base
10
- def initialize(embedding_provider: nil, source_type: "edge")
10
+ def initialize(embedding_provider: nil, source_type: "edge", cipher: nil)
11
11
  self.class.load_model!
12
12
  @embedding_provider = embedding_provider
13
13
  @source_type = source_type.to_s
14
+ @cipher = cipher || Llmemory.build_cipher
14
15
  end
15
16
 
16
17
  def self.load_model!
@@ -34,7 +35,7 @@ module Llmemory
34
35
  source_id: id.to_s
35
36
  )
36
37
  rec.embedding = embedding.to_a.map(&:to_f)
37
- rec.text_content = text_content
38
+ rec.text_content = encrypt_text_content(text_content)
38
39
  rec.save!
39
40
  id
40
41
  end
@@ -58,7 +59,11 @@ module Llmemory
58
59
  {
59
60
  id: r.source_id,
60
61
  score: score,
61
- metadata: { "text" => r.text_content, "created_at" => r.created_at, "user_id" => r.user_id }
62
+ metadata: {
63
+ "text" => decrypt_text_content(r.text_content),
64
+ "created_at" => r.created_at,
65
+ "user_id" => r.user_id
66
+ }
62
67
  }
63
68
  end
64
69
  end
@@ -69,6 +74,22 @@ module Llmemory
69
74
  query_embedding = @embedding_provider.embed(query_text)
70
75
  search(query_embedding, top_k: top_k, user_id: user_id)
71
76
  end
77
+
78
+ private
79
+
80
+ def encrypt_text_content(text)
81
+ return text if text.nil? || text.to_s.empty?
82
+ return text unless @cipher.enabled?
83
+
84
+ @cipher.encrypt(text.to_s)
85
+ end
86
+
87
+ def decrypt_text_content(text)
88
+ return text if text.nil?
89
+ return text unless text.is_a?(String) && @cipher.enabled? && @cipher.encrypted?(text)
90
+
91
+ @cipher.decrypt(text)
92
+ end
72
93
  end
73
94
  end
74
95
  end
@@ -5,9 +5,10 @@ require_relative "base"
5
5
  module Llmemory
6
6
  module VectorStore
7
7
  class MemoryStore < Base
8
- def initialize(embedding_provider: nil)
8
+ def initialize(embedding_provider: nil, cipher: nil)
9
9
  @entries = {}
10
10
  @embedding_provider = embedding_provider
11
+ @cipher = cipher || Llmemory.build_cipher
11
12
  end
12
13
 
13
14
  def embed(text)
@@ -17,7 +18,13 @@ module Llmemory
17
18
 
18
19
  def store(id:, embedding:, metadata: {}, user_id: nil)
19
20
  key = user_id ? "#{user_id}:#{id}" : id.to_s
20
- @entries[key] = { embedding: embedding.to_a.map(&:to_f), metadata: (metadata || {}).merge("user_id" => user_id) }
21
+ meta = (metadata || {}).dup
22
+ if meta["text"] && @cipher.enabled?
23
+ meta["text"] = @cipher.encrypt(meta["text"].to_s)
24
+ elsif meta[:text] && @cipher.enabled?
25
+ meta[:text] = @cipher.encrypt(meta[:text].to_s)
26
+ end
27
+ @entries[key] = { embedding: embedding.to_a.map(&:to_f), metadata: meta.merge("user_id" => user_id) }
21
28
  id
22
29
  end
23
30
 
@@ -27,7 +34,7 @@ module Llmemory
27
34
  entries = user_id ? @entries.select { |k, _| k.to_s.start_with?("#{user_id}:") } : @entries
28
35
  scores = entries.map do |id, data|
29
36
  sim = cosine_similarity(query, data[:embedding])
30
- { id: id, score: sim, metadata: data[:metadata] }
37
+ { id: id, score: sim, metadata: decrypt_metadata(data[:metadata]) }
31
38
  end
32
39
  scores.sort_by { |s| -s[:score] }.first(top_k)
33
40
  end
@@ -40,6 +47,19 @@ module Llmemory
40
47
 
41
48
  private
42
49
 
50
+ def decrypt_metadata(meta)
51
+ return meta unless meta.is_a?(Hash) && @cipher.enabled?
52
+
53
+ out = meta.dup
54
+ text = out["text"] || out[:text]
55
+ if text.is_a?(String) && @cipher.encrypted?(text)
56
+ decrypted = @cipher.decrypt(text)
57
+ out["text"] = decrypted
58
+ out[:text] = decrypted
59
+ end
60
+ out
61
+ end
62
+
43
63
  def cosine_similarity(a, b)
44
64
  return 0.0 if a.size != b.size || a.empty?
45
65
  dot = a.zip(b).sum { |x, y| x * y }
@@ -10,14 +10,15 @@ module Llmemory
10
10
  # from config (:active_record persists in llmemory_embeddings; otherwise
11
11
  # in-process). `source_type` namespaces persisted embeddings so different
12
12
  # memory types (edges, episodes, skills) never collide in the shared table.
13
- def self.build(source_type: "edge")
13
+ def self.build(source_type: "edge", cipher: nil)
14
+ resolved_cipher = cipher || Llmemory.build_cipher
14
15
  embeddings = OpenAIEmbeddings.new
15
16
  store_type = (Llmemory.configuration.long_term_store || :memory).to_s.to_sym
16
17
  if store_type == :active_record || store_type == :activerecord
17
18
  require_relative "vector_store/active_record_store"
18
- ActiveRecordStore.new(embedding_provider: embeddings, source_type: source_type)
19
+ ActiveRecordStore.new(embedding_provider: embeddings, source_type: source_type, cipher: resolved_cipher)
19
20
  else
20
- MemoryStore.new(embedding_provider: embeddings)
21
+ MemoryStore.new(embedding_provider: embeddings, cipher: resolved_cipher)
21
22
  end
22
23
  end
23
24
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Llmemory
4
- VERSION = "0.2.2"
4
+ VERSION = "0.2.3"
5
5
  end
data/lib/llmemory.rb CHANGED
@@ -26,3 +26,5 @@ module Llmemory
26
26
  class StoreError < Error; end
27
27
  class LLMError < Error; end
28
28
  end
29
+
30
+ require_relative "llmemory/crypto/cipher"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: llmemory
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.2.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - llmemory
@@ -162,6 +162,8 @@ files:
162
162
  - lib/llmemory/cli/commands/users.rb
163
163
  - lib/llmemory/cli/commands/working.rb
164
164
  - lib/llmemory/configuration.rb
165
+ - lib/llmemory/crypto/cipher.rb
166
+ - lib/llmemory/crypto/field_helpers.rb
165
167
  - lib/llmemory/dashboard.rb
166
168
  - lib/llmemory/dashboard/engine.rb
167
169
  - lib/llmemory/extractors.rb