llmemory 0.2.6 → 0.2.7

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 (55) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +22 -5
  3. data/app/controllers/llmemory/dashboard/application_controller.rb +12 -0
  4. data/app/views/llmemory/dashboard/short_term/show.html.erb +1 -1
  5. data/lib/llmemory/active_record_helpers.rb +21 -0
  6. data/lib/llmemory/cli/commands/mcp.rb +5 -3
  7. data/lib/llmemory/configuration.rb +41 -6
  8. data/lib/llmemory/crypto/field_helpers.rb +5 -0
  9. data/lib/llmemory/extractors/entity_relation_extractor.rb +9 -2
  10. data/lib/llmemory/extractors/fact_extractor.rb +67 -1
  11. data/lib/llmemory/forget_log.rb +7 -3
  12. data/lib/llmemory/llm/anthropic.rb +4 -6
  13. data/lib/llmemory/llm/http_client.rb +55 -0
  14. data/lib/llmemory/llm/openai.rb +5 -7
  15. data/lib/llmemory/llm/tracking_client.rb +22 -12
  16. data/lib/llmemory/llm/usage.rb +4 -0
  17. data/lib/llmemory/llm/usage_ledger.rb +25 -24
  18. data/lib/llmemory/long_term/episodic/memory.rb +5 -2
  19. data/lib/llmemory/long_term/episodic/storages/active_record_storage.rb +3 -1
  20. data/lib/llmemory/long_term/file_based/memory.rb +9 -4
  21. data/lib/llmemory/long_term/file_based/storage.rb +15 -2
  22. data/lib/llmemory/long_term/file_based/storages/active_record_storage.rb +3 -1
  23. data/lib/llmemory/long_term/graph_based/memory.rb +39 -5
  24. data/lib/llmemory/long_term/graph_based/storage.rb +15 -2
  25. data/lib/llmemory/long_term/procedural/memory.rb +5 -2
  26. data/lib/llmemory/long_term/procedural/storages/active_record_storage.rb +3 -1
  27. data/lib/llmemory/maintenance/consolidator.rb +6 -2
  28. data/lib/llmemory/mcp/authentication.rb +5 -3
  29. data/lib/llmemory/mcp/server.rb +21 -6
  30. data/lib/llmemory/mcp/store_helpers.rb +35 -0
  31. data/lib/llmemory/mcp/tools/memory_save.rb +15 -18
  32. data/lib/llmemory/mcp/tools/memory_search.rb +16 -19
  33. data/lib/llmemory/memory.rb +29 -11
  34. data/lib/llmemory/memory_module.rb +6 -2
  35. data/lib/llmemory/provenance.rb +23 -1
  36. data/lib/llmemory/retrieval/feedback_store.rb +11 -5
  37. data/lib/llmemory/retrieval/multi_source.rb +26 -0
  38. data/lib/llmemory/retrieval/temporal_ranker.rb +4 -2
  39. data/lib/llmemory/retrieval.rb +1 -0
  40. data/lib/llmemory/short_term/pruner.rb +16 -4
  41. data/lib/llmemory/short_term/session_lifecycle.rb +18 -9
  42. data/lib/llmemory/short_term/stores/active_record_store.rb +37 -8
  43. data/lib/llmemory/short_term/stores/base.rb +9 -0
  44. data/lib/llmemory/short_term/stores/key_codec.rb +33 -0
  45. data/lib/llmemory/short_term/stores/memory_store.rb +38 -7
  46. data/lib/llmemory/short_term/stores/postgres_store.rb +81 -33
  47. data/lib/llmemory/short_term/stores/redis_store.rb +85 -9
  48. data/lib/llmemory/short_term/stores.rb +17 -2
  49. data/lib/llmemory/vector_store/active_record_store.rb +23 -8
  50. data/lib/llmemory/vector_store/base.rb +4 -0
  51. data/lib/llmemory/vector_store/memory_store.rb +9 -0
  52. data/lib/llmemory/vector_store/openai_embeddings.rb +6 -7
  53. data/lib/llmemory/version.rb +1 -1
  54. data/lib/llmemory/working_memory.rb +10 -7
  55. metadata +6 -15
