llmemory 0.2.2 → 0.2.4
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 +65 -1
- data/lib/llmemory/cli/commands/stats.rb +5 -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/instrumentation.rb +4 -2
- data/lib/llmemory/llm/anthropic.rb +10 -4
- data/lib/llmemory/llm/base.rb +42 -0
- data/lib/llmemory/llm/openai.rb +29 -13
- data/lib/llmemory/llm/response.rb +18 -0
- data/lib/llmemory/llm/tracking_client.rb +61 -0
- data/lib/llmemory/llm/usage.rb +31 -0
- data/lib/llmemory/llm/usage_ledger.rb +118 -0
- data/lib/llmemory/llm/usage_recorder.rb +37 -0
- data/lib/llmemory/llm.rb +5 -0
- data/lib/llmemory/long_term/episodic/memory.rb +16 -4
- 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 +17 -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 +16 -4
- 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/mcp/tools/memory_stats.rb +13 -0
- data/lib/llmemory/memory.rb +66 -15
- data/lib/llmemory/short_term/checkpoint.rb +5 -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 +30 -3
- data/lib/llmemory/vector_store/memory_store.rb +29 -3
- data/lib/llmemory/vector_store/openai_embeddings.rb +23 -2
- data/lib/llmemory/vector_store.rb +4 -3
- data/lib/llmemory/version.rb +1 -1
- data/lib/llmemory.rb +2 -0
- metadata +8 -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
|
]
|
|
@@ -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)
|
|
@@ -161,6 +162,7 @@ module Llmemory
|
|
|
161
162
|
|
|
162
163
|
edge_text = "#{subject} #{predicate} #{object}"
|
|
163
164
|
embedding = @vector_store.respond_to?(:embed) ? @vector_store.embed(edge_text) : nil
|
|
165
|
+
record_embed_usage(@vector_store) if embedding
|
|
164
166
|
if embedding && @vector_store.respond_to?(:store)
|
|
165
167
|
@vector_store.store(id: edge_id, embedding: embedding, metadata: { text: edge_text, created_at: Time.now }, user_id: @user_id)
|
|
166
168
|
end
|
|
@@ -171,8 +173,10 @@ module Llmemory
|
|
|
171
173
|
vector_results = []
|
|
172
174
|
if @vector_store.respond_to?(:search_by_text)
|
|
173
175
|
vector_results = @vector_store.search_by_text(query.to_s, top_k: top_k, user_id: @user_id)
|
|
176
|
+
record_embed_usage(@vector_store)
|
|
174
177
|
elsif @vector_store.respond_to?(:embed) && @vector_store.respond_to?(:search)
|
|
175
178
|
emb = @vector_store.embed(query.to_s)
|
|
179
|
+
record_embed_usage(@vector_store)
|
|
176
180
|
vector_results = @vector_store.search(emb, top_k: top_k, user_id: @user_id)
|
|
177
181
|
end
|
|
178
182
|
|
|
@@ -230,6 +234,16 @@ module Llmemory
|
|
|
230
234
|
lines << "=== END MEMORIES ==="
|
|
231
235
|
lines.join("\n")
|
|
232
236
|
end
|
|
237
|
+
|
|
238
|
+
def record_embed_usage(vector_store)
|
|
239
|
+
return unless vector_store
|
|
240
|
+
|
|
241
|
+
Llmemory::LLM::UsageRecorder.record_embed_from_store(
|
|
242
|
+
user_id: @user_id,
|
|
243
|
+
vector_store: vector_store,
|
|
244
|
+
store: Llmemory::ShortTerm::Stores.build(cipher: @cipher)
|
|
245
|
+
)
|
|
246
|
+
end
|
|
233
247
|
end
|
|
234
248
|
end
|
|
235
249
|
end
|
|
@@ -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
|
|
|
@@ -140,6 +141,7 @@ module Llmemory
|
|
|
140
141
|
vs = vector_store
|
|
141
142
|
return if vs.nil? || text.to_s.strip.empty?
|
|
142
143
|
embedding = vs.embed(text)
|
|
144
|
+
record_embed_usage(vs)
|
|
143
145
|
return unless embedding
|
|
144
146
|
vs.store(id: id, embedding: embedding, metadata: { text: text, created_at: Time.now }, user_id: @user_id)
|
|
145
147
|
rescue StandardError
|
|
@@ -147,7 +149,9 @@ module Llmemory
|
|
|
147
149
|
end
|
|
148
150
|
|
|
149
151
|
def vector_candidates(query, top_k, vs)
|
|
150
|
-
vs.search_by_text(query.to_s, top_k: top_k, user_id: @user_id)
|
|
152
|
+
results = vs.search_by_text(query.to_s, top_k: top_k, user_id: @user_id)
|
|
153
|
+
record_embed_usage(vs)
|
|
154
|
+
results.filter_map do |r|
|
|
151
155
|
raw = @storage.get_skill(@user_id, r[:id] || r["id"])
|
|
152
156
|
raw && candidate_for(raw, (r[:score] || r["score"] || 1.0).to_f)
|
|
153
157
|
end
|
|
@@ -177,6 +181,14 @@ module Llmemory
|
|
|
177
181
|
end
|
|
178
182
|
by_id.values.sort_by { |c| -c[:score].to_f }.first(top_k)
|
|
179
183
|
end
|
|
184
|
+
|
|
185
|
+
def record_embed_usage(vector_store)
|
|
186
|
+
Llmemory::LLM::UsageRecorder.record_embed_from_store(
|
|
187
|
+
user_id: @user_id,
|
|
188
|
+
vector_store: vector_store,
|
|
189
|
+
store: Llmemory::ShortTerm::Stores.build(cipher: @cipher)
|
|
190
|
+
)
|
|
191
|
+
end
|
|
180
192
|
end
|
|
181
193
|
end
|
|
182
194
|
end
|
|
@@ -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
|