llmemory 0.1.1

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 (63) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +193 -0
  4. data/lib/generators/llmemory/install/install_generator.rb +24 -0
  5. data/lib/generators/llmemory/install/templates/create_llmemory_tables.rb +73 -0
  6. data/lib/llmemory/configuration.rb +51 -0
  7. data/lib/llmemory/extractors/entity_relation_extractor.rb +74 -0
  8. data/lib/llmemory/extractors/fact_extractor.rb +74 -0
  9. data/lib/llmemory/extractors.rb +9 -0
  10. data/lib/llmemory/llm/anthropic.rb +48 -0
  11. data/lib/llmemory/llm/base.rb +17 -0
  12. data/lib/llmemory/llm/openai.rb +46 -0
  13. data/lib/llmemory/llm.rb +18 -0
  14. data/lib/llmemory/long_term/file_based/category.rb +22 -0
  15. data/lib/llmemory/long_term/file_based/item.rb +31 -0
  16. data/lib/llmemory/long_term/file_based/memory.rb +83 -0
  17. data/lib/llmemory/long_term/file_based/resource.rb +22 -0
  18. data/lib/llmemory/long_term/file_based/retrieval.rb +90 -0
  19. data/lib/llmemory/long_term/file_based/storage.rb +35 -0
  20. data/lib/llmemory/long_term/file_based/storages/active_record_models.rb +26 -0
  21. data/lib/llmemory/long_term/file_based/storages/active_record_storage.rb +144 -0
  22. data/lib/llmemory/long_term/file_based/storages/base.rb +71 -0
  23. data/lib/llmemory/long_term/file_based/storages/database_storage.rb +231 -0
  24. data/lib/llmemory/long_term/file_based/storages/file_storage.rb +180 -0
  25. data/lib/llmemory/long_term/file_based/storages/memory_storage.rb +100 -0
  26. data/lib/llmemory/long_term/file_based.rb +15 -0
  27. data/lib/llmemory/long_term/graph_based/conflict_resolver.rb +33 -0
  28. data/lib/llmemory/long_term/graph_based/edge.rb +49 -0
  29. data/lib/llmemory/long_term/graph_based/knowledge_graph.rb +114 -0
  30. data/lib/llmemory/long_term/graph_based/memory.rb +143 -0
  31. data/lib/llmemory/long_term/graph_based/node.rb +42 -0
  32. data/lib/llmemory/long_term/graph_based/storage.rb +24 -0
  33. data/lib/llmemory/long_term/graph_based/storages/active_record_models.rb +23 -0
  34. data/lib/llmemory/long_term/graph_based/storages/active_record_storage.rb +132 -0
  35. data/lib/llmemory/long_term/graph_based/storages/base.rb +39 -0
  36. data/lib/llmemory/long_term/graph_based/storages/memory_storage.rb +106 -0
  37. data/lib/llmemory/long_term/graph_based.rb +15 -0
  38. data/lib/llmemory/long_term.rb +9 -0
  39. data/lib/llmemory/maintenance/consolidator.rb +55 -0
  40. data/lib/llmemory/maintenance/reindexer.rb +27 -0
  41. data/lib/llmemory/maintenance/runner.rb +34 -0
  42. data/lib/llmemory/maintenance/summarizer.rb +57 -0
  43. data/lib/llmemory/maintenance.rb +8 -0
  44. data/lib/llmemory/memory.rb +96 -0
  45. data/lib/llmemory/retrieval/context_assembler.rb +53 -0
  46. data/lib/llmemory/retrieval/engine.rb +74 -0
  47. data/lib/llmemory/retrieval/temporal_ranker.rb +23 -0
  48. data/lib/llmemory/retrieval.rb +10 -0
  49. data/lib/llmemory/short_term/checkpoint.rb +47 -0
  50. data/lib/llmemory/short_term/stores/active_record_checkpoint.rb +14 -0
  51. data/lib/llmemory/short_term/stores/active_record_store.rb +58 -0
  52. data/lib/llmemory/short_term/stores/base.rb +21 -0
  53. data/lib/llmemory/short_term/stores/memory_store.rb +37 -0
  54. data/lib/llmemory/short_term/stores/postgres_store.rb +80 -0
  55. data/lib/llmemory/short_term/stores/redis_store.rb +54 -0
  56. data/lib/llmemory/short_term.rb +8 -0
  57. data/lib/llmemory/vector_store/base.rb +19 -0
  58. data/lib/llmemory/vector_store/memory_store.rb +53 -0
  59. data/lib/llmemory/vector_store/openai_embeddings.rb +49 -0
  60. data/lib/llmemory/vector_store.rb +10 -0
  61. data/lib/llmemory/version.rb +5 -0
  62. data/lib/llmemory.rb +19 -0
  63. metadata +163 -0
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
5
+ module Llmemory
6
+ module Maintenance
7
+ class Consolidator
8
+ def initialize(storage)
9
+ @storage = storage
10
+ end
11
+
12
+ def run_nightly(user_id)
13
+ recent = @storage.get_items_since(user_id, hours: 24)
14
+ duplicates = find_duplicates(recent)
15
+
16
+ duplicates.each do |group|
17
+ merged = merge_items(group)
18
+ ids = group.map { |i| i[:id] }
19
+ @storage.replace_items(user_id, ids, merged)
20
+ end
21
+
22
+ true
23
+ end
24
+
25
+ private
26
+
27
+ def find_duplicates(items)
28
+ groups = []
29
+ seen = {}
30
+ items.each do |item|
31
+ key = normalize_content(item[:content].to_s)
32
+ next if key.empty?
33
+ seen[key] ||= []
34
+ seen[key] << item
35
+ end
36
+ seen.each_value { |v| groups << v if v.size > 1 }
37
+ groups
38
+ end
39
+
40
+ def normalize_content(content)
41
+ content.to_s.downcase.gsub(/\s+/, " ").strip[0..200]
42
+ end
43
+
44
+ def merge_items(group)
45
+ contents = group.map { |i| i[:content].to_s }.uniq
46
+ {
47
+ id: "merged_#{SecureRandom.hex(4)}",
48
+ category: group.first[:category],
49
+ content: contents.join("; "),
50
+ source_resource_id: group.first[:source_resource_id]
51
+ }
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Llmemory
4
+ module Maintenance
5
+ class Reindexer
6
+ ARCHIVE_AFTER_DAYS = 180
7
+
8
+ def initialize(storage)
9
+ @storage = storage
10
+ end
11
+
12
+ def run_monthly(user_id)
13
+ items = @storage.get_all_items(user_id)
14
+ resources = @storage.get_all_resources(user_id)
15
+ cutoff = Time.now - (ARCHIVE_AFTER_DAYS * 86400)
16
+
17
+ old_item_ids = items.select { |i| i[:created_at] < cutoff }.map { |i| i[:id] }
18
+ old_resource_ids = resources.select { |r| r[:created_at] < cutoff }.map { |r| r[:id] }
19
+
20
+ @storage.archive_items(user_id, old_item_ids) if old_item_ids.any?
21
+ @storage.archive_resources(user_id, old_resource_ids) if old_resource_ids.any?
22
+
23
+ true
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "consolidator"
4
+ require_relative "summarizer"
5
+ require_relative "reindexer"
6
+
7
+ module Llmemory
8
+ module Maintenance
9
+ class Runner
10
+ class << self
11
+ def run_nightly(user_id, storage: nil)
12
+ storage ||= default_storage(user_id)
13
+ Consolidator.new(storage).run_nightly(user_id)
14
+ end
15
+
16
+ def run_weekly(user_id, storage: nil)
17
+ storage ||= default_storage(user_id)
18
+ Summarizer.new(storage).run_weekly(user_id)
19
+ end
20
+
21
+ def run_monthly(user_id, storage: nil)
22
+ storage ||= default_storage(user_id)
23
+ Reindexer.new(storage).run_monthly(user_id)
24
+ end
25
+
26
+ private
27
+
28
+ def default_storage(user_id)
29
+ Llmemory::LongTerm::FileBased::Storages.build
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Llmemory
4
+ module Maintenance
5
+ class Summarizer
6
+ def initialize(storage, llm: nil)
7
+ @storage = storage
8
+ @llm = llm || Llmemory::LLM.client
9
+ end
10
+
11
+ def run_weekly(user_id, prune_after_days: nil)
12
+ prune_after_days ||= Llmemory.configuration.prune_after_days
13
+ old_items = @storage.get_items_older_than(user_id, days: 30)
14
+ categories = group_by_category(old_items)
15
+
16
+ categories.each do |category, items|
17
+ next if items.empty?
18
+ summary = create_summary(items)
19
+ existing = @storage.load_category(user_id, category)
20
+ updated = existing.to_s.empty? ? summary : "#{existing}\n\n## Archived summary\n#{summary}"
21
+ @storage.save_category(user_id, category, updated)
22
+ @storage.archive_items(user_id, items.map { |i| i[:id] })
23
+ end
24
+
25
+ prune_stale_items(user_id, prune_after_days)
26
+ true
27
+ end
28
+
29
+ private
30
+
31
+ def group_by_category(items)
32
+ items.group_by { |i| i[:category] }
33
+ end
34
+
35
+ def create_summary(items)
36
+ bullet_points = items.map { |i| "- #{i[:content]}" }.join("\n")
37
+ return bullet_points if bullet_points.length < 500
38
+ prompt = <<~PROMPT
39
+ Summarize these memory items into a short markdown paragraph (max 200 words).
40
+ Items:
41
+ #{bullet_points}
42
+ Return only the summary.
43
+ PROMPT
44
+ @llm.invoke(prompt.strip).to_s
45
+ rescue Llmemory::LLMError
46
+ bullet_points[0..500]
47
+ end
48
+
49
+ def prune_stale_items(user_id, days)
50
+ old = @storage.get_items_older_than(user_id, days: days)
51
+ return if old.empty?
52
+ ids = old.map { |i| i[:id] }
53
+ @storage.archive_items(user_id, ids)
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "maintenance/runner"
4
+
5
+ module Llmemory
6
+ module Maintenance
7
+ end
8
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "short_term/checkpoint"
4
+ require_relative "long_term/file_based"
5
+ require_relative "retrieval/engine"
6
+
7
+ module Llmemory
8
+ class Memory
9
+ DEFAULT_SESSION_ID = "default"
10
+ STATE_KEY_MESSAGES = :messages
11
+
12
+ def initialize(user_id:, session_id: DEFAULT_SESSION_ID, checkpoint: nil, long_term: nil, long_term_type: nil, retrieval_engine: nil)
13
+ @user_id = user_id
14
+ @session_id = session_id
15
+ @checkpoint = checkpoint || ShortTerm::Checkpoint.new(user_id: user_id, session_id: session_id)
16
+ type = long_term_type || Llmemory.configuration.long_term_type || :file_based
17
+ @long_term = long_term || build_long_term(type)
18
+ @retrieval_engine = retrieval_engine || Retrieval::Engine.new(@long_term)
19
+ end
20
+
21
+ def add_message(role:, content:)
22
+ msgs = messages
23
+ msgs << { role: role.to_sym, content: content.to_s }
24
+ save_state(messages: msgs)
25
+ true
26
+ end
27
+
28
+ def messages
29
+ state = @checkpoint.restore_state
30
+ return [] unless state.is_a?(Hash)
31
+ list = state[STATE_KEY_MESSAGES] || state[STATE_KEY_MESSAGES.to_s]
32
+ list.is_a?(Array) ? list.dup : []
33
+ end
34
+
35
+ def retrieve(query, max_tokens: nil)
36
+ short_context = format_short_term_context(messages)
37
+ long_context = @retrieval_engine.retrieve_for_inference(query, user_id: @user_id, max_tokens: max_tokens)
38
+ combine_contexts(short_context, long_context)
39
+ end
40
+
41
+ def consolidate!
42
+ msgs = messages
43
+ return true if msgs.empty?
44
+ conversation_text = msgs.map { |m| "#{m[:role]}: #{m[:content]}" }.join("\n")
45
+ @long_term.memorize(conversation_text)
46
+ true
47
+ end
48
+
49
+ def clear_session!
50
+ @checkpoint.clear_state
51
+ true
52
+ end
53
+
54
+ def user_id
55
+ @user_id
56
+ end
57
+
58
+ private
59
+
60
+ def build_long_term(long_term_type)
61
+ case long_term_type.to_s.to_sym
62
+ when :graph_based
63
+ LongTerm::GraphBased::Memory.new(
64
+ user_id: @user_id,
65
+ storage: LongTerm::GraphBased::Storages.build
66
+ )
67
+ else
68
+ LongTerm::FileBased::Memory.new(user_id: @user_id, storage: LongTerm::FileBased::Storages.build)
69
+ end
70
+ end
71
+
72
+ def save_state(messages:)
73
+ @checkpoint.save_state(STATE_KEY_MESSAGES => messages)
74
+ end
75
+
76
+ def format_short_term_context(msgs)
77
+ return "" if msgs.empty?
78
+ lines = ["=== RECENT CONVERSATION ===", ""]
79
+ msgs.each do |m|
80
+ role = m[:role] || m["role"]
81
+ content = m[:content] || m["content"]
82
+ lines << "#{role}: #{content}"
83
+ end
84
+ lines << ""
85
+ lines << "=== END RECENT CONVERSATION ==="
86
+ lines.join("\n")
87
+ end
88
+
89
+ def combine_contexts(short_context, long_context)
90
+ parts = []
91
+ parts << short_context if short_context.to_s.strip.length.positive?
92
+ parts << long_context.to_s.strip if long_context.to_s.strip.length.positive?
93
+ parts.join("\n\n")
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Llmemory
4
+ module Retrieval
5
+ class ContextAssembler
6
+ def initialize(max_tokens: nil)
7
+ @max_tokens = max_tokens || Llmemory.configuration.max_retrieval_tokens
8
+ end
9
+
10
+ def assemble(ranked_memories, max_tokens: nil)
11
+ max_tokens ||= @max_tokens
12
+ selected = []
13
+ token_count = 0
14
+
15
+ ranked_memories.each do |memory|
16
+ text = memory[:text] || memory["text"] || ""
17
+ memory_tokens = count_tokens(text)
18
+ break if token_count + memory_tokens > max_tokens
19
+
20
+ selected << {
21
+ text: text,
22
+ timestamp: memory[:timestamp] || memory["timestamp"],
23
+ confidence: memory[:temporal_score] || memory[:score] || memory["score"]
24
+ }
25
+ token_count += memory_tokens
26
+ end
27
+
28
+ format_context(selected)
29
+ end
30
+
31
+ def count_tokens(text)
32
+ (text.to_s.length / 4.0).ceil
33
+ end
34
+
35
+ private
36
+
37
+ def format_context(memories)
38
+ lines = ["=== RELEVANT MEMORIES ===", ""]
39
+ memories.each do |mem|
40
+ ts = mem[:timestamp]
41
+ ts_str = ts.respond_to?(:iso8601) ? ts.iso8601 : ts.to_s
42
+ conf = mem[:confidence]
43
+ conf_str = conf.is_a?(Numeric) ? format("%.2f", conf) : conf.to_s
44
+ lines << "[#{ts_str}] (confidence: #{conf_str})"
45
+ lines << mem[:text].to_s
46
+ lines << ""
47
+ end
48
+ lines << "=== END MEMORIES ==="
49
+ lines.join("\n")
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "temporal_ranker"
4
+ require_relative "context_assembler"
5
+
6
+ module Llmemory
7
+ module Retrieval
8
+ class Engine
9
+ RELEVANCE_THRESHOLD = 0.7
10
+
11
+ def initialize(memory, llm: nil)
12
+ @memory = memory
13
+ @llm = llm || Llmemory::LLM.client
14
+ @ranker = TemporalRanker.new
15
+ @assembler = ContextAssembler.new
16
+ end
17
+
18
+ def retrieve_for_inference(user_message, user_id: nil, max_tokens: nil)
19
+ user_id ||= @memory.respond_to?(:user_id) ? @memory.user_id : nil
20
+ search_query = generate_query(user_message)
21
+ candidates = fetch_candidates(search_query, user_id)
22
+
23
+ relevant = filter_by_relevance(candidates, user_message)
24
+ ranked = @ranker.rank(relevant)
25
+ @assembler.assemble(ranked, max_tokens: max_tokens)
26
+ end
27
+
28
+ private
29
+
30
+ def generate_query(user_message)
31
+ return user_message.to_s if user_message.to_s.length <= 100
32
+ prompt = <<~PROMPT
33
+ Summarize this user message into a short search query (one sentence, under 15 words) to find relevant memories.
34
+ Message: #{user_message}
35
+ Return only the search query.
36
+ PROMPT
37
+ @llm.invoke(prompt.strip).to_s.strip
38
+ rescue Llmemory::LLMError
39
+ user_message.to_s[0..200]
40
+ end
41
+
42
+ def fetch_candidates(search_query, user_id)
43
+ return [] unless @memory.respond_to?(:search_candidates)
44
+
45
+ raw = @memory.search_candidates(search_query, user_id: user_id, top_k: 20)
46
+ raw.map do |c|
47
+ {
48
+ text: c[:text] || c["text"],
49
+ timestamp: parse_timestamp(c[:timestamp] || c["timestamp"] || c[:created_at] || c["created_at"]),
50
+ score: (c[:score] || c["score"] || 1.0).to_f
51
+ }
52
+ end
53
+ end
54
+
55
+ def parse_timestamp(ts)
56
+ return ts if ts.is_a?(Time)
57
+ return Time.parse(ts.to_s) if ts
58
+ Time.now
59
+ end
60
+
61
+ def filter_by_relevance(candidates, user_message)
62
+ return candidates if candidates.size <= 5
63
+ user_lower = user_message.to_s.downcase
64
+ candidates.select do |c|
65
+ text = (c[:text] || c["text"]).to_s.downcase
66
+ score = (c[:score] || c["score"] || 1.0).to_f
67
+ next true if score >= RELEVANCE_THRESHOLD
68
+ next true if user_lower.split.any? { |w| w.length > 3 && text.include?(w) }
69
+ score >= 0.5
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Llmemory
4
+ module Retrieval
5
+ class TemporalRanker
6
+ def initialize(half_life_days: nil)
7
+ @half_life_days = half_life_days || Llmemory.configuration.time_decay_half_life_days
8
+ end
9
+
10
+ def rank(candidates, now: Time.now)
11
+ candidates.map do |c|
12
+ score = (c[:score] || c["score"] || 1.0).to_f
13
+ timestamp = c[:timestamp] || c["timestamp"]
14
+ timestamp = Time.parse(timestamp.to_s) if timestamp.is_a?(String)
15
+ age_days = timestamp ? (now - timestamp).to_i / 86400 : 0
16
+ time_decay = 1.0 / (1.0 + (age_days.to_f / @half_life_days))
17
+ final_score = score * time_decay
18
+ c.merge(score: score, temporal_score: final_score, timestamp: timestamp)
19
+ end.sort_by { |c| -(c[:temporal_score] || 0) }
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "retrieval/temporal_ranker"
4
+ require_relative "retrieval/context_assembler"
5
+ require_relative "retrieval/engine"
6
+
7
+ module Llmemory
8
+ module Retrieval
9
+ end
10
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "stores/base"
4
+ require_relative "stores/memory_store"
5
+ require_relative "stores/redis_store"
6
+ require_relative "stores/postgres_store"
7
+
8
+ module Llmemory
9
+ module ShortTerm
10
+ class Checkpoint
11
+ DEFAULT_SESSION_ID = "default"
12
+
13
+ def initialize(user_id:, session_id: DEFAULT_SESSION_ID, store: nil)
14
+ @user_id = user_id
15
+ @session_id = session_id
16
+ @store = store || build_store
17
+ end
18
+
19
+ def save_state(state)
20
+ @store.save(@user_id, @session_id, state)
21
+ end
22
+
23
+ def restore_state
24
+ @store.load(@user_id, @session_id)
25
+ end
26
+
27
+ def clear_state
28
+ @store.delete(@user_id, @session_id)
29
+ end
30
+
31
+ private
32
+
33
+ def build_store
34
+ case Llmemory.configuration.short_term_store.to_sym
35
+ when :memory then Stores::MemoryStore.new
36
+ when :redis then Stores::RedisStore.new
37
+ when :postgres then Stores::PostgresStore.new
38
+ when :active_record, :activerecord
39
+ require_relative "stores/active_record_store"
40
+ Stores::ActiveRecordStore.new
41
+ else
42
+ Stores::MemoryStore.new
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Model for short-term checkpoint (ActiveRecordStore). Table: llmemory_checkpoints.
4
+ # Create with: rails g llmemory:install && rails db:migrate
5
+
6
+ module Llmemory
7
+ module ShortTerm
8
+ module Stores
9
+ class ActiveRecordCheckpoint < ::ActiveRecord::Base
10
+ self.table_name = "llmemory_checkpoints"
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Llmemory
6
+ module ShortTerm
7
+ module Stores
8
+ class ActiveRecordStore < Base
9
+ def initialize
10
+ self.class.load_model!
11
+ end
12
+
13
+ def self.load_model!
14
+ return if @model_loaded
15
+ require "active_record"
16
+ require_relative "active_record_checkpoint"
17
+ @model_loaded = true
18
+ end
19
+
20
+ def save(user_id, session_id, state)
21
+ record = Llmemory::ShortTerm::Stores::ActiveRecordCheckpoint.find_or_initialize_by(
22
+ user_id: user_id,
23
+ session_id: session_id
24
+ )
25
+ record.state = state
26
+ record.updated_at = Time.current
27
+ record.save!
28
+ true
29
+ end
30
+
31
+ def load(user_id, session_id)
32
+ record = Llmemory::ShortTerm::Stores::ActiveRecordCheckpoint.find_by(
33
+ user_id: user_id,
34
+ session_id: session_id
35
+ )
36
+ return nil unless record
37
+ raw = record.state
38
+ raw.is_a?(Hash) ? raw.transform_keys(&:to_sym) : deserialize(raw)
39
+ end
40
+
41
+ def delete(user_id, session_id)
42
+ Llmemory::ShortTerm::Stores::ActiveRecordCheckpoint.where(
43
+ user_id: user_id,
44
+ session_id: session_id
45
+ ).destroy_all
46
+ true
47
+ end
48
+
49
+ private
50
+
51
+ def deserialize(data)
52
+ return data if data.is_a?(Hash)
53
+ JSON.parse(data.to_s, symbolize_names: true)
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Llmemory
4
+ module ShortTerm
5
+ module Stores
6
+ class Base
7
+ def save(user_id, session_id, state)
8
+ raise NotImplementedError, "#{self.class}#save must be implemented"
9
+ end
10
+
11
+ def load(user_id, session_id)
12
+ raise NotImplementedError, "#{self.class}#load must be implemented"
13
+ end
14
+
15
+ def delete(user_id, session_id)
16
+ raise NotImplementedError, "#{self.class}#delete must be implemented"
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Llmemory
6
+ module ShortTerm
7
+ module Stores
8
+ class MemoryStore < Base
9
+ def initialize
10
+ @store = {}
11
+ end
12
+
13
+ def save(user_id, session_id, state)
14
+ key = key_for(user_id, session_id)
15
+ @store[key] = { state: state, updated_at: Time.now }
16
+ true
17
+ end
18
+
19
+ def load(user_id, session_id)
20
+ key = key_for(user_id, session_id)
21
+ @store.dig(key, :state)
22
+ end
23
+
24
+ def delete(user_id, session_id)
25
+ @store.delete(key_for(user_id, session_id))
26
+ true
27
+ end
28
+
29
+ private
30
+
31
+ def key_for(user_id, session_id)
32
+ "#{user_id}:#{session_id}"
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end