@@ -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, cipher: nil)
24
+ def initialize(user_id:, storage: nil, vector_store: nil, cipher: nil, forget_log_store: nil)
25
25
  @user_id = user_id
26
26
  @cipher = cipher || Llmemory.build_cipher
27
+ @forget_log_store = forget_log_store
27
28
  @storage = storage || Storages.build(cipher: @cipher)
28
29
  @vector_store = vector_store
29
30
  @vector_explicit = !vector_store.nil?
@@ -71,11 +72,13 @@ module Llmemory
71
72
  # recency (temporal decay) and importance (P3), with provenance (P10).
72
73
  # Hybrid (vector + keyword) when a vector store is active; otherwise
73
74
  # keyword-only.
75
+ KEYWORD_MATCH_SCORE = 0.6
76
+
74
77
  def search_candidates(query, user_id: nil, top_k: 20)
75
78
  uid = user_id || @user_id
76
79
  return [] unless uid == @user_id
77
80
 
78
- keyword = @storage.search_episodes(uid, query).first(top_k).map { |e| candidate_for(e, 1.0) }
81
+ keyword = @storage.search_episodes(uid, query).first(top_k).map { |e| candidate_for(e, KEYWORD_MATCH_SCORE) }
79
82
  vs = vector_store
80
83
  return keyword unless vs
81
84
 
@@ -5,6 +5,7 @@ require "securerandom"
5
5
  require "time"
6
6
  require_relative "base"
7
7
  require_relative "../../../crypto/field_helpers"
8
+ require_relative "../../../active_record_helpers"
8
9
 
9
10
  module Llmemory
10
11
  module LongTerm
@@ -15,6 +16,7 @@ module Llmemory
15
16
  # handles. Mirrors the file-based ActiveRecordStorage pattern.
16
17
  class ActiveRecordStorage < Base
17
18
  include Llmemory::Crypto::FieldHelpers
19
+ include Llmemory::ActiveRecordHelpers
18
20
 
19
21
  def initialize(cipher: nil)
20
22
  @cipher = cipher || Llmemory.build_cipher
@@ -39,7 +41,7 @@ module Llmemory
39
41
  rec.search_text = enc(text)
40
42
  rec.search_tokens = search_tokens_for(text) if LlmemoryEpisode.column_names.include?("search_tokens")
41
43
  rec.created_at ||= Time.current
42
- rec.save!
44
+ with_unique_retry { rec.save! }
43
45
  id
44
46
  end
45
47
 
@@ -13,9 +13,10 @@ module Llmemory
13
13
  class Memory
14
14
  include Llmemory::MemoryModule
15
15
 
16
- def initialize(user_id:, storage: nil, llm: nil, extractor: nil)
16
+ def initialize(user_id:, storage: nil, llm: nil, extractor: nil, forget_log_store: nil)
17
17
  @user_id = user_id
18
18
  @storage = storage || Storages.build
19
+ @forget_log_store = forget_log_store
19
20
  @llm = llm || Llmemory::LLM.client
20
21
  @extractor = extractor || Llmemory::Extractors::FactExtractor.new(llm: @llm)
21
22
  end
@@ -27,12 +28,16 @@ module Llmemory
27
28
  resource_id = save_resource(text)
28
29
  append_to_daily_log(text) if Llmemory.configuration.daily_logs_enabled && @storage.respond_to?(:save_daily_log_entry)
29
30
  items = @extractor.extract_items(text)
31
+ contents = items.map do |item|
32
+ item.is_a?(Hash) ? (item["content"] || item[:content]).to_s : item.to_s
33
+ end
34
+ classifications = @extractor.classify_items(contents)
30
35
  updates_by_category = {}
31
36
 
32
37
  items.each do |item|
33
38
  content = item.is_a?(Hash) ? (item["content"] || item[:content]) : item.to_s
34
39
  importance = (item["importance"] || item[:importance] || 0.7).to_f
35
- cat = @extractor.classify_item(content)
40
+ cat = classifications[content] || @extractor.classify_item(content)
36
41
  updates_by_category[cat] ||= []
