llmemory 0.2.1 → 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 +78 -1
- data/app/controllers/llmemory/dashboard/application_controller.rb +15 -1
- data/app/controllers/llmemory/dashboard/episodic_controller.rb +22 -0
- data/app/controllers/llmemory/dashboard/forget_log_controller.rb +12 -0
- data/app/controllers/llmemory/dashboard/maintenance_controller.rb +92 -0
- data/app/controllers/llmemory/dashboard/procedural_controller.rb +22 -0
- data/app/controllers/llmemory/dashboard/reflection_controller.rb +37 -0
- data/app/controllers/llmemory/dashboard/working_controller.rb +14 -0
- data/app/views/llmemory/dashboard/episodic/index.html.erb +37 -0
- data/app/views/llmemory/dashboard/forget_log/show.html.erb +23 -0
- data/app/views/llmemory/dashboard/maintenance/show.html.erb +65 -0
- data/app/views/llmemory/dashboard/procedural/index.html.erb +38 -0
- data/app/views/llmemory/dashboard/reflection/show.html.erb +29 -0
- data/app/views/llmemory/dashboard/users/show.html.erb +16 -0
- data/app/views/llmemory/dashboard/working/show.html.erb +20 -0
- data/config/routes.rb +14 -0
- data/lib/generators/llmemory/install/templates/create_llmemory_tables.rb +2 -0
- data/lib/llmemory/cli/commands/maintain.rb +62 -0
- data/lib/llmemory/cli/commands/mine_skills.rb +50 -0
- data/lib/llmemory/cli.rb +6 -0
- data/lib/llmemory/configuration.rb +28 -2
- data/lib/llmemory/crypto/cipher.rb +147 -0
- data/lib/llmemory/crypto/field_helpers.rb +110 -0
- data/lib/llmemory/instrumentation.rb +33 -0
- data/lib/llmemory/llm/anthropic.rb +21 -16
- data/lib/llmemory/llm/openai.rb +18 -13
- data/lib/llmemory/long_term/episodic/memory.rb +27 -13
- data/lib/llmemory/long_term/episodic/storage.rb +11 -4
- data/lib/llmemory/long_term/episodic/storages/active_record_storage.rb +33 -10
- data/lib/llmemory/long_term/episodic/storages/base.rb +15 -2
- data/lib/llmemory/long_term/episodic/storages/database_storage.rb +51 -8
- data/lib/llmemory/long_term/episodic/storages/file_storage.rb +47 -9
- data/lib/llmemory/long_term/episodic/storages/memory_storage.rb +35 -4
- data/lib/llmemory/long_term/file_based/memory.rb +12 -4
- data/lib/llmemory/long_term/file_based/storage.rb +11 -4
- data/lib/llmemory/long_term/file_based/storages/active_record_storage.rb +20 -12
- data/lib/llmemory/long_term/file_based/storages/base.rb +2 -2
- data/lib/llmemory/long_term/file_based/storages/database_storage.rb +28 -10
- data/lib/llmemory/long_term/file_based/storages/file_storage.rb +32 -16
- data/lib/llmemory/long_term/file_based/storages/memory_storage.rb +4 -2
- data/lib/llmemory/long_term/graph_based/memory.rb +16 -7
- data/lib/llmemory/long_term/graph_based/storage.rb +3 -2
- data/lib/llmemory/long_term/graph_based/storages/active_record_storage.rb +51 -23
- data/lib/llmemory/long_term/graph_based/storages/base.rb +2 -2
- data/lib/llmemory/long_term/graph_based/storages/memory_storage.rb +4 -2
- data/lib/llmemory/long_term/procedural/memory.rb +30 -16
- data/lib/llmemory/long_term/procedural/skill.rb +6 -2
- data/lib/llmemory/long_term/procedural/storage.rb +11 -4
- data/lib/llmemory/long_term/procedural/storages/active_record_storage.rb +47 -17
- data/lib/llmemory/long_term/procedural/storages/base.rb +14 -1
- data/lib/llmemory/long_term/procedural/storages/database_storage.rb +52 -10
- data/lib/llmemory/long_term/procedural/storages/file_storage.rb +49 -11
- data/lib/llmemory/long_term/procedural/storages/memory_storage.rb +36 -5
- data/lib/llmemory/maintenance/cognitive_pass.rb +109 -0
- data/lib/llmemory/maintenance/ttl_expiry.rb +50 -0
- data/lib/llmemory/maintenance.rb +2 -0
- data/lib/llmemory/mcp/server.rb +5 -1
- data/lib/llmemory/mcp/tools/memory_maintain.rb +53 -0
- data/lib/llmemory/mcp/tools/memory_mine_skills.rb +53 -0
- data/lib/llmemory/memory.rb +60 -8
- data/lib/llmemory/memory_module.rb +13 -6
- data/lib/llmemory/reflection/reflector.rb +24 -20
- data/lib/llmemory/retrieval/engine.rb +25 -16
- 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/skill_mining/miner.rb +163 -0
- data/lib/llmemory/skill_mining.rb +8 -0
- 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/openai_embeddings.rb +11 -7
- data/lib/llmemory/vector_store.rb +4 -3
- data/lib/llmemory/version.rb +1 -1
- data/lib/llmemory.rb +4 -0
- metadata +24 -1
|
@@ -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
|
]
|
|
@@ -169,15 +173,16 @@ module Llmemory
|
|
|
169
173
|
conn.exec("SELECT DISTINCT user_id FROM llmemory_categories").map { |r| r["user_id"] }).uniq
|
|
170
174
|
end
|
|
171
175
|
|
|
172
|
-
def list_resources(user_id:, limit: nil)
|
|
176
|
+
def list_resources(user_id:, limit: nil, offset: nil)
|
|
173
177
|
ensure_tables!
|
|
174
178
|
sql = "SELECT id, text, created_at FROM llmemory_resources WHERE user_id = $1 ORDER BY created_at"
|
|
175
179
|
sql += " LIMIT #{limit.to_i}" if limit && limit.to_i.positive?
|
|
180
|
+
sql += " OFFSET #{offset.to_i}" if offset && offset.to_i.positive?
|
|
176
181
|
rows = conn.exec_params(sql, [user_id])
|
|
177
182
|
rows_to_resources(rows)
|
|
178
183
|
end
|
|
179
184
|
|
|
180
|
-
def list_items(user_id:, category: nil, limit: nil)
|
|
185
|
+
def list_items(user_id:, category: nil, limit: nil, offset: nil)
|
|
181
186
|
ensure_tables!
|
|
182
187
|
sql = "SELECT id, category, content, source_resource_id, importance, provenance, created_at FROM llmemory_items WHERE user_id = $1"
|
|
183
188
|
params = [user_id]
|
|
@@ -187,6 +192,7 @@ module Llmemory
|
|
|
187
192
|
end
|
|
188
193
|
sql += " ORDER BY created_at"
|
|
189
194
|
sql += " LIMIT #{limit.to_i}" if limit && limit.to_i.positive?
|
|
195
|
+
sql += " OFFSET #{offset.to_i}" if offset && offset.to_i.positive?
|
|
190
196
|
rows = params.size == 1 ? conn.exec_params(sql, params) : conn.exec_params(sql, params)
|
|
191
197
|
rows_to_items(rows)
|
|
192
198
|
end
|
|
@@ -281,7 +287,7 @@ module Llmemory
|
|
|
281
287
|
{
|
|
282
288
|
id: r["id"],
|
|
283
289
|
category: r["category"],
|
|
284
|
-
content: r["content"],
|
|
290
|
+
content: dec(r["content"]),
|
|
285
291
|
source_resource_id: r["source_resource_id"],
|
|
286
292
|
importance: (r["importance"] || 0.7).to_f,
|
|
287
293
|
provenance: parse_provenance(r["provenance"]),
|
|
@@ -302,16 +308,28 @@ module Llmemory
|
|
|
302
308
|
|
|
303
309
|
def parse_provenance(value)
|
|
304
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
|
+
|
|
305
314
|
JSON.parse(value, symbolize_names: true)
|
|
306
315
|
rescue JSON::ParserError
|
|
307
316
|
nil
|
|
308
317
|
end
|
|
309
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
|
+
|
|
310
328
|
def rows_to_resources(rows)
|
|
311
329
|
rows.map do |r|
|
|
312
330
|
{
|
|
313
331
|
id: r["id"],
|
|
314
|
-
text: r["text"],
|
|
332
|
+
text: dec(r["text"]),
|
|
315
333
|
created_at: Time.parse(r["created_at"])
|
|
316
334
|
}
|
|
317
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
|
|
|
@@ -152,14 +159,16 @@ module Llmemory
|
|
|
152
159
|
Dir.children(@base_path).select { |e| File.directory?(File.join(@base_path, e)) && !e.start_with?(".") }
|
|
153
160
|
end
|
|
154
161
|
|
|
155
|
-
def list_resources(user_id:, limit: nil)
|
|
162
|
+
def list_resources(user_id:, limit: nil, offset: nil)
|
|
156
163
|
list = get_all_resources(user_id)
|
|
164
|
+
list = list.drop(offset.to_i) if offset && offset.to_i.positive?
|
|
157
165
|
limit ? list.take(limit) : list
|
|
158
166
|
end
|
|
159
167
|
|
|
160
|
-
def list_items(user_id:, category: nil, limit: nil)
|
|
168
|
+
def list_items(user_id:, category: nil, limit: nil, offset: nil)
|
|
161
169
|
list = get_all_items(user_id)
|
|
162
170
|
list = list.select { |i| (i[:category] || i["category"]).to_s == category.to_s } if category
|
|
171
|
+
list = list.drop(offset.to_i) if offset && offset.to_i.positive?
|
|
163
172
|
list = list.take(limit) if limit
|
|
164
173
|
list
|
|
165
174
|
end
|
|
@@ -222,6 +231,13 @@ module Llmemory
|
|
|
222
231
|
return Time.parse(val.to_s) if val
|
|
223
232
|
Time.now
|
|
224
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
|
|
225
241
|
end
|
|
226
242
|
end
|
|
227
243
|
end
|
|
@@ -98,17 +98,19 @@ module Llmemory
|
|
|
98
98
|
(@resources.keys + @items.keys + @categories.keys).uniq
|
|
99
99
|
end
|
|
100
100
|
|
|
101
|
-
def list_resources(user_id:, limit: nil)
|
|
101
|
+
def list_resources(user_id:, limit: nil, offset: nil)
|
|
102
102
|
list = @resources[user_id].dup
|
|
103
|
+
list = list.drop(offset.to_i) if offset && offset.to_i.positive?
|
|
103
104
|
limit ? list.take(limit) : list
|
|
104
105
|
end
|
|
105
106
|
|
|
106
|
-
def list_items(user_id:, category: nil, limit: nil)
|
|
107
|
+
def list_items(user_id:, category: nil, limit: nil, offset: nil)
|
|
107
108
|
list = if category
|
|
108
109
|
@items[user_id].select { |i| i[:category].to_s == category.to_s }
|
|
109
110
|
else
|
|
110
111
|
@items[user_id].dup
|
|
111
112
|
end
|
|
113
|
+
list = list.drop(offset.to_i) if offset && offset.to_i.positive?
|
|
112
114
|
list = list.take(limit) if limit
|
|
113
115
|
list
|
|
114
116
|
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
|
|
@@ -78,11 +79,15 @@ module Llmemory
|
|
|
78
79
|
# --- MemoryModule uniform interface ---
|
|
79
80
|
|
|
80
81
|
def write(payload, **_meta)
|
|
81
|
-
|
|
82
|
+
result = nil
|
|
83
|
+
Llmemory::Instrumentation.instrument(:memory_write, memory_type: "graph_based", user_id: @user_id) do
|
|
84
|
+
result = memorize(payload)
|
|
85
|
+
end
|
|
86
|
+
result
|
|
82
87
|
end
|
|
83
88
|
|
|
84
|
-
def list(user_id: nil, limit: nil)
|
|
85
|
-
@graph_storage.list_nodes(user_id || @user_id, limit: limit)
|
|
89
|
+
def list(user_id: nil, limit: nil, offset: nil)
|
|
90
|
+
@graph_storage.list_nodes(user_id || @user_id, limit: limit, offset: offset)
|
|
86
91
|
end
|
|
87
92
|
|
|
88
93
|
def stats(user_id: nil)
|
|
@@ -95,16 +100,20 @@ module Llmemory
|
|
|
95
100
|
# removal in the audit log. Edges are soft-archived (archived_at) so they
|
|
96
101
|
# no longer appear in retrieval; nodes are left in place (a node may still
|
|
97
102
|
# be referenced by other active edges). Returns the number archived.
|
|
98
|
-
def forget(ids:, reason: nil)
|
|
103
|
+
def forget(ids:, reason: nil, mode: :soft)
|
|
104
|
+
# `:hard` would physically delete edge rows; not yet wired (the graph
|
|
105
|
+
# store only exposes soft archive_edge). Both modes route to archive
|
|
106
|
+
# for now; behavior is the same — kept for API uniformity.
|
|
99
107
|
archived = Array(ids).map(&:to_s).select { |edge_id| @kg.archive_edge(edge_id) }
|
|
100
108
|
forget_log.record(@user_id, memory_type: "graph_based", ids: archived, reason: reason)
|
|
109
|
+
Llmemory::Instrumentation.instrument(:memory_forget, memory_type: "graph_based", user_id: @user_id, count: archived.size, mode: mode)
|
|
101
110
|
archived.size
|
|
102
111
|
end
|
|
103
112
|
|
|
104
113
|
private
|
|
105
114
|
|
|
106
115
|
def build_vector_store
|
|
107
|
-
Llmemory::VectorStore.build(source_type: "edge")
|
|
116
|
+
Llmemory::VectorStore.build(source_type: "edge", cipher: @cipher)
|
|
108
117
|
end
|
|
109
118
|
|
|
110
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,14 +57,19 @@ 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
|
-
def list_nodes(user_id, entity_type: nil, limit: nil)
|
|
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?
|
|
72
|
+
scope = scope.offset(offset) if offset && offset.to_i.positive?
|
|
59
73
|
scope.map { |r| record_to_node(r) }
|
|
60
74
|
end
|
|
61
75
|
|
|
@@ -63,24 +77,23 @@ module Llmemory
|
|
|
63
77
|
e = edge.is_a?(Edge) ? edge : Edge.from_h(edge.to_h)
|
|
64
78
|
rec = if e.id && e.id.is_a?(Integer)
|
|
65
79
|
LlmemoryGraphEdge.find_by(user_id: user_id, id: e.id)
|
|
66
|
-
else
|
|
67
|
-
nil
|
|
68
80
|
end
|
|
81
|
+
stored_props = store_properties(e.properties || {})
|
|
69
82
|
if rec
|
|
70
83
|
rec.update!(
|
|
71
84
|
subject_id: e.subject_id,
|
|
72
|
-
predicate: e.predicate,
|
|
85
|
+
predicate: enc_det(e.predicate),
|
|
73
86
|
object_id: e.target_id,
|
|
74
|
-
properties:
|
|
87
|
+
properties: stored_props
|
|
75
88
|
)
|
|
76
89
|
rec.id
|
|
77
90
|
else
|
|
78
91
|
rec = LlmemoryGraphEdge.create!(
|
|
79
92
|
user_id: user_id,
|
|
80
93
|
subject_id: e.subject_id,
|
|
81
|
-
predicate: e.predicate,
|
|
94
|
+
predicate: enc_det(e.predicate),
|
|
82
95
|
object_id: e.target_id,
|
|
83
|
-
properties:
|
|
96
|
+
properties: stored_props
|
|
84
97
|
)
|
|
85
98
|
rec.id
|
|
86
99
|
end
|
|
@@ -90,7 +103,7 @@ module Llmemory
|
|
|
90
103
|
scope = LlmemoryGraphEdge.where(user_id: user_id)
|
|
91
104
|
scope = scope.where(archived_at: nil) unless include_archived
|
|
92
105
|
scope = scope.where(subject_id: subject_id) if subject_id
|
|
93
|
-
scope = scope.where(predicate: predicate) if predicate
|
|
106
|
+
scope = scope.where(predicate: enc_det(predicate.to_s)) if predicate
|
|
94
107
|
scope = scope.where(object_id: object_id) if object_id
|
|
95
108
|
scope.map { |r| record_to_edge(r) }
|
|
96
109
|
end
|
|
@@ -106,12 +119,13 @@ module Llmemory
|
|
|
106
119
|
(LlmemoryGraphNode.distinct.pluck(:user_id) + LlmemoryGraphEdge.distinct.pluck(:user_id)).uniq
|
|
107
120
|
end
|
|
108
121
|
|
|
109
|
-
def list_edges(user_id, subject_id: nil, predicate: nil, limit: nil)
|
|
122
|
+
def list_edges(user_id, subject_id: nil, predicate: nil, limit: nil, offset: nil)
|
|
110
123
|
scope = LlmemoryGraphEdge.where(user_id: user_id, archived_at: nil)
|
|
111
124
|
scope = scope.where(subject_id: subject_id) if subject_id
|
|
112
|
-
scope = scope.where(predicate: predicate) if predicate
|
|
125
|
+
scope = scope.where(predicate: enc_det(predicate.to_s)) if predicate
|
|
113
126
|
scope = scope.order(created_at: :desc) if limit && limit.to_i.positive?
|
|
114
127
|
scope = scope.limit(limit) if limit && limit.to_i.positive?
|
|
128
|
+
scope = scope.offset(offset) if offset && offset.to_i.positive?
|
|
115
129
|
scope.map { |r| record_to_edge(r) }
|
|
116
130
|
end
|
|
117
131
|
|
|
@@ -151,13 +165,27 @@ module Llmemory
|
|
|
151
165
|
|
|
152
166
|
private
|
|
153
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
|
+
|
|
154
182
|
def record_to_node(r)
|
|
155
183
|
Node.new(
|
|
156
184
|
id: r.id,
|
|
157
185
|
user_id: r.user_id,
|
|
158
|
-
entity_type: r.entity_type,
|
|
159
|
-
name: r.name,
|
|
160
|
-
properties: r.properties
|
|
186
|
+
entity_type: dec(r.entity_type),
|
|
187
|
+
name: dec(r.name),
|
|
188
|
+
properties: load_properties(r.properties),
|
|
161
189
|
created_at: r.created_at,
|
|
162
190
|
updated_at: r.updated_at
|
|
163
191
|
)
|
|
@@ -168,9 +196,9 @@ module Llmemory
|
|
|
168
196
|
id: r.id,
|
|
169
197
|
user_id: r.user_id,
|
|
170
198
|
subject_id: r.subject_id,
|
|
171
|
-
predicate: r.predicate,
|
|
199
|
+
predicate: dec(r.predicate),
|
|
172
200
|
target_id: r.object_id,
|
|
173
|
-
properties: r.properties
|
|
201
|
+
properties: load_properties(r.properties),
|
|
174
202
|
created_at: r.created_at,
|
|
175
203
|
archived_at: r.archived_at
|
|
176
204
|
)
|
|
@@ -17,7 +17,7 @@ module Llmemory
|
|
|
17
17
|
raise NotImplementedError, "#{self.class}#find_node_by_name must be implemented"
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
-
def list_nodes(user_id, entity_type: nil, limit: nil)
|
|
20
|
+
def list_nodes(user_id, entity_type: nil, limit: nil, offset: nil)
|
|
21
21
|
raise NotImplementedError, "#{self.class}#list_nodes must be implemented"
|
|
22
22
|
end
|
|
23
23
|
|
|
@@ -37,7 +37,7 @@ module Llmemory
|
|
|
37
37
|
raise NotImplementedError, "#{self.class}#list_users must be implemented"
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
-
def list_edges(user_id, subject_id: nil, predicate: nil, limit: nil)
|
|
40
|
+
def list_edges(user_id, subject_id: nil, predicate: nil, limit: nil, offset: nil)
|
|
41
41
|
raise NotImplementedError, "#{self.class}#list_edges must be implemented"
|
|
42
42
|
end
|
|
43
43
|
|
|
@@ -43,9 +43,10 @@ module Llmemory
|
|
|
43
43
|
@nodes[user_id].values.find { |n| n.entity_type == entity_type.to_s && n.name.to_s == name.to_s }
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
-
def list_nodes(user_id, entity_type: nil, limit: nil)
|
|
46
|
+
def list_nodes(user_id, entity_type: nil, limit: nil, offset: nil)
|
|
47
47
|
list = @nodes[user_id].values
|
|
48
48
|
list = list.select { |n| n.entity_type.to_s == entity_type.to_s } if entity_type
|
|
49
|
+
list = list.drop(offset.to_i) if offset && offset.to_i.positive?
|
|
49
50
|
limit ? list.take(limit) : list
|
|
50
51
|
end
|
|
51
52
|
|
|
@@ -106,8 +107,9 @@ module Llmemory
|
|
|
106
107
|
(@nodes.keys + @edges.keys).uniq
|
|
107
108
|
end
|
|
108
109
|
|
|
109
|
-
def list_edges(user_id, subject_id: nil, predicate: nil, limit: nil)
|
|
110
|
+
def list_edges(user_id, subject_id: nil, predicate: nil, limit: nil, offset: nil)
|
|
110
111
|
list = find_edges(user_id, subject_id: subject_id, predicate: predicate, object_id: nil, include_archived: false)
|
|
112
|
+
list = list.drop(offset.to_i) if offset && offset.to_i.positive?
|
|
111
113
|
limit ? list.take(limit) : list
|
|
112
114
|
end
|
|
113
115
|
|
|
@@ -22,20 +22,21 @@ 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
|
|
31
32
|
|
|
32
33
|
# Registers a skill. If `version` is omitted and a skill with the same
|
|
33
34
|
# name exists, the version auto-increments (skill evolution).
|
|
34
|
-
def register_skill(name:, body:, description: nil, kind: Skill::DEFAULT_KIND, version: nil)
|
|
35
|
+
def register_skill(name:, body:, description: nil, kind: Skill::DEFAULT_KIND, version: nil, provenance: nil)
|
|
35
36
|
version ||= next_version_for(name)
|
|
36
37
|
skill = Skill.new(
|
|
37
38
|
id: nil, user_id: @user_id, name: name, body: body,
|
|
38
|
-
description: description, kind: kind, version: version
|
|
39
|
+
description: description, kind: kind, version: version, provenance: provenance
|
|
39
40
|
)
|
|
40
41
|
id = @storage.save_skill(@user_id, skill.to_h)
|
|
41
42
|
index_vector(id, skill.searchable_text)
|
|
@@ -52,8 +53,8 @@ module Llmemory
|
|
|
52
53
|
raw && Skill.from_h(raw)
|
|
53
54
|
end
|
|
54
55
|
|
|
55
|
-
def skills(limit: nil)
|
|
56
|
-
@storage.list_skills(@user_id, limit: limit).map { |s| Skill.from_h(s) }
|
|
56
|
+
def skills(limit: nil, offset: nil)
|
|
57
|
+
@storage.list_skills(@user_id, limit: limit, offset: offset).map { |s| Skill.from_h(s) }
|
|
57
58
|
end
|
|
58
59
|
|
|
59
60
|
def count
|
|
@@ -83,25 +84,38 @@ module Llmemory
|
|
|
83
84
|
|
|
84
85
|
# --- MemoryModule uniform interface ---
|
|
85
86
|
|
|
86
|
-
def write(name:, body:, description: nil, kind: Skill::DEFAULT_KIND, version: nil, **_meta)
|
|
87
|
-
|
|
87
|
+
def write(name:, body:, description: nil, kind: Skill::DEFAULT_KIND, version: nil, provenance: nil, **_meta)
|
|
88
|
+
result = nil
|
|
89
|
+
Llmemory::Instrumentation.instrument(:memory_write, memory_type: "procedural", user_id: @user_id) do
|
|
90
|
+
result = register_skill(name: name, body: body, description: description, kind: kind, version: version, provenance: provenance)
|
|
91
|
+
end
|
|
92
|
+
result
|
|
88
93
|
end
|
|
89
94
|
|
|
90
|
-
def list(user_id: nil, limit: nil)
|
|
91
|
-
skills(limit: limit)
|
|
95
|
+
def list(user_id: nil, limit: nil, offset: nil)
|
|
96
|
+
skills(limit: limit, offset: offset)
|
|
92
97
|
end
|
|
93
98
|
|
|
94
99
|
def stats(user_id: nil)
|
|
95
100
|
{ skills: count }
|
|
96
101
|
end
|
|
97
102
|
|
|
98
|
-
def forget(ids:, reason: nil)
|
|
103
|
+
def forget(ids:, reason: nil, mode: :soft)
|
|
99
104
|
requested = Array(ids).map(&:to_s)
|
|
100
105
|
existing = @storage.list_skills(@user_id).map { |s| (s[:id] || s["id"]).to_s }
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
106
|
+
targeted = requested & existing
|
|
107
|
+
count = case mode
|
|
108
|
+
when :hard then @storage.delete_skills(@user_id, targeted).to_i
|
|
109
|
+
else @storage.archive_skills(@user_id, targeted).to_i
|
|
110
|
+
end
|
|
111
|
+
forget_log.record(@user_id, memory_type: "procedural", ids: targeted, reason: reason)
|
|
112
|
+
Llmemory::Instrumentation.instrument(:memory_forget, memory_type: "procedural", user_id: @user_id, count: count, mode: mode)
|
|
113
|
+
count
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Storage accessor for the TTL maintenance job.
|
|
117
|
+
def expired_ids(cutoff:)
|
|
118
|
+
@storage.expired_skill_ids(@user_id, cutoff: cutoff)
|
|
105
119
|
end
|
|
106
120
|
|
|
107
121
|
private
|
|
@@ -118,7 +132,7 @@ module Llmemory
|
|
|
118
132
|
if @vector_explicit
|
|
119
133
|
@vector_store
|
|
120
134
|
elsif Llmemory.configuration.procedural_vector_enabled
|
|
121
|
-
@vector_store ||= Llmemory::VectorStore.build(source_type: "skill")
|
|
135
|
+
@vector_store ||= Llmemory::VectorStore.build(source_type: "skill", cipher: @cipher)
|
|
122
136
|
end
|
|
123
137
|
end
|
|
124
138
|
|