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,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "node"
4
+ require_relative "edge"
5
+ require_relative "storages/base"
6
+ require_relative "storages/memory_storage"
7
+
8
+ module Llmemory
9
+ module LongTerm
10
+ module GraphBased
11
+ class KnowledgeGraph
12
+ def initialize(user_id:, storage: nil)
13
+ @user_id = user_id
14
+ @storage = storage || Storages::MemoryStorage.new
15
+ end
16
+
17
+ def add_node(entity_type:, name:, properties: {})
18
+ existing = @storage.find_node_by_name(@user_id, entity_type, name)
19
+ return existing.id if existing
20
+ node = Node.new(
21
+ id: nil,
22
+ user_id: @user_id,
23
+ entity_type: entity_type.to_s,
24
+ name: name.to_s,
25
+ properties: properties,
26
+ created_at: Time.now,
27
+ updated_at: Time.now
28
+ )
29
+ @storage.save_node(@user_id, node)
30
+ end
31
+
32
+ def find_node(name: nil, id: nil)
33
+ if id
34
+ @storage.find_node_by_id(@user_id, id)
35
+ elsif name
36
+ list_nodes.find { |n| n.name.to_s == name.to_s }
37
+ end
38
+ end
39
+
40
+ def find_node_by_id(id)
41
+ @storage.find_node_by_id(@user_id, id)
42
+ end
43
+
44
+ def add_edge(subject:, predicate:, object:, properties: {})
45
+ subject_id = subject.is_a?(Node) ? subject.id : subject.to_s
46
+ object_id = object.is_a?(Node) ? object.id : object.to_s
47
+ edge = Edge.new(
48
+ id: nil,
49
+ user_id: @user_id,
50
+ subject_id: subject_id,
51
+ predicate: predicate.to_s,
52
+ object_id: object_id,
53
+ properties: properties,
54
+ created_at: Time.now,
55
+ archived_at: nil
56
+ )
57
+ @storage.save_edge(@user_id, edge)
58
+ end
59
+
60
+ def find_edges(subject: nil, predicate: nil, object: nil, include_archived: false)
61
+ subject_id = subject.is_a?(Node) ? subject.id : subject
62
+ object_id = object.is_a?(Node) ? object.id : object
63
+ @storage.find_edges(
64
+ @user_id,
65
+ subject_id: subject_id,
66
+ predicate: predicate&.to_s,
67
+ object_id: object_id,
68
+ include_archived: include_archived
69
+ )
70
+ end
71
+
72
+ def traverse(start_node:, depth: 2)
73
+ start_id = start_node.is_a?(Node) ? start_node.id : start_node.to_s
74
+ visited = {}
75
+ queue = [[start_id, 0]]
76
+ result_nodes = []
77
+ result_edges = []
78
+
79
+ while queue.any?
80
+ node_id, d = queue.shift
81
+ next if d > depth
82
+ next if visited[node_id]
83
+ visited[node_id] = true
84
+ node = @storage.find_node_by_id(@user_id, node_id)
85
+ result_nodes << node if node
86
+
87
+ edges = @storage.find_edges(@user_id, subject_id: node_id, include_archived: false)
88
+ edges.each do |e|
89
+ result_edges << e
90
+ queue << [e.object_id, d + 1] unless visited[e.object_id]
91
+ end
92
+ edges_out = @storage.find_edges(@user_id, object_id: node_id, include_archived: false)
93
+ edges_out.each do |e|
94
+ result_edges << e
95
+ queue << [e.subject_id, d + 1] unless visited[e.subject_id]
96
+ end
97
+ end
98
+
99
+ { nodes: result_nodes.uniq, edges: result_edges.uniq }
100
+ end
101
+
102
+ def archive_edge(edge_id, reason: nil)
103
+ @storage.archive_edge(@user_id, edge_id, archived_at: Time.now)
104
+ end
105
+
106
+ def list_nodes
107
+ @storage.list_nodes(@user_id)
108
+ end
109
+
110
+ attr_reader :storage, :user_id
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "node"
4
+ require_relative "edge"
5
+ require_relative "knowledge_graph"
6
+ require_relative "conflict_resolver"
7
+ require_relative "storage"
8
+
9
+ module Llmemory
10
+ module LongTerm
11
+ module GraphBased
12
+ class Memory
13
+ def initialize(user_id:, storage: nil, vector_store: nil, llm: nil, extractor: nil)
14
+ @user_id = user_id
15
+ @graph_storage = storage || Storages.build
16
+ @kg = KnowledgeGraph.new(user_id: user_id, storage: @graph_storage)
17
+ @conflict_resolver = ConflictResolver.new(@kg)
18
+ @vector_store = vector_store || build_vector_store
19
+ @llm = llm || Llmemory::LLM.client
20
+ @extractor = extractor || Llmemory::Extractors::EntityRelationExtractor.new(llm: @llm)
21
+ end
22
+
23
+ def memorize(conversation_text)
24
+ data = @extractor.extract(conversation_text)
25
+ name_to_id = {}
26
+
27
+ data[:entities].each do |e|
28
+ id = @kg.add_node(entity_type: e[:type], name: e[:name], properties: {})
29
+ name_to_id[e[:name]] ||= id
30
+ end
31
+
32
+ data[:relations].each do |r|
33
+ subject_id = name_to_id[r[:subject]] || @kg.add_node(entity_type: "concept", name: r[:subject], properties: {})
34
+ object_id = name_to_id[r[:object]] || @kg.add_node(entity_type: "concept", name: r[:object], properties: {})
35
+
36
+ edge = Edge.new(
37
+ id: nil,
38
+ user_id: @user_id,
39
+ subject_id: subject_id,
40
+ predicate: r[:predicate],
41
+ object_id: object_id,
42
+ properties: {},
43
+ created_at: Time.now,
44
+ archived_at: nil
45
+ )
46
+ @conflict_resolver.resolve(edge)
47
+ edge_id = @kg.add_edge(subject: subject_id, predicate: r[:predicate], object: object_id, properties: {})
48
+
49
+ text = "#{r[:subject]} #{r[:predicate]} #{r[:object]}"
50
+ embedding = @vector_store.respond_to?(:embed) ? @vector_store.embed(text) : nil
51
+ if embedding && @vector_store.respond_to?(:store)
52
+ @vector_store.store(id: "edge_#{edge_id}", embedding: embedding, metadata: { text: text, created_at: Time.now }, user_id: @user_id)
53
+ end
54
+ end
55
+
56
+ true
57
+ end
58
+
59
+ def retrieve(query, top_k: 10)
60
+ results = hybrid_search(query, top_k: top_k)
61
+ format_as_context(results)
62
+ end
63
+
64
+ def search_candidates(query, user_id: nil, top_k: 20)
65
+ uid = user_id || @user_id
66
+ return [] unless uid == @user_id
67
+ results = hybrid_search(query, top_k: top_k)
68
+ results.map do |r|
69
+ {
70
+ text: r[:text],
71
+ timestamp: r[:created_at] || r[:timestamp],
72
+ score: r[:score] || 1.0
73
+ }
74
+ end
75
+ end
76
+
77
+ attr_reader :user_id
78
+
79
+ def storage
80
+ @graph_storage
81
+ end
82
+
83
+ private
84
+
85
+ def build_vector_store
86
+ emb = Llmemory::VectorStore::OpenAIEmbeddings.new
87
+ Llmemory::VectorStore::MemoryStore.new(embedding_provider: emb)
88
+ end
89
+
90
+ def hybrid_search(query, top_k:)
91
+ vector_results = []
92
+ if @vector_store.respond_to?(:search_by_text)
93
+ vector_results = @vector_store.search_by_text(query.to_s, top_k: top_k, user_id: @user_id)
94
+ elsif @vector_store.respond_to?(:embed) && @vector_store.respond_to?(:search)
95
+ emb = @vector_store.embed(query.to_s)
96
+ vector_results = @vector_store.search(emb, top_k: top_k, user_id: @user_id)
97
+ end
98
+
99
+ out = vector_results.map do |v|
100
+ id = v[:id] || v["id"]
101
+ meta = v[:metadata] || v["metadata"] || {}
102
+ { text: meta["text"] || meta[:text] || id.to_s, score: v[:score] || v["score"] || 1.0, created_at: meta["created_at"] || meta[:created_at] }
103
+ end
104
+
105
+ node_ids = out.flat_map { |r| extract_node_ids_from_text(r[:text]) }.compact.uniq
106
+ node_ids.first(3).each do |node_id|
107
+ node = @kg.find_node_by_id(node_id)
108
+ next unless node
109
+ traversed = @kg.traverse(start_node: node, depth: 1)
110
+ traversed[:edges].each do |e|
111
+ subj = @kg.find_node_by_id(e.subject_id)
112
+ obj = @kg.find_node_by_id(e.object_id)
113
+ edge_text = "#{subj&.name} #{e.predicate} #{obj&.name}"
114
+ out << { text: edge_text, score: 0.85, created_at: e.created_at } unless out.any? { |o| o[:text] == edge_text }
115
+ end
116
+ end
117
+
118
+ out.sort_by { |r| -(r[:score] || 0) }.first(top_k)
119
+ end
120
+
121
+ def extract_node_ids_from_text(text)
122
+ return [] if text.to_s.empty?
123
+ ids = []
124
+ @kg.list_nodes.each do |n|
125
+ ids << n.id if text.to_s.include?(n.name.to_s)
126
+ end
127
+ ids
128
+ end
129
+
130
+ def format_as_context(results)
131
+ return "" if results.empty?
132
+ lines = ["=== RELEVANT MEMORIES (GRAPH) ===", ""]
133
+ results.each do |r|
134
+ lines << "- #{r[:text]}"
135
+ end
136
+ lines << ""
137
+ lines << "=== END MEMORIES ==="
138
+ lines.join("\n")
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Llmemory
4
+ module LongTerm
5
+ module GraphBased
6
+ Node = Struct.new(
7
+ :id,
8
+ :user_id,
9
+ :entity_type,
10
+ :name,
11
+ :properties,
12
+ :created_at,
13
+ :updated_at,
14
+ keyword_init: true
15
+ ) do
16
+ def self.from_h(hash)
17
+ new(
18
+ id: hash[:id] || hash["id"],
19
+ user_id: hash[:user_id] || hash["user_id"],
20
+ entity_type: (hash[:entity_type] || hash["entity_type"]).to_s,
21
+ name: (hash[:name] || hash["name"]).to_s,
22
+ properties: hash[:properties] || hash["properties"] || {},
23
+ created_at: hash[:created_at] || hash["created_at"],
24
+ updated_at: hash[:updated_at] || hash["updated_at"]
25
+ )
26
+ end
27
+
28
+ def to_h
29
+ {
30
+ id: id,
31
+ user_id: user_id,
32
+ entity_type: entity_type,
33
+ name: name,
34
+ properties: properties || {},
35
+ created_at: created_at,
36
+ updated_at: updated_at
37
+ }
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "storages/base"
4
+ require_relative "storages/memory_storage"
5
+
6
+ module Llmemory
7
+ module LongTerm
8
+ module GraphBased
9
+ module Storages
10
+ def self.build(store: nil)
11
+ case (store || :memory).to_s.to_sym
12
+ when :memory
13
+ MemoryStorage.new
14
+ when :active_record, :activerecord
15
+ require_relative "storages/active_record_storage"
16
+ ActiveRecordStorage.new
17
+ else
18
+ MemoryStorage.new
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Llmemory
4
+ module LongTerm
5
+ module GraphBased
6
+ module Storages
7
+ class LlmemoryGraphNode < ::ActiveRecord::Base
8
+ self.table_name = "llmemory_graph_nodes"
9
+ self.primary_key = "id"
10
+ has_many :out_edges, class_name: "Llmemory::LongTerm::GraphBased::Storages::LlmemoryGraphEdge", foreign_key: :subject_id
11
+ has_many :in_edges, class_name: "Llmemory::LongTerm::GraphBased::Storages::LlmemoryGraphEdge", foreign_key: :object_id
12
+ end
13
+
14
+ class LlmemoryGraphEdge < ::ActiveRecord::Base
15
+ self.table_name = "llmemory_graph_edges"
16
+ self.primary_key = "id"
17
+ belongs_to :subject_node, class_name: "Llmemory::LongTerm::GraphBased::Storages::LlmemoryGraphNode", foreign_key: :subject_id, optional: true
18
+ belongs_to :object_node, class_name: "Llmemory::LongTerm::GraphBased::Storages::LlmemoryGraphNode", foreign_key: :object_id, optional: true
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+ require_relative "base"
5
+ require_relative "active_record_models"
6
+ require_relative "../node"
7
+ require_relative "../edge"
8
+
9
+ module Llmemory
10
+ module LongTerm
11
+ module GraphBased
12
+ module Storages
13
+ class ActiveRecordStorage < Base
14
+ def initialize
15
+ self.class.load_models!
16
+ end
17
+
18
+ def self.load_models!
19
+ return if @models_loaded
20
+ require "active_record"
21
+ require_relative "active_record_models"
22
+ @models_loaded = true
23
+ end
24
+
25
+ def save_node(user_id, node)
26
+ n = node.is_a?(Node) ? node : Node.from_h(node.to_h)
27
+ rec = if n.id
28
+ LlmemoryGraphNode.find_by(user_id: user_id, id: n.id)
29
+ else
30
+ LlmemoryGraphNode.find_by(user_id: user_id, entity_type: n.entity_type, name: n.name)
31
+ end
32
+ if rec
33
+ rec.update!(properties: n.properties || {}, updated_at: Time.current)
34
+ rec.id
35
+ else
36
+ id = n.id || "node_#{SecureRandom.hex(8)}"
37
+ LlmemoryGraphNode.create!(
38
+ id: id,
39
+ user_id: user_id,
40
+ entity_type: n.entity_type.to_s,
41
+ name: n.name.to_s,
42
+ properties: n.properties || {}
43
+ )
44
+ id
45
+ end
46
+ end
47
+
48
+ def find_node_by_id(user_id, id)
49
+ rec = LlmemoryGraphNode.find_by(user_id: user_id, id: id)
50
+ record_to_node(rec) if rec
51
+ end
52
+
53
+ def find_node_by_name(user_id, entity_type, name)
54
+ rec = LlmemoryGraphNode.find_by(user_id: user_id, entity_type: entity_type.to_s, name: name.to_s)
55
+ record_to_node(rec) if rec
56
+ end
57
+
58
+ def list_nodes(user_id)
59
+ LlmemoryGraphNode.where(user_id: user_id).map { |r| record_to_node(r) }
60
+ end
61
+
62
+ def save_edge(user_id, edge)
63
+ e = edge.is_a?(Edge) ? edge : Edge.from_h(edge.to_h)
64
+ id = e.id || "edge_#{SecureRandom.hex(8)}"
65
+ rec = LlmemoryGraphEdge.find_by(user_id: user_id, id: id)
66
+ if rec
67
+ rec.update!(
68
+ subject_id: e.subject_id,
69
+ predicate: e.predicate,
70
+ object_id: e.object_id,
71
+ properties: e.properties || {}
72
+ )
73
+ else
74
+ LlmemoryGraphEdge.create!(
75
+ id: id,
76
+ user_id: user_id,
77
+ subject_id: e.subject_id,
78
+ predicate: e.predicate,
79
+ object_id: e.object_id,
80
+ properties: e.properties || {}
81
+ )
82
+ end
83
+ id
84
+ end
85
+
86
+ def find_edges(user_id, subject_id: nil, predicate: nil, object_id: nil, include_archived: false)
87
+ scope = LlmemoryGraphEdge.where(user_id: user_id)
88
+ scope = scope.where(archived_at: nil) unless include_archived
89
+ scope = scope.where(subject_id: subject_id) if subject_id
90
+ scope = scope.where(predicate: predicate) if predicate
91
+ scope = scope.where(object_id: object_id) if object_id
92
+ scope.map { |r| record_to_edge(r) }
93
+ end
94
+
95
+ def archive_edge(user_id, edge_id, archived_at: nil)
96
+ rec = LlmemoryGraphEdge.find_by(user_id: user_id, id: edge_id)
97
+ return false unless rec
98
+ rec.update!(archived_at: archived_at || Time.current)
99
+ true
100
+ end
101
+
102
+ private
103
+
104
+ def record_to_node(r)
105
+ Node.new(
106
+ id: r.id,
107
+ user_id: r.user_id,
108
+ entity_type: r.entity_type,
109
+ name: r.name,
110
+ properties: r.properties || {},
111
+ created_at: r.created_at,
112
+ updated_at: r.updated_at
113
+ )
114
+ end
115
+
116
+ def record_to_edge(r)
117
+ Edge.new(
118
+ id: r.id,
119
+ user_id: r.user_id,
120
+ subject_id: r.subject_id,
121
+ predicate: r.predicate,
122
+ object_id: r.object_id,
123
+ properties: r.properties || {},
124
+ created_at: r.created_at,
125
+ archived_at: r.archived_at
126
+ )
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Llmemory
4
+ module LongTerm
5
+ module GraphBased
6
+ module Storages
7
+ class Base
8
+ def save_node(user_id, node)
9
+ raise NotImplementedError, "#{self.class}#save_node must be implemented"
10
+ end
11
+
12
+ def find_node_by_id(user_id, id)
13
+ raise NotImplementedError, "#{self.class}#find_node_by_id must be implemented"
14
+ end
15
+
16
+ def find_node_by_name(user_id, entity_type, name)
17
+ raise NotImplementedError, "#{self.class}#find_node_by_name must be implemented"
18
+ end
19
+
20
+ def list_nodes(user_id)
21
+ raise NotImplementedError, "#{self.class}#list_nodes must be implemented"
22
+ end
23
+
24
+ def save_edge(user_id, edge)
25
+ raise NotImplementedError, "#{self.class}#save_edge must be implemented"
26
+ end
27
+
28
+ def find_edges(user_id, subject_id: nil, predicate: nil, object_id: nil, include_archived: false)
29
+ raise NotImplementedError, "#{self.class}#find_edges must be implemented"
30
+ end
31
+
32
+ def archive_edge(user_id, edge_id, archived_at: nil)
33
+ raise NotImplementedError, "#{self.class}#archive_edge must be implemented"
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+ require_relative "base"
5
+ require_relative "../node"
6
+ require_relative "../edge"
7
+
8
+ module Llmemory
9
+ module LongTerm
10
+ module GraphBased
11
+ module Storages
12
+ class MemoryStorage < Base
13
+ def initialize
14
+ @nodes = Hash.new { |h, k| h[k] = {} }
15
+ @edges = Hash.new { |h, k| h[k] = [] }
16
+ @node_id_seq = 0
17
+ @edge_id_seq = 0
18
+ end
19
+
20
+ def save_node(user_id, node)
21
+ n = node.is_a?(Node) ? node : Node.from_h(node.to_h)
22
+ unless n.id
23
+ @node_id_seq += 1
24
+ n = Node.new(
25
+ id: "node_#{@node_id_seq}",
26
+ user_id: user_id,
27
+ entity_type: n.entity_type,
28
+ name: n.name,
29
+ properties: n.properties || {},
30
+ created_at: n.created_at || Time.now,
31
+ updated_at: Time.now
32
+ )
33
+ end
34
+ @nodes[user_id][n.id] = n
35
+ n.id
36
+ end
37
+
38
+ def find_node_by_id(user_id, id)
39
+ @nodes[user_id][id]
40
+ end
41
+
42
+ def find_node_by_name(user_id, entity_type, name)
43
+ @nodes[user_id].values.find { |n| n.entity_type == entity_type.to_s && n.name.to_s == name.to_s }
44
+ end
45
+
46
+ def list_nodes(user_id)
47
+ @nodes[user_id].values.to_a
48
+ end
49
+
50
+ def save_edge(user_id, edge)
51
+ e = edge.is_a?(Edge) ? edge : Edge.from_h(edge.to_h)
52
+ unless e.id
53
+ @edge_id_seq += 1
54
+ e = Edge.new(
55
+ id: "edge_#{@edge_id_seq}",
56
+ user_id: user_id,
57
+ subject_id: e.subject_id,
58
+ predicate: e.predicate,
59
+ object_id: e.object_id,
60
+ properties: e.properties || {},
61
+ created_at: e.created_at || Time.now,
62
+ archived_at: nil
63
+ )
64
+ end
65
+ idx = @edges[user_id].find_index { |x| x.id == e.id }
66
+ if idx
67
+ @edges[user_id][idx] = e
68
+ else
69
+ @edges[user_id] << e
70
+ end
71
+ e.id
72
+ end
73
+
74
+ def find_edges(user_id, subject_id: nil, predicate: nil, object_id: nil, include_archived: false)
75
+ list = @edges[user_id].select do |e|
76
+ next false unless include_archived || !e.archived?
77
+ next false if subject_id && e.subject_id != subject_id
78
+ next false if predicate && e.predicate != predicate.to_s
79
+ next false if object_id && e.object_id != object_id
80
+ true
81
+ end
82
+ list
83
+ end
84
+
85
+ def archive_edge(user_id, edge_id, archived_at: nil)
86
+ t = archived_at || Time.now
87
+ e = @edges[user_id].find { |x| x.id == edge_id }
88
+ return false unless e
89
+ @edges[user_id].delete(e)
90
+ @edges[user_id] << Edge.new(
91
+ id: e.id,
92
+ user_id: e.user_id,
93
+ subject_id: e.subject_id,
94
+ predicate: e.predicate,
95
+ object_id: e.object_id,
96
+ properties: e.properties,
97
+ created_at: e.created_at,
98
+ archived_at: t
99
+ )
100
+ true
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "graph_based/node"
4
+ require_relative "graph_based/edge"
5
+ require_relative "graph_based/storage"
6
+ require_relative "graph_based/knowledge_graph"
7
+ require_relative "graph_based/conflict_resolver"
8
+ require_relative "graph_based/memory"
9
+
10
+ module Llmemory
11
+ module LongTerm
12
+ module GraphBased
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "long_term/file_based"
4
+ require_relative "long_term/graph_based"
5
+
6
+ module Llmemory
7
+ module LongTerm
8
+ end
9
+ end