37
42
  updates_by_category[cat] << content.to_s
38
43
  save_item(category: cat, item: item, source_resource_id: resource_id, importance: importance)
@@ -48,8 +53,8 @@ module Llmemory
48
53
  end
49
54
 
50
55
  def retrieve(query)
51
- retrieval = Retrieval.new(user_id: @user_id, storage: @storage, llm: @llm)
52
- retrieval.retrieve(query)
56
+ candidates = search_candidates(query, top_k: 20)
57
+ Llmemory::Retrieval::ContextAssembler.new.assemble(candidates)
53
58
  end
54
59
 
55
60
  def search_candidates(query, user_id: nil, top_k: 20)
@@ -16,9 +16,14 @@ module Llmemory
16
16
  module Storages
17
17
  def self.build(store: nil, base_path: nil, database_url: nil, cipher: nil)
18
18
  resolved_cipher = cipher || Llmemory.build_cipher
19
- case (store || Llmemory.configuration.long_term_store).to_s.to_sym
19
+ store_type = (store || Llmemory.configuration.long_term_store).to_s.to_sym
20
+ case store_type
20
21
  when :memory
21
- MemoryStorage.new
22
+ if Llmemory.configuration.shared_memory_stores
23
+ shared_memory_storage
24
+ else
25
+ MemoryStorage.new
26
+ end
22
27
  when :file
