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.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +193 -0
- data/lib/generators/llmemory/install/install_generator.rb +24 -0
- data/lib/generators/llmemory/install/templates/create_llmemory_tables.rb +73 -0
- data/lib/llmemory/configuration.rb +51 -0
- data/lib/llmemory/extractors/entity_relation_extractor.rb +74 -0
- data/lib/llmemory/extractors/fact_extractor.rb +74 -0
- data/lib/llmemory/extractors.rb +9 -0
- data/lib/llmemory/llm/anthropic.rb +48 -0
- data/lib/llmemory/llm/base.rb +17 -0
- data/lib/llmemory/llm/openai.rb +46 -0
- data/lib/llmemory/llm.rb +18 -0
- data/lib/llmemory/long_term/file_based/category.rb +22 -0
- data/lib/llmemory/long_term/file_based/item.rb +31 -0
- data/lib/llmemory/long_term/file_based/memory.rb +83 -0
- data/lib/llmemory/long_term/file_based/resource.rb +22 -0
- data/lib/llmemory/long_term/file_based/retrieval.rb +90 -0
- data/lib/llmemory/long_term/file_based/storage.rb +35 -0
- data/lib/llmemory/long_term/file_based/storages/active_record_models.rb +26 -0
- data/lib/llmemory/long_term/file_based/storages/active_record_storage.rb +144 -0
- data/lib/llmemory/long_term/file_based/storages/base.rb +71 -0
- data/lib/llmemory/long_term/file_based/storages/database_storage.rb +231 -0
- data/lib/llmemory/long_term/file_based/storages/file_storage.rb +180 -0
- data/lib/llmemory/long_term/file_based/storages/memory_storage.rb +100 -0
- data/lib/llmemory/long_term/file_based.rb +15 -0
- data/lib/llmemory/long_term/graph_based/conflict_resolver.rb +33 -0
- data/lib/llmemory/long_term/graph_based/edge.rb +49 -0
- data/lib/llmemory/long_term/graph_based/knowledge_graph.rb +114 -0
- data/lib/llmemory/long_term/graph_based/memory.rb +143 -0
- data/lib/llmemory/long_term/graph_based/node.rb +42 -0
- data/lib/llmemory/long_term/graph_based/storage.rb +24 -0
- data/lib/llmemory/long_term/graph_based/storages/active_record_models.rb +23 -0
- data/lib/llmemory/long_term/graph_based/storages/active_record_storage.rb +132 -0
- data/lib/llmemory/long_term/graph_based/storages/base.rb +39 -0
- data/lib/llmemory/long_term/graph_based/storages/memory_storage.rb +106 -0
- data/lib/llmemory/long_term/graph_based.rb +15 -0
- data/lib/llmemory/long_term.rb +9 -0
- data/lib/llmemory/maintenance/consolidator.rb +55 -0
- data/lib/llmemory/maintenance/reindexer.rb +27 -0
- data/lib/llmemory/maintenance/runner.rb +34 -0
- data/lib/llmemory/maintenance/summarizer.rb +57 -0
- data/lib/llmemory/maintenance.rb +8 -0
- data/lib/llmemory/memory.rb +96 -0
- data/lib/llmemory/retrieval/context_assembler.rb +53 -0
- data/lib/llmemory/retrieval/engine.rb +74 -0
- data/lib/llmemory/retrieval/temporal_ranker.rb +23 -0
- data/lib/llmemory/retrieval.rb +10 -0
- data/lib/llmemory/short_term/checkpoint.rb +47 -0
- data/lib/llmemory/short_term/stores/active_record_checkpoint.rb +14 -0
- data/lib/llmemory/short_term/stores/active_record_store.rb +58 -0
- data/lib/llmemory/short_term/stores/base.rb +21 -0
- data/lib/llmemory/short_term/stores/memory_store.rb +37 -0
- data/lib/llmemory/short_term/stores/postgres_store.rb +80 -0
- data/lib/llmemory/short_term/stores/redis_store.rb +54 -0
- data/lib/llmemory/short_term.rb +8 -0
- data/lib/llmemory/vector_store/base.rb +19 -0
- data/lib/llmemory/vector_store/memory_store.rb +53 -0
- data/lib/llmemory/vector_store/openai_embeddings.rb +49 -0
- data/lib/llmemory/vector_store.rb +10 -0
- data/lib/llmemory/version.rb +5 -0
- data/lib/llmemory.rb +19 -0
- 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
|