llmemory 0.2.2 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +31 -0
- data/lib/llmemory/configuration.rb +22 -2
- data/lib/llmemory/crypto/cipher.rb +147 -0
- data/lib/llmemory/crypto/field_helpers.rb +110 -0
- data/lib/llmemory/llm/anthropic.rb +2 -1
- data/lib/llmemory/llm/openai.rb +2 -1
- data/lib/llmemory/long_term/episodic/memory.rb +4 -3
- data/lib/llmemory/long_term/episodic/storage.rb +11 -4
- data/lib/llmemory/long_term/episodic/storages/active_record_storage.rb +19 -6
- data/lib/llmemory/long_term/episodic/storages/database_storage.rb +25 -3
- data/lib/llmemory/long_term/episodic/storages/file_storage.rb +22 -5
- data/lib/llmemory/long_term/file_based/storage.rb +11 -4
- data/lib/llmemory/long_term/file_based/storages/active_record_storage.rb +16 -10
- data/lib/llmemory/long_term/file_based/storages/database_storage.rb +24 -8
- data/lib/llmemory/long_term/file_based/storages/file_storage.rb +28 -14
- data/lib/llmemory/long_term/graph_based/memory.rb +4 -3
- data/lib/llmemory/long_term/graph_based/storage.rb +3 -2
- data/lib/llmemory/long_term/graph_based/storages/active_record_storage.rb +47 -21
- data/lib/llmemory/long_term/procedural/memory.rb +4 -3
- data/lib/llmemory/long_term/procedural/storage.rb +11 -4
- data/lib/llmemory/long_term/procedural/storages/active_record_storage.rb +33 -13
- data/lib/llmemory/long_term/procedural/storages/database_storage.rb +25 -4
- data/lib/llmemory/long_term/procedural/storages/file_storage.rb +23 -6
- data/lib/llmemory/memory.rb +40 -8
- data/lib/llmemory/short_term/checkpoint.rb +3 -2
- data/lib/llmemory/short_term/stores/active_record_store.rb +12 -10
- data/lib/llmemory/short_term/stores/memory_store.rb +1 -1
- data/lib/llmemory/short_term/stores/postgres_store.rb +11 -5
- data/lib/llmemory/short_term/stores/redis_store.rb +7 -5
- data/lib/llmemory/short_term/stores.rb +7 -6
- data/lib/llmemory/vector_store/active_record_store.rb +24 -3
- data/lib/llmemory/vector_store/memory_store.rb +23 -3
- data/lib/llmemory/vector_store.rb +4 -3
- data/lib/llmemory/version.rb +1 -1
- data/lib/llmemory.rb +2 -0
- metadata +3 -1
|
@@ -2,13 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
require "securerandom"
|
|
4
4
|
require_relative "base"
|
|
5
|
+
require_relative "../../../crypto/field_helpers"
|
|
5
6
|
|
|
6
7
|
module Llmemory
|
|
7
8
|
module LongTerm
|
|
8
9
|
module FileBased
|
|
9
10
|
module Storages
|
|
10
11
|
class ActiveRecordStorage < Base
|
|
11
|
-
|
|
12
|
+
include Llmemory::Crypto::FieldHelpers
|
|
13
|
+
|
|
14
|
+
def initialize(cipher: nil)
|
|
15
|
+
@cipher = cipher || Llmemory.build_cipher
|
|
12
16
|
self.class.load_models!
|
|
13
17
|
end
|
|
14
18
|
|
|
@@ -24,7 +28,7 @@ module Llmemory
|
|
|
24
28
|
LlmemoryResource.create!(
|
|
25
29
|
id: id,
|
|
26
30
|
user_id: user_id,
|
|
27
|
-
text: text,
|
|
31
|
+
text: enc(text),
|
|
28
32
|
created_at: Time.current
|
|
29
33
|
)
|
|
30
34
|
id
|
|
@@ -36,24 +40,26 @@ module Llmemory
|
|
|
36
40
|
id: id,
|
|
37
41
|
user_id: user_id,
|
|
38
42
|
category: category,
|
|
39
|
-
content: content,
|
|
43
|
+
content: enc(content),
|
|
40
44
|
source_resource_id: source_resource_id,
|
|
41
45
|
created_at: Time.current
|
|
42
46
|
}
|
|
43
47
|
attrs[:importance] = importance if LlmemoryItem.column_names.include?("importance")
|
|
44
|
-
|
|
48
|
+
if provenance && LlmemoryItem.column_names.include?("provenance")
|
|
49
|
+
attrs[:provenance] = cipher.enabled? ? enc_json(provenance) : provenance
|
|
50
|
+
end
|
|
45
51
|
LlmemoryItem.create!(attrs)
|
|
46
52
|
id
|
|
47
53
|
end
|
|
48
54
|
|
|
49
55
|
def load_category(user_id, category_name)
|
|
50
56
|
rec = LlmemoryCategory.find_by(user_id: user_id, category_name: category_name)
|
|
51
|
-
rec ? rec.content.to_s : ""
|
|
57
|
+
rec ? dec(rec.content.to_s) : ""
|
|
52
58
|
end
|
|
53
59
|
|
|
54
60
|
def save_category(user_id, category_name, content)
|
|
55
61
|
rec = LlmemoryCategory.find_or_initialize_by(user_id: user_id, category_name: category_name)
|
|
56
|
-
rec.content = content
|
|
62
|
+
rec.content = enc(content)
|
|
57
63
|
rec.updated_at = Time.current
|
|
58
64
|
rec.save!
|
|
59
65
|
true
|
|
@@ -101,7 +107,7 @@ module Llmemory
|
|
|
101
107
|
id: "item_#{SecureRandom.hex(8)}",
|
|
102
108
|
user_id: user_id,
|
|
103
109
|
category: merged_item[:category],
|
|
104
|
-
content: merged_item[:content],
|
|
110
|
+
content: enc(merged_item[:content]),
|
|
105
111
|
source_resource_id: merged_item[:source_resource_id],
|
|
106
112
|
created_at: created_at
|
|
107
113
|
}
|
|
@@ -194,19 +200,19 @@ module Llmemory
|
|
|
194
200
|
h = {
|
|
195
201
|
id: r.id,
|
|
196
202
|
category: r.category,
|
|
197
|
-
content: r.content,
|
|
203
|
+
content: dec(r.content),
|
|
198
204
|
source_resource_id: r.source_resource_id,
|
|
199
205
|
created_at: r.created_at
|
|
200
206
|
}
|
|
201
207
|
h[:importance] = r.respond_to?(:importance) ? (r.importance || 0.7).to_f : 0.7
|
|
202
|
-
h[:provenance] = r.provenance if r.respond_to?(:provenance)
|
|
208
|
+
h[:provenance] = parse_provenance(r.provenance) if r.respond_to?(:provenance)
|
|
203
209
|
h
|
|
204
210
|
end
|
|
205
211
|
|
|
206
212
|
def row_to_resource(r)
|
|
207
213
|
{
|
|
208
214
|
id: r.id,
|
|
209
|
-
text: r.text,
|
|
215
|
+
text: dec(r.text),
|
|
210
216
|
created_at: r.created_at
|
|
211
217
|
}
|
|
212
218
|
end
|
|
@@ -3,15 +3,19 @@
|
|
|
3
3
|
require "json"
|
|
4
4
|
require "securerandom"
|
|
5
5
|
require_relative "base"
|
|
6
|
+
require_relative "../../../crypto/field_helpers"
|
|
6
7
|
|
|
7
8
|
module Llmemory
|
|
8
9
|
module LongTerm
|
|
9
10
|
module FileBased
|
|
10
11
|
module Storages
|
|
11
12
|
class DatabaseStorage < Base
|
|
12
|
-
|
|
13
|
+
include Llmemory::Crypto::FieldHelpers
|
|
14
|
+
|
|
15
|
+
def initialize(database_url: nil, cipher: nil)
|
|
13
16
|
@database_url = database_url || Llmemory.configuration.database_url
|
|
14
17
|
@connection = nil
|
|
18
|
+
@cipher = cipher || Llmemory.build_cipher
|
|
15
19
|
end
|
|
16
20
|
|
|
17
21
|
def save_resource(user_id, text)
|
|
@@ -19,7 +23,7 @@ module Llmemory
|
|
|
19
23
|
id = "res_#{SecureRandom.hex(8)}"
|
|
20
24
|
conn.exec_params(
|
|
21
25
|
"INSERT INTO llmemory_resources (id, user_id, text, created_at) VALUES ($1, $2, $3, $4)",
|
|
22
|
-
[id, user_id, text, Time.now.utc.iso8601]
|
|
26
|
+
[id, user_id, enc(text), Time.now.utc.iso8601]
|
|
23
27
|
)
|
|
24
28
|
id
|
|
25
29
|
end
|
|
@@ -29,7 +33,7 @@ module Llmemory
|
|
|
29
33
|
id = "item_#{SecureRandom.hex(8)}"
|
|
30
34
|
conn.exec_params(
|
|
31
35
|
"INSERT INTO llmemory_items (id, user_id, category, content, source_resource_id, importance, provenance, created_at) VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb, $8)",
|
|
32
|
-
[id, user_id, category, content, source_resource_id, importance.to_f,
|
|
36
|
+
[id, user_id, category, enc(content), source_resource_id, importance.to_f, provenance_json(provenance), Time.now.utc.iso8601]
|
|
33
37
|
)
|
|
34
38
|
id
|
|
35
39
|
end
|
|
@@ -40,7 +44,7 @@ module Llmemory
|
|
|
40
44
|
"SELECT content FROM llmemory_categories WHERE user_id = $1 AND category_name = $2",
|
|
41
45
|
[user_id, category_name]
|
|
42
46
|
)
|
|
43
|
-
result.any? ? result.first["content"].to_s : ""
|
|
47
|
+
result.any? ? dec(result.first["content"].to_s) : ""
|
|
44
48
|
end
|
|
45
49
|
|
|
46
50
|
def save_category(user_id, category_name, content)
|
|
@@ -52,7 +56,7 @@ module Llmemory
|
|
|
52
56
|
ON CONFLICT (user_id, category_name)
|
|
53
57
|
DO UPDATE SET content = $3, updated_at = $4
|
|
54
58
|
SQL
|
|
55
|
-
[user_id, category_name, content, Time.now.utc.iso8601]
|
|
59
|
+
[user_id, category_name, enc(content), Time.now.utc.iso8601]
|
|
56
60
|
)
|
|
57
61
|
true
|
|
58
62
|
end
|
|
@@ -145,7 +149,7 @@ module Llmemory
|
|
|
145
149
|
id,
|
|
146
150
|
user_id,
|
|
147
151
|
merged_item[:category],
|
|
148
|
-
merged_item[:content],
|
|
152
|
+
enc(merged_item[:content]),
|
|
149
153
|
merged_item[:source_resource_id],
|
|
150
154
|
created_at
|
|
151
155
|
]
|
|
@@ -283,7 +287,7 @@ module Llmemory
|
|
|
283
287
|
{
|
|
284
288
|
id: r["id"],
|
|
285
289
|
category: r["category"],
|
|
286
|
-
content: r["content"],
|
|
290
|
+
content: dec(r["content"]),
|
|
287
291
|
source_resource_id: r["source_resource_id"],
|
|
288
292
|
importance: (r["importance"] || 0.7).to_f,
|
|
289
293
|
provenance: parse_provenance(r["provenance"]),
|
|
@@ -304,16 +308,28 @@ module Llmemory
|
|
|
304
308
|
|
|
305
309
|
def parse_provenance(value)
|
|
306
310
|
return nil if value.nil? || value.to_s.strip.empty?
|
|
311
|
+
return value.transform_keys(&:to_sym) if value.is_a?(Hash)
|
|
312
|
+
return dec_json(value) if value.is_a?(String) && cipher.encrypted?(value)
|
|
313
|
+
|
|
307
314
|
JSON.parse(value, symbolize_names: true)
|
|
308
315
|
rescue JSON::ParserError
|
|
309
316
|
nil
|
|
310
317
|
end
|
|
311
318
|
|
|
319
|
+
def provenance_json(provenance)
|
|
320
|
+
return nil unless provenance
|
|
321
|
+
if cipher.enabled?
|
|
322
|
+
JSON.generate(enc_json(provenance))
|
|
323
|
+
else
|
|
324
|
+
JSON.generate(provenance)
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
|
|
312
328
|
def rows_to_resources(rows)
|
|
313
329
|
rows.map do |r|
|
|
314
330
|
{
|
|
315
331
|
id: r["id"],
|
|
316
|
-
text: r["text"],
|
|
332
|
+
text: dec(r["text"]),
|
|
317
333
|
created_at: Time.parse(r["created_at"])
|
|
318
334
|
}
|
|
319
335
|
end
|
|
@@ -3,15 +3,19 @@
|
|
|
3
3
|
require "fileutils"
|
|
4
4
|
require "json"
|
|
5
5
|
require_relative "base"
|
|
6
|
+
require_relative "../../../crypto/field_helpers"
|
|
6
7
|
|
|
7
8
|
module Llmemory
|
|
8
9
|
module LongTerm
|
|
9
10
|
module FileBased
|
|
10
11
|
module Storages
|
|
11
12
|
class FileStorage < Base
|
|
12
|
-
|
|
13
|
+
include Llmemory::Crypto::FieldHelpers
|
|
14
|
+
|
|
15
|
+
def initialize(base_path: nil, cipher: nil)
|
|
13
16
|
@base_path = base_path || Llmemory.configuration.long_term_storage_path || "./llmemory_data"
|
|
14
17
|
@base_path = File.expand_path(@base_path)
|
|
18
|
+
@cipher = cipher || Llmemory.build_cipher
|
|
15
19
|
end
|
|
16
20
|
|
|
17
21
|
def save_resource(user_id, text)
|
|
@@ -19,8 +23,8 @@ module Llmemory
|
|
|
19
23
|
seq = next_seq(user_id, "resource_id_seq")
|
|
20
24
|
id = "res_#{seq}"
|
|
21
25
|
path = resource_path(user_id, id)
|
|
22
|
-
data = { text: text, created_at: Time.now.iso8601 }
|
|
23
|
-
|
|
26
|
+
data = { text: enc(text), created_at: Time.now.iso8601 }
|
|
27
|
+
write_encrypted_file(path, data)
|
|
24
28
|
id
|
|
25
29
|
end
|
|
26
30
|
|
|
@@ -32,26 +36,26 @@ module Llmemory
|
|
|
32
36
|
data = {
|
|
33
37
|
id: id,
|
|
34
38
|
category: category,
|
|
35
|
-
content: content,
|
|
39
|
+
content: enc(content),
|
|
36
40
|
source_resource_id: source_resource_id,
|
|
37
41
|
importance: importance,
|
|
38
|
-
provenance: provenance,
|
|
42
|
+
provenance: provenance ? enc_json(provenance) : nil,
|
|
39
43
|
created_at: Time.now.iso8601
|
|
40
44
|
}
|
|
41
|
-
|
|
45
|
+
write_encrypted_file(path, data)
|
|
42
46
|
id
|
|
43
47
|
end
|
|
44
48
|
|
|
45
49
|
def load_category(user_id, category_name)
|
|
46
50
|
path = category_path(user_id, category_name)
|
|
47
51
|
return "" unless File.file?(path)
|
|
48
|
-
|
|
52
|
+
read_encrypted_text_file(path)
|
|
49
53
|
end
|
|
50
54
|
|
|
51
55
|
def save_category(user_id, category_name, content)
|
|
52
56
|
ensure_user_dir(user_id, "categories")
|
|
53
57
|
path = category_path(user_id, category_name)
|
|
54
|
-
|
|
58
|
+
write_encrypted_text_file(path, content)
|
|
55
59
|
true
|
|
56
60
|
end
|
|
57
61
|
|
|
@@ -83,7 +87,8 @@ module Llmemory
|
|
|
83
87
|
dir = user_path(user_id, "items")
|
|
84
88
|
return [] unless Dir.exist?(dir)
|
|
85
89
|
Dir.children(dir).select { |f| f.end_with?(".json") }.map do |f|
|
|
86
|
-
data =
|
|
90
|
+
data = read_encrypted_file(File.join(dir, f))
|
|
91
|
+
data = decrypt_item(data)
|
|
87
92
|
data[:created_at] = parse_time(data[:created_at])
|
|
88
93
|
data
|
|
89
94
|
end.sort_by { |i| i[:created_at] }
|
|
@@ -93,9 +98,10 @@ module Llmemory
|
|
|
93
98
|
dir = user_path(user_id, "resources")
|
|
94
99
|
return [] unless Dir.exist?(dir)
|
|
95
100
|
Dir.children(dir).select { |f| f.end_with?(".json") }.map do |f|
|
|
96
|
-
data =
|
|
101
|
+
data = read_encrypted_file(File.join(dir, f))
|
|
97
102
|
id = File.basename(f, ".json")
|
|
98
103
|
data[:id] = id
|
|
104
|
+
data[:text] = dec(data[:text] || data["text"])
|
|
99
105
|
data[:created_at] = parse_time(data[:created_at])
|
|
100
106
|
data
|
|
101
107
|
end.sort_by { |r| r[:created_at] }
|
|
@@ -113,7 +119,9 @@ module Llmemory
|
|
|
113
119
|
id = "item_#{seq}"
|
|
114
120
|
path = item_path(user_id, id)
|
|
115
121
|
data = merged_item.merge(id: id).transform_values { |v| v.respond_to?(:iso8601) ? v.iso8601 : v }
|
|
116
|
-
|
|
122
|
+
data[:content] = enc(data[:content]) if data[:content]
|
|
123
|
+
data[:provenance] = enc_json(data[:provenance]) if data[:provenance]
|
|
124
|
+
write_encrypted_file(path, data)
|
|
117
125
|
end
|
|
118
126
|
|
|
119
127
|
def archive_items(user_id, item_ids)
|
|
@@ -127,9 +135,8 @@ module Llmemory
|
|
|
127
135
|
def save_daily_log_entry(user_id, date, content)
|
|
128
136
|
ensure_user_dir(user_id, "memory")
|
|
129
137
|
path = daily_log_path(user_id, date)
|
|
130
|
-
existing = File.file?(path) ? File.read(path) : ""
|
|
131
138
|
entry = "#{Time.now.strftime('%H:%M')} #{content}\n"
|
|
132
|
-
|
|
139
|
+
write_encrypted_text_file(path, entry, append: File.file?(path))
|
|
133
140
|
true
|
|
134
141
|
end
|
|
135
142
|
|
|
@@ -143,7 +150,7 @@ module Llmemory
|
|
|
143
150
|
path = daily_log_path(user_id, d)
|
|
144
151
|
next unless File.file?(path)
|
|
145
152
|
|
|
146
|
-
{ date: d, content:
|
|
153
|
+
{ date: d, content: read_encrypted_text_file(path) }
|
|
147
154
|
end
|
|
148
155
|
end
|
|
149
156
|
|
|
@@ -224,6 +231,13 @@ module Llmemory
|
|
|
224
231
|
return Time.parse(val.to_s) if val
|
|
225
232
|
Time.now
|
|
226
233
|
end
|
|
234
|
+
|
|
235
|
+
def decrypt_item(data)
|
|
236
|
+
data[:content] = dec(data[:content] || data["content"])
|
|
237
|
+
prov = data[:provenance] || data["provenance"]
|
|
238
|
+
data[:provenance] = parse_provenance(prov) if prov
|
|
239
|
+
data
|
|
240
|
+
end
|
|
227
241
|
end
|
|
228
242
|
end
|
|
229
243
|
end
|
|
@@ -14,9 +14,10 @@ module Llmemory
|
|
|
14
14
|
class Memory
|
|
15
15
|
include Llmemory::MemoryModule
|
|
16
16
|
|
|
17
|
-
def initialize(user_id:, storage: nil, vector_store: nil, llm: nil, extractor: nil)
|
|
17
|
+
def initialize(user_id:, storage: nil, vector_store: nil, llm: nil, extractor: nil, cipher: nil)
|
|
18
18
|
@user_id = user_id
|
|
19
|
-
@
|
|
19
|
+
@cipher = cipher || Llmemory.build_cipher
|
|
20
|
+
@graph_storage = storage || Storages.build(cipher: @cipher)
|
|
20
21
|
@kg = KnowledgeGraph.new(user_id: user_id, storage: @graph_storage)
|
|
21
22
|
@conflict_resolver = ConflictResolver.new(@kg)
|
|
22
23
|
@vector_store = vector_store || build_vector_store
|
|
@@ -112,7 +113,7 @@ module Llmemory
|
|
|
112
113
|
private
|
|
113
114
|
|
|
114
115
|
def build_vector_store
|
|
115
|
-
Llmemory::VectorStore.build(source_type: "edge")
|
|
116
|
+
Llmemory::VectorStore.build(source_type: "edge", cipher: @cipher)
|
|
116
117
|
end
|
|
117
118
|
|
|
118
119
|
def extract_graph(text)
|
|
@@ -7,13 +7,14 @@ module Llmemory
|
|
|
7
7
|
module LongTerm
|
|
8
8
|
module GraphBased
|
|
9
9
|
module Storages
|
|
10
|
-
def self.build(store: nil)
|
|
10
|
+
def self.build(store: nil, cipher: nil)
|
|
11
|
+
resolved_cipher = cipher || Llmemory.build_cipher
|
|
11
12
|
case (store || Llmemory.configuration.long_term_store).to_s.to_sym
|
|
12
13
|
when :memory
|
|
13
14
|
MemoryStorage.new
|
|
14
15
|
when :active_record, :activerecord
|
|
15
16
|
require_relative "storages/active_record_storage"
|
|
16
|
-
ActiveRecordStorage.new
|
|
17
|
+
ActiveRecordStorage.new(cipher: resolved_cipher)
|
|
17
18
|
else
|
|
18
19
|
MemoryStorage.new
|
|
19
20
|
end
|
|
@@ -4,13 +4,17 @@ require_relative "base"
|
|
|
4
4
|
require_relative "active_record_models"
|
|
5
5
|
require_relative "../node"
|
|
6
6
|
require_relative "../edge"
|
|
7
|
+
require_relative "../../../crypto/field_helpers"
|
|
7
8
|
|
|
8
9
|
module Llmemory
|
|
9
10
|
module LongTerm
|
|
10
11
|
module GraphBased
|
|
11
12
|
module Storages
|
|
12
13
|
class ActiveRecordStorage < Base
|
|
13
|
-
|
|
14
|
+
include Llmemory::Crypto::FieldHelpers
|
|
15
|
+
|
|
16
|
+
def initialize(cipher: nil)
|
|
17
|
+
@cipher = cipher || Llmemory.build_cipher
|
|
14
18
|
self.class.load_models!
|
|
15
19
|
end
|
|
16
20
|
|
|
@@ -26,17 +30,22 @@ module Llmemory
|
|
|
26
30
|
rec = if n.id
|
|
27
31
|
LlmemoryGraphNode.find_by(user_id: user_id, id: n.id)
|
|
28
32
|
else
|
|
29
|
-
LlmemoryGraphNode.find_by(
|
|
33
|
+
LlmemoryGraphNode.find_by(
|
|
34
|
+
user_id: user_id,
|
|
35
|
+
entity_type: enc_det(n.entity_type),
|
|
36
|
+
name: enc_det(n.name)
|
|
37
|
+
)
|
|
30
38
|
end
|
|
39
|
+
stored_props = store_properties(n.properties || {})
|
|
31
40
|
if rec
|
|
32
|
-
rec.update!(properties:
|
|
41
|
+
rec.update!(properties: stored_props, updated_at: Time.current)
|
|
33
42
|
rec.id
|
|
34
43
|
else
|
|
35
44
|
rec = LlmemoryGraphNode.create!(
|
|
36
45
|
user_id: user_id,
|
|
37
|
-
entity_type: n.entity_type.to_s,
|
|
38
|
-
name: n.name.to_s,
|
|
39
|
-
properties:
|
|
46
|
+
entity_type: enc_det(n.entity_type.to_s),
|
|
47
|
+
name: enc_det(n.name.to_s),
|
|
48
|
+
properties: stored_props
|
|
40
49
|
)
|
|
41
50
|
rec.id
|
|
42
51
|
end
|
|
@@ -48,13 +57,17 @@ module Llmemory
|
|
|
48
57
|
end
|
|
49
58
|
|
|
50
59
|
def find_node_by_name(user_id, entity_type, name)
|
|
51
|
-
rec = LlmemoryGraphNode.find_by(
|
|
60
|
+
rec = LlmemoryGraphNode.find_by(
|
|
61
|
+
user_id: user_id,
|
|
62
|
+
entity_type: enc_det(entity_type.to_s),
|
|
63
|
+
name: enc_det(name.to_s)
|
|
64
|
+
)
|
|
52
65
|
record_to_node(rec) if rec
|
|
53
66
|
end
|
|
54
67
|
|
|
55
68
|
def list_nodes(user_id, entity_type: nil, limit: nil, offset: nil)
|
|
56
69
|
scope = LlmemoryGraphNode.where(user_id: user_id)
|
|
57
|
-
scope = scope.where(entity_type: entity_type) if entity_type
|
|
70
|
+
scope = scope.where(entity_type: enc_det(entity_type.to_s)) if entity_type
|
|
58
71
|
scope = scope.limit(limit) if limit && limit.to_i.positive?
|
|
59
72
|
scope = scope.offset(offset) if offset && offset.to_i.positive?
|
|
60
73
|
scope.map { |r| record_to_node(r) }
|
|
@@ -64,24 +77,23 @@ module Llmemory
|
|
|
64
77
|
e = edge.is_a?(Edge) ? edge : Edge.from_h(edge.to_h)
|
|
65
78
|
rec = if e.id && e.id.is_a?(Integer)
|
|
66
79
|
LlmemoryGraphEdge.find_by(user_id: user_id, id: e.id)
|
|
67
|
-
else
|
|
68
|
-
nil
|
|
69
80
|
end
|
|
81
|
+
stored_props = store_properties(e.properties || {})
|
|
70
82
|
if rec
|
|
71
83
|
rec.update!(
|
|
72
84
|
subject_id: e.subject_id,
|
|
73
|
-
predicate: e.predicate,
|
|
85
|
+
predicate: enc_det(e.predicate),
|
|
74
86
|
object_id: e.target_id,
|
|
75
|
-
properties:
|
|
87
|
+
properties: stored_props
|
|
76
88
|
)
|
|
77
89
|
rec.id
|
|
78
90
|
else
|
|
79
91
|
rec = LlmemoryGraphEdge.create!(
|
|
80
92
|
user_id: user_id,
|
|
81
93
|
subject_id: e.subject_id,
|
|
82
|
-
predicate: e.predicate,
|
|
94
|
+
predicate: enc_det(e.predicate),
|
|
83
95
|
object_id: e.target_id,
|
|
84
|
-
properties:
|
|
96
|
+
properties: stored_props
|
|
85
97
|
)
|
|
86
98
|
rec.id
|
|
87
99
|
end
|
|
@@ -91,7 +103,7 @@ module Llmemory
|
|
|
91
103
|
scope = LlmemoryGraphEdge.where(user_id: user_id)
|
|
92
104
|
scope = scope.where(archived_at: nil) unless include_archived
|
|
93
105
|
scope = scope.where(subject_id: subject_id) if subject_id
|
|
94
|
-
scope = scope.where(predicate: predicate) if predicate
|
|
106
|
+
scope = scope.where(predicate: enc_det(predicate.to_s)) if predicate
|
|
95
107
|
scope = scope.where(object_id: object_id) if object_id
|
|
96
108
|
scope.map { |r| record_to_edge(r) }
|
|
97
109
|
end
|
|
@@ -110,7 +122,7 @@ module Llmemory
|
|
|
110
122
|
def list_edges(user_id, subject_id: nil, predicate: nil, limit: nil, offset: nil)
|
|
111
123
|
scope = LlmemoryGraphEdge.where(user_id: user_id, archived_at: nil)
|
|
112
124
|
scope = scope.where(subject_id: subject_id) if subject_id
|
|
113
|
-
scope = scope.where(predicate: predicate) if predicate
|
|
125
|
+
scope = scope.where(predicate: enc_det(predicate.to_s)) if predicate
|
|
114
126
|
scope = scope.order(created_at: :desc) if limit && limit.to_i.positive?
|
|
115
127
|
scope = scope.limit(limit) if limit && limit.to_i.positive?
|
|
116
128
|
scope = scope.offset(offset) if offset && offset.to_i.positive?
|
|
@@ -153,13 +165,27 @@ module Llmemory
|
|
|
153
165
|
|
|
154
166
|
private
|
|
155
167
|
|
|
168
|
+
def store_properties(props)
|
|
169
|
+
return props || {} unless cipher.enabled?
|
|
170
|
+
return {} if props.nil? || props.empty?
|
|
171
|
+
|
|
172
|
+
enc_json(props)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def load_properties(raw)
|
|
176
|
+
return raw || {} if raw.nil? || raw == {}
|
|
177
|
+
return dec_json(raw) if raw.is_a?(String) && cipher.encrypted?(raw)
|
|
178
|
+
|
|
179
|
+
raw
|
|
180
|
+
end
|
|
181
|
+
|
|
156
182
|
def record_to_node(r)
|
|
157
183
|
Node.new(
|
|
158
184
|
id: r.id,
|
|
159
185
|
user_id: r.user_id,
|
|
160
|
-
entity_type: r.entity_type,
|
|
161
|
-
name: r.name,
|
|
162
|
-
properties: r.properties
|
|
186
|
+
entity_type: dec(r.entity_type),
|
|
187
|
+
name: dec(r.name),
|
|
188
|
+
properties: load_properties(r.properties),
|
|
163
189
|
created_at: r.created_at,
|
|
164
190
|
updated_at: r.updated_at
|
|
165
191
|
)
|
|
@@ -170,9 +196,9 @@ module Llmemory
|
|
|
170
196
|
id: r.id,
|
|
171
197
|
user_id: r.user_id,
|
|
172
198
|
subject_id: r.subject_id,
|
|
173
|
-
predicate: r.predicate,
|
|
199
|
+
predicate: dec(r.predicate),
|
|
174
200
|
target_id: r.object_id,
|
|
175
|
-
properties: r.properties
|
|
201
|
+
properties: load_properties(r.properties),
|
|
176
202
|
created_at: r.created_at,
|
|
177
203
|
archived_at: r.archived_at
|
|
178
204
|
)
|
|
@@ -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)
|
|
25
|
+
def initialize(user_id:, storage: nil, vector_store: nil, cipher: nil)
|
|
26
26
|
@user_id = user_id
|
|
27
|
-
@
|
|
27
|
+
@cipher = cipher || Llmemory.build_cipher
|
|
28
|
+
@storage = storage || Storages.build(cipher: @cipher)
|
|
28
29
|
@vector_store = vector_store
|
|
29
30
|
@vector_explicit = !vector_store.nil?
|
|
30
31
|
end
|
|
@@ -131,7 +132,7 @@ module Llmemory
|
|
|
131
132
|
if @vector_explicit
|
|
132
133
|
@vector_store
|
|
133
134
|
elsif Llmemory.configuration.procedural_vector_enabled
|
|
134
|
-
@vector_store ||= Llmemory::VectorStore.build(source_type: "skill")
|
|
135
|
+
@vector_store ||= Llmemory::VectorStore.build(source_type: "skill", cipher: @cipher)
|
|
135
136
|
end
|
|
136
137
|
end
|
|
137
138
|
|
|
@@ -12,17 +12,24 @@ module Llmemory
|
|
|
12
12
|
Storage = Storages::MemoryStorage
|
|
13
13
|
|
|
14
14
|
module Storages
|
|
15
|
-
def self.build(store: nil, base_path: nil, database_url: nil)
|
|
15
|
+
def self.build(store: nil, base_path: nil, database_url: nil, cipher: nil)
|
|
16
|
+
resolved_cipher = cipher || Llmemory.build_cipher
|
|
16
17
|
case (store || Llmemory.configuration.long_term_store).to_s.to_sym
|
|
17
18
|
when :memory
|
|
18
19
|
MemoryStorage.new
|
|
19
20
|
when :file
|
|
20
|
-
FileStorage.new(
|
|
21
|
+
FileStorage.new(
|
|
22
|
+
base_path: base_path || Llmemory.configuration.long_term_storage_path,
|
|
23
|
+
cipher: resolved_cipher
|
|
24
|
+
)
|
|
21
25
|
when :postgres, :database
|
|
22
|
-
DatabaseStorage.new(
|
|
26
|
+
DatabaseStorage.new(
|
|
27
|
+
database_url: database_url || Llmemory.configuration.database_url,
|
|
28
|
+
cipher: resolved_cipher
|
|
29
|
+
)
|
|
23
30
|
when :active_record, :activerecord
|
|
24
31
|
require_relative "storages/active_record_storage"
|
|
25
|
-
ActiveRecordStorage.new
|
|
32
|
+
ActiveRecordStorage.new(cipher: resolved_cipher)
|
|
26
33
|
else
|
|
27
34
|
MemoryStorage.new
|
|
28
35
|
end
|
|
@@ -4,6 +4,7 @@ require "json"
|
|
|
4
4
|
require "securerandom"
|
|
5
5
|
require "time"
|
|
6
6
|
require_relative "base"
|
|
7
|
+
require_relative "../../../crypto/field_helpers"
|
|
7
8
|
|
|
8
9
|
module Llmemory
|
|
9
10
|
module LongTerm
|
|
@@ -13,7 +14,10 @@ module Llmemory
|
|
|
13
14
|
# auto-deserializes jsonb to a Hash (string keys), which Skill.from_h
|
|
14
15
|
# handles. Mirrors the file-based ActiveRecordStorage pattern.
|
|
15
16
|
class ActiveRecordStorage < Base
|
|
16
|
-
|
|
17
|
+
include Llmemory::Crypto::FieldHelpers
|
|
18
|
+
|
|
19
|
+
def initialize(cipher: nil)
|
|
20
|
+
@cipher = cipher || Llmemory.build_cipher
|
|
17
21
|
self.class.load_models!
|
|
18
22
|
end
|
|
19
23
|
|
|
@@ -30,42 +34,50 @@ module Llmemory
|
|
|
30
34
|
data["created_at"] ||= Time.now.utc.iso8601
|
|
31
35
|
rec = LlmemorySkill.find_or_initialize_by(id: id)
|
|
32
36
|
rec.user_id = user_id
|
|
33
|
-
rec.data = data
|
|
34
|
-
rec.search_text = searchable_text(data)
|
|
37
|
+
rec.data = cipher.enabled? ? enc_json(data) : data
|
|
38
|
+
rec.search_text = enc(searchable_text(data))
|
|
35
39
|
rec.created_at ||= Time.current
|
|
36
40
|
rec.save!
|
|
37
41
|
id
|
|
38
42
|
end
|
|
39
43
|
|
|
40
44
|
def get_skill(user_id, id)
|
|
41
|
-
LlmemorySkill.find_by(user_id: user_id, id: id)
|
|
45
|
+
rec = LlmemorySkill.find_by(user_id: user_id, id: id)
|
|
46
|
+
return nil unless rec
|
|
47
|
+
|
|
48
|
+
decode_data(rec.data)
|
|
42
49
|
end
|
|
43
50
|
|
|
44
51
|
def list_skills(user_id, limit: nil, offset: nil)
|
|
45
52
|
scope = LlmemorySkill.where(user_id: user_id, archived_at: nil).order(created_at: :desc)
|
|
46
53
|
scope = scope.limit(limit) if limit && limit.to_i.positive?
|
|
47
54
|
scope = scope.offset(offset) if offset && offset.to_i.positive?
|
|
48
|
-
scope.map(
|
|
55
|
+
scope.map { |r| decode_data(r.data) }
|
|
49
56
|
end
|
|
50
57
|
|
|
51
58
|
def search_skills(user_id, query)
|
|
52
59
|
token_scope(LlmemorySkill.where(user_id: user_id, archived_at: nil), "search_text", query)
|
|
53
|
-
.order(created_at: :desc).map(
|
|
60
|
+
.order(created_at: :desc).map { |r| decode_data(r.data) }
|
|
54
61
|
end
|
|
55
62
|
|
|
56
63
|
def find_skills_by_name(user_id, name)
|
|
57
|
-
|
|
64
|
+
if cipher.enabled?
|
|
65
|
+
LlmemorySkill.where(user_id: user_id, archived_at: nil).map { |r| decode_data(r.data) }
|
|
66
|
+
.select { |s| (s[:name] || s["name"]).to_s == name.to_s }
|
|
67
|
+
else
|
|
68
|
+
LlmemorySkill.where(user_id: user_id, archived_at: nil).where("data->>'name' = ?", name.to_s).map(&:data)
|
|
69
|
+
end
|
|
58
70
|
end
|
|
59
71
|
|
|
60
72
|
def record_outcome(user_id, skill_id, success:)
|
|
61
73
|
rec = LlmemorySkill.find_by(user_id: user_id, id: skill_id)
|
|
62
74
|
return nil unless rec
|
|
63
|
-
data = rec.data || {}
|
|
64
|
-
key = success ?
|
|
75
|
+
data = decode_data(rec.data) || {}
|
|
76
|
+
key = success ? :success_count : :failure_count
|
|
65
77
|
data[key] = (data[key] || 0).to_i + 1
|
|
66
|
-
data[
|
|
67
|
-
rec.data = data
|
|
68
|
-
rec.search_text = searchable_text(data)
|
|
78
|
+
data[:updated_at] = Time.now.utc.iso8601
|
|
79
|
+
rec.data = cipher.enabled? ? enc_json(data) : data
|
|
80
|
+
rec.search_text = enc(searchable_text(data))
|
|
69
81
|
rec.save!
|
|
70
82
|
data
|
|
71
83
|
end
|
|
@@ -105,7 +117,15 @@ module Llmemory
|
|
|
105
117
|
end
|
|
106
118
|
|
|
107
119
|
def searchable_text(data)
|
|
108
|
-
|
|
120
|
+
h = data.is_a?(Hash) ? data : {}
|
|
121
|
+
[h["name"] || h[:name], h["description"] || h[:description], h["body"] || h[:body]].compact.join("\n")
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def decode_data(raw)
|
|
125
|
+
return raw.transform_keys(&:to_sym) if raw.is_a?(Hash)
|
|
126
|
+
return dec_json(raw) if raw.is_a?(String) && cipher.encrypted?(raw)
|
|
127
|
+
|
|
128
|
+
raw
|
|
109
129
|
end
|
|
110
130
|
end
|
|
111
131
|
end
|