23
28
  FileStorage.new(
24
29
  base_path: base_path || Llmemory.configuration.long_term_storage_path,
@@ -36,6 +41,14 @@ module Llmemory
36
41
  MemoryStorage.new
37
42
  end
38
43
  end
44
+
45
+ def self.shared_memory_storage
46
+ @shared_memory_storage ||= MemoryStorage.new
47
+ end
48
+
49
+ def self.reset_shared_singletons!
50
+ @shared_memory_storage = nil
51
+ end
39
52
  end
40
53
  end
41
54
  end
@@ -3,6 +3,7 @@
3
3
  require "securerandom"
4
4
  require_relative "base"
5
5
  require_relative "../../../crypto/field_helpers"
6
+ require_relative "../../../active_record_helpers"
6
7
 
7
8
  module Llmemory
8
9
  module LongTerm
@@ -10,6 +11,7 @@ module Llmemory
10
11
  module Storages
11
12
  class ActiveRecordStorage < Base
12
13
  include Llmemory::Crypto::FieldHelpers
14
+ include Llmemory::ActiveRecordHelpers
13
15
 
14
16
  def initialize(cipher: nil)
15
17
  @cipher = cipher || Llmemory.build_cipher
@@ -64,7 +66,7 @@ module Llmemory
64
66
  rec = LlmemoryCategory.find_or_initialize_by(user_id: user_id, category_name: category_name)
65
67
  rec.content = enc(content)
66
68
  rec.updated_at = Time.current
67
- rec.save!
69
+ with_unique_retry { rec.save! }
68
70
  true
69
71
  end
70
72
 
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "set"
3
4
  require_relative "node"
4
5
  require_relative "edge"
5
6
  require_relative "knowledge_graph"
@@ -14,9 +15,10 @@ module Llmemory
14
15
  class Memory
15
16
  include Llmemory::MemoryModule
16
17
 
17
- def initialize(user_id:, storage: nil, vector_store: nil, llm: nil, extractor: nil, cipher: nil)
18
+ def initialize(user_id:, storage: nil, vector_store: nil, llm: nil, extractor: nil, cipher: nil, forget_log_store: nil)
18
19
  @user_id = user_id
19
20
  @cipher = cipher || Llmemory.build_cipher
21
+ @forget_log_store = forget_log_store
20
22
  @graph_storage = storage || Storages.build(cipher: @cipher)
21
23
  @kg = KnowledgeGraph.new(user_id: user_id, storage: @graph_storage)
22
24
  @conflict_resolver = ConflictResolver.new(@kg)
@@ -105,6 +107,7 @@ module Llmemory
105
107
  # store only exposes soft archive_edge). Both modes route to archive
106
108
  # for now; behavior is the same — kept for API uniformity.
107
109
  archived = Array(ids).map(&:to_s).select { |edge_id| @kg.archive_edge(edge_id) }
110
+ remove_edge_embeddings(archived)
108
111
  forget_log.record(@user_id, memory_type: "graph_based", ids: archived, reason: reason)
109
112
  Llmemory::Instrumentation.instrument(:memory_forget, memory_type: "graph_based", user_id: @user_id, count: archived.size, mode: mode)
110
113
  archived.size
@@ -147,6 +150,14 @@ module Llmemory
147
150
  subject_id = name_to_id[subject] || @kg.add_node(entity_type: "concept", name: subject, properties: { "provenance" => provenance })
148
151
  object_id = name_to_id[object] || @kg.add_node(entity_type: "concept", name: object, properties: { "provenance" => provenance })
149
152
 
153
+ existing = @kg.find_edges(
154
+ subject: subject_id,
155
+ predicate: predicate,
156
+ object: object_id,
157
+ include_archived: false
158
+ )
159
+ next if existing.any?
160
+
150
161
  edge = Edge.new(
151
162
  id: nil,
152
163
  user_id: @user_id,
@@ -157,7 +168,8 @@ module Llmemory
157
168
  created_at: Time.now,
158
169
  archived_at: nil
159
170
  )
160
- @conflict_resolver.resolve(edge)
171
+ archived_ids = @conflict_resolver.resolve(edge)
172
+ remove_edge_embeddings(archived_ids)
161
173
  edge_id = @kg.add_edge(subject: subject_id, predicate: predicate, object: object_id, properties: { "provenance" => provenance })
162
174
 
163
175
  edge_text = "#{subject} #{predicate} #{object}"
@@ -180,8 +192,10 @@ module Llmemory
180
192
  vector_results = @vector_store.search(emb, top_k: top_k, user_id: @user_id)
181
193
  end
182
194
 
183
- out = vector_results.map do |v|
195
+ out = vector_results.filter_map do |v|
184
196
  id = v[:id] || v["id"]
197
+ next unless active_edge?(id)
198
+
185
199
  meta = v[:metadata] || v["metadata"] || {}
186
200
  { id: id, text: meta["text"] || meta[:text] || id.to_s, score: v[:score] || v["score"] || 1.0, created_at: meta["created_at"] || meta[:created_at] }
187
201
  end
@@ -217,9 +231,14 @@ module Llmemory
217
231
 
218
232
  def extract_node_ids_from_text(text)
219
233
  return [] if text.to_s.empty?
234
+
235
+ query_tokens = Llmemory::Tokenizer.tokenize(text).to_set
220
236
  ids = []
221
- @kg.list_nodes.each do |n|
222
- ids << n.id if text.to_s.include?(n.name.to_s)
237
+ @kg.list_nodes.each do |node|
238
+ name_tokens = Llmemory::Tokenizer.tokenize(node.name.to_s)
239
+ next if name_tokens.empty?
240
+
241
+ ids << node.id if name_tokens.all? { |token| query_tokens.include?(token) }
223
242
  end
224
243
  ids
225
244
  end
@@ -244,6 +263,21 @@ module Llmemory
244
263
  store: Llmemory::ShortTerm::Stores.build(cipher: @cipher)
245
264
  )
246
265
  end
266
+
267
+ def remove_edge_embeddings(edge_ids)
268
+ return unless @vector_store.respond_to?(:delete)
269
+
270
+ Array(edge_ids).each do |edge_id|
271
+ @vector_store.delete(id: edge_id, user_id: @user_id)
272
+ end
273
+ end
274
+
275
+ def active_edge?(edge_id)
276
+ return false if edge_id.nil?
277
+
278
+ @kg.find_edges(subject: nil, predicate: nil, object: nil, include_archived: false)
279
+ .any? { |edge| edge.id.to_s == edge_id.to_s }
280
+ end
247
281
  end
248
282
  end
249
283
  end
@@ -9,9 +9,14 @@ module Llmemory
9
9
  module Storages
10
10
  def self.build(store: nil, cipher: nil)
11
11
  resolved_cipher = cipher || Llmemory.build_cipher
12
- case (store || Llmemory.configuration.long_term_store).to_s.to_sym
12
+ store_type = (store || Llmemory.configuration.long_term_store).to_s.to_sym
13
+ case store_type
13
14
  when :memory
14
- MemoryStorage.new
15
+ if Llmemory.configuration.shared_memory_stores
16
+ shared_memory_storage
17
+ else
18
+ MemoryStorage.new
19
+ end
15
20
  when :active_record, :activerecord
16
21
  require_relative "storages/active_record_storage"
17
22
  ActiveRecordStorage.new(cipher: resolved_cipher)
@@ -21,6 +26,14 @@ module Llmemory
21
26
  "graph_based long-term memory supports long_term_store :memory or :active_record; got #{store_name.inspect}"
22
27
  end
23
28
  end
29
+
30
+ def self.shared_memory_storage
31
+ @shared_memory_storage ||= MemoryStorage.new
32
+ end
33
+
34
+ def self.reset_shared_singletons!
35
+ @shared_memory_storage = nil
36
+ end
24
37
  end
25
38
  end
26
39
  end
@@ -22,9 +22,10 @@ module Llmemory
22
22
 
23
23
  attr_reader :user_id, :storage
24
24
 
25
- def initialize(user_id:, storage: nil, vector_store: nil, cipher: nil)
25
+ def initialize(user_id:, storage: nil, vector_store: nil, cipher: nil, forget_log_store: nil)
26
26
  @user_id = user_id
27
27
  @cipher = cipher || Llmemory.build_cipher
28
+ @forget_log_store = forget_log_store
28
29
  @storage = storage || Storages.build(cipher: @cipher)
29
30
  @vector_store = vector_store
30
31
  @vector_explicit = !vector_store.nil?
@@ -71,11 +72,13 @@ module Llmemory
71
72
  # Retrieval Engine integration: skills ranked by relevance, recency and
72
73
  # proven utility (success rate exposed as importance). Hybrid (vector +
73
74
  # keyword) when a vector store is active; otherwise keyword-only.
75
+ KEYWORD_MATCH_SCORE = 0.6
76
+
74
77
  def search_candidates(query, user_id: nil, top_k: 20)
75
78
  uid = user_id || @user_id
76
79
  return [] unless uid == @user_id
77
80
 
78
- keyword = @storage.search_skills(uid, query).first(top_k).map { |raw| candidate_for(raw, 1.0) }
81
+ keyword = @storage.search_skills(uid, query).first(top_k).map { |raw| candidate_for(raw, KEYWORD_MATCH_SCORE) }
79
82
  vs = vector_store
80
83
  return keyword unless vs
81
84
 
@@ -5,6 +5,7 @@ require "securerandom"
5
5
  require "time"
6
6
  require_relative "base"
7
7
  require_relative "../../../crypto/field_helpers"
8
+ require_relative "../../../active_record_helpers"
8
9
 
9
10
  module Llmemory
10
11
  module LongTerm
@@ -15,6 +16,7 @@ module Llmemory
15
16
  # handles. Mirrors the file-based ActiveRecordStorage pattern.
16
17
  class ActiveRecordStorage < Base
17
18
  include Llmemory::Crypto::FieldHelpers
19
+ include Llmemory::ActiveRecordHelpers
18
20
 
19
21
  def initialize(cipher: nil)
20
22
  @cipher = cipher || Llmemory.build_cipher
@@ -41,7 +43,7 @@ module Llmemory
41
43
  name = (data["name"] || data[:name]).to_s
42
44
  rec.name_det = enc_det(name) if LlmemorySkill.column_names.include?("name_det")
43
45
  rec.created_at ||= Time.current
44
- rec.save!
46
+ with_unique_retry { rec.save! }
45
47
  id
46
48
  end
47
49
 
@@ -43,12 +43,16 @@ module Llmemory
43
43
 
44
44
  def merge_items(group)
45
45
  contents = group.map { |i| i[:content].to_s }.uniq
46
+ importances = group.map { |i| (i[:importance] || i["importance"]).to_f }.compact
47
+ provenances = group.filter_map { |i| i[:provenance] || i["provenance"] }
46
48
  {
47
49
  id: "merged_#{SecureRandom.hex(4)}",
48
50
  category: group.first[:category],
49
51
  content: contents.join("; "),
50
- source_resource_id: group.first[:source_resource_id]
51
- }
52
+ source_resource_id: group.first[:source_resource_id],
53
+ importance: importances.empty? ? nil : importances.max,
54
+ provenance: provenances.last
55
+ }.compact
52
56
  end
53
57
  end
54
58
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "cgi"
4
+
3
5
  module Llmemory
4
6
  module MCP
5
7
  # Rack middleware for token-based authentication
@@ -17,10 +19,8 @@ module Llmemory
17
19
  end
18
20
 
19
21
  def call(env)
20
- # If no token is configured, skip authentication
21
22
  return @app.call(env) unless @token
22
23
 
23
- # Check for valid token
24
24
  return UNAUTHORIZED_RESPONSE unless valid_token?(env)
25
25
 
26
26
  @app.call(env)
@@ -50,7 +50,9 @@ module Llmemory
50
50
  def parse_query_string(query_string)
51
51
  query_string.split("&").each_with_object({}) do |pair, hash|
52
52
  key, value = pair.split("=", 2)
53
- hash[key] = value if key
53
+ next unless key
54
+
55
+ hash[CGI.unescape(key)] = CGI.unescape(value.to_s)
54
56
  end
55
57
  end
56
58
 
@@ -39,7 +39,10 @@ module Llmemory
39
39
  transport.open
40
40
  end
41
41
 
42
- def run_http(port: 3100, host: "0.0.0.0", ssl_cert: nil, ssl_key: nil)
42
+ def run_http(port: 3100, host: "127.0.0.1", ssl_cert: nil, ssl_key: nil)
43
+ require "socket"
44
+ validate_http_bind!(host)
45
+
43
46
  require "webrick"
44
47
 
45
48
  @http_transport = ::MCP::Server::Transports::StreamableHTTPTransport.new(@server)
@@ -96,13 +99,25 @@ module Llmemory
96
99
 
97
100
  def build_rack_app(transport)
98
101
  app = ->(env) { transport.handle_request(RackRequest.new(env)) }
102
+ app = Authentication.new(app)
103
+ app
104
+ end
99
105
 
100
- # Wrap with authentication if MCP_TOKEN is set
101
- if ENV["MCP_TOKEN"]
102
- app = Authentication.new(app)
103
- end
106
+ def validate_http_bind!(host)
107
+ return if loopback_host?(host)
108
+ return if ENV["MCP_TOKEN"] && !ENV["MCP_TOKEN"].to_s.empty?
104
109
 
105
- app
110
+ raise Llmemory::ConfigurationError,
111
+ "MCP HTTP server cannot bind to #{host} without MCP_TOKEN; use 127.0.0.1 or set MCP_TOKEN"
112
+ end
113
+
114
+ def loopback_host?(host)
115
+ case host.to_s
116
+ when "127.0.0.1", "localhost", "::1"
117
+ true
118
+ else
119
+ false
120
+ end
106
121
  end
107
122
 
108
123
  def build_rack_env(req)
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Llmemory
4
+ module MCP
5
+ # Shared store resolution for MCP tools. Respects global configuration and
6
+ # long_term_type when selecting backends.
7
+ module StoreHelpers
8
+ module_function
9
+
10
+ def short_term_store
11
+ ShortTerm::Stores.build
12
+ end
13
+
14
+ def long_term_storage
15
+ if graph_based?
16
+ LongTerm::GraphBased::Storages.build
17
+ else
18
+ LongTerm::FileBased::Storages.build
19
+ end
20
+ end
21
+
22
+ def long_term_memory(user_id:)
23
+ if graph_based?
24
+ LongTerm::GraphBased::Memory.new(user_id: user_id, storage: long_term_storage)
25
+ else
26
+ LongTerm::FileBased::Memory.new(user_id: user_id, storage: long_term_storage)
27
+ end
28
+ end
29
+
30
+ def graph_based?
31
+ Llmemory.configuration.long_term_type.to_sym == :graph_based
32
+ end
33
+ end
34
+ end
35
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../store_helpers"
4
+
3
5
  module Llmemory
4
6
  module MCP
5
7
  module Tools
@@ -20,20 +22,21 @@ module Llmemory
20
22
 
21
23
  class << self
22
24
  def call(user_id:, content:, category: nil, server_context: nil)
23
- storage = build_storage
24
-
25
- # If no category provided, use a default
26
25
  cat = category || "observations"
27
26
 
28
- # Generate a simple resource ID for tracking
29
- resource_id = "mcp_#{Time.now.to_i}_#{rand(1000)}"
30
-
31
- storage.save_item(
32
- user_id,
33
- category: cat,
34
- content: content,
35
- source_resource_id: resource_id
36
- )
27
+ if StoreHelpers.graph_based?
28
+ memory = StoreHelpers.long_term_memory(user_id: user_id)
29
+ memory.remember_fact(content: content, category: cat)
30
+ else
31
+ storage = StoreHelpers.long_term_storage
32
+ resource_id = "mcp_#{Time.now.to_i}_#{rand(1000)}"
33
+ storage.save_item(
34
+ user_id,
35
+ category: cat,
36
+ content: content,
37
+ source_resource_id: resource_id
38
+ )
39
+ end
37
40
 
38
41
  ::MCP::Tool::Response.new([{
39
42
  type: "text",
@@ -45,12 +48,6 @@ module Llmemory
45
48
  text: "Error saving memory: #{e.message}"
46
49
  }], error: true)
47
50
  end
48
-
49
- private
50
-
51
- def build_storage
52
- LongTerm::FileBased::Storages.build
53
- end
54
51
  end
55
52
  end
56
53
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../store_helpers"
4
+
3
5
  module Llmemory
4
6
  module MCP
5
7
  module Tools
@@ -48,7 +50,7 @@ module Llmemory
48
50
  private
49
51
 
50
52
  def search_short_term(user_id, query, limit)
51
- store = build_short_term_store
53
+ store = StoreHelpers.short_term_store
52
54
  sessions = store.list_sessions(user_id: user_id)
53
55
  results = []
54
56
  query_lower = query.downcase
@@ -74,9 +76,21 @@ module Llmemory
74
76
  end
75
77
 
76
78
  def search_long_term(user_id, query, limit)
77
- storage = build_long_term_storage
78
79
  results = []
79
80
 
81
+ if StoreHelpers.graph_based?
82
+ memory = StoreHelpers.long_term_memory(user_id: user_id)
83
+ memory.search_candidates(query, user_id: user_id, top_k: limit).each do |item|
84
+ results << {
85
+ type: "long_term_fact",
86
+ content: item[:text] || item["text"] || item[:id] || item["id"],
87
+ created_at: item[:timestamp] || item["timestamp"]
88
+ }
89
+ end
90
+ return results
91
+ end
92
+
93
+ storage = StoreHelpers.long_term_storage
80
94
  items = storage.search_items(user_id, query)
81
95
  items.first(limit).each do |item|
82
96
  results << {
@@ -90,23 +104,6 @@ module Llmemory
90
104
  results
91
105
  end
92
106
 
93
- def build_short_term_store
94
- case Llmemory.configuration.short_term_store.to_sym
95
- when :memory then ShortTerm::Stores::MemoryStore.new
96
- when :redis then ShortTerm::Stores::RedisStore.new
97
- when :postgres then ShortTerm::Stores::PostgresStore.new
98
- when :active_record, :activerecord
99
- require_relative "../../short_term/stores/active_record_store"
100
- ShortTerm::Stores::ActiveRecordStore.new
101
- else
102
- ShortTerm::Stores::MemoryStore.new
103
- end
104
- end
105
-
106
- def build_long_term_storage
107
- LongTerm::FileBased::Storages.build
108
- end
109
-
110
107
  def format_results(results)
111
108
  return "No memories found matching the query." if results.empty?
112
109