llmemory 0.1.15 → 0.1.16
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/lib/generators/llmemory/install/templates/create_llmemory_tables.rb +1 -0
- data/lib/llmemory/configuration.rb +2 -0
- data/lib/llmemory/long_term/file_based/item.rb +4 -2
- data/lib/llmemory/long_term/file_based/memory.rb +6 -2
- data/lib/llmemory/long_term/file_based/storages/active_record_storage.rb +3 -1
- data/lib/llmemory/long_term/file_based/storages/base.rb +1 -1
- data/lib/llmemory/long_term/file_based/storages/database_storage.rb +18 -8
- data/lib/llmemory/long_term/file_based/storages/file_storage.rb +2 -1
- data/lib/llmemory/long_term/file_based/storages/memory_storage.rb +2 -1
- data/lib/llmemory/long_term/graph_based/edge.rb +7 -0
- data/lib/llmemory/long_term/graph_based/memory.rb +8 -6
- data/lib/llmemory/long_term/graph_based/node.rb +7 -0
- data/lib/llmemory/provenance.rb +64 -0
- data/lib/llmemory/retrieval/engine.rb +1 -0
- data/lib/llmemory/retrieval/temporal_ranker.rb +17 -3
- data/lib/llmemory/version.rb +1 -1
- data/lib/llmemory.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 135c27c05f80b660972a5aa8df5ba315cbdd8235ac68c662ddcfe9249ca79109
|
|
4
|
+
data.tar.gz: 2e43bacc332ddb44613c79ebd7e4e832091fc68c451a75073860502962d496d9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9b0d7d67c2647ec0392f993ed31aedc5d51fdc3dadf40bd1a511b90a55b24ce415a9b9a420cc6e676d5a3ba8eb780048e047eff9d0800618dc10e99c4c779a1f
|
|
7
|
+
data.tar.gz: 10184e66c86c94976d2c164f8717633fbb438e0e95cfb0dbffed4ce52f8bbf8c4266c0b3e48879c792250da15a385464983a886408c69598044c0f60c13f27e7
|
|
@@ -14,6 +14,7 @@ module Llmemory
|
|
|
14
14
|
:database_url,
|
|
15
15
|
:vector_store,
|
|
16
16
|
:time_decay_half_life_days,
|
|
17
|
+
:importance_weight,
|
|
17
18
|
:max_retrieval_tokens,
|
|
18
19
|
:prune_after_days,
|
|
19
20
|
:compact_max_bytes,
|
|
@@ -56,6 +57,7 @@ module Llmemory
|
|
|
56
57
|
@database_url = ENV["DATABASE_URL"]
|
|
57
58
|
@vector_store = nil
|
|
58
59
|
@time_decay_half_life_days = 30
|
|
60
|
+
@importance_weight = 1.0
|
|
59
61
|
@max_retrieval_tokens = 2000
|
|
60
62
|
@prune_after_days = 90
|
|
61
63
|
@compact_max_bytes = 8192
|
|
@@ -4,14 +4,15 @@ module Llmemory
|
|
|
4
4
|
module LongTerm
|
|
5
5
|
module FileBased
|
|
6
6
|
class Item
|
|
7
|
-
attr_reader :id, :user_id, :category, :content, :source_resource_id, :created_at
|
|
7
|
+
attr_reader :id, :user_id, :category, :content, :source_resource_id, :provenance, :created_at
|
|
8
8
|
|
|
9
|
-
def initialize(id:, user_id:, category:, content:, source_resource_id: nil, created_at: nil)
|
|
9
|
+
def initialize(id:, user_id:, category:, content:, source_resource_id: nil, provenance: nil, created_at: nil)
|
|
10
10
|
@id = id
|
|
11
11
|
@user_id = user_id
|
|
12
12
|
@category = category
|
|
13
13
|
@content = content
|
|
14
14
|
@source_resource_id = source_resource_id
|
|
15
|
+
@provenance = provenance
|
|
15
16
|
@created_at = created_at || Time.now
|
|
16
17
|
end
|
|
17
18
|
|
|
@@ -22,6 +23,7 @@ module Llmemory
|
|
|
22
23
|
category: category,
|
|
23
24
|
content: content,
|
|
24
25
|
source_resource_id: source_resource_id,
|
|
26
|
+
provenance: provenance,
|
|
25
27
|
created_at: created_at.iso8601
|
|
26
28
|
}
|
|
27
29
|
end
|
|
@@ -65,7 +65,8 @@ module Llmemory
|
|
|
65
65
|
out << {
|
|
66
66
|
text: i[:content] || i["content"],
|
|
67
67
|
timestamp: i[:created_at] || i["created_at"],
|
|
68
|
-
score:
|
|
68
|
+
score: 1.0,
|
|
69
|
+
importance: (i[:importance] || i["importance"] || 1.0).to_f,
|
|
69
70
|
evergreen: i[:evergreen] || i["evergreen"]
|
|
70
71
|
}
|
|
71
72
|
end
|
|
@@ -94,7 +95,10 @@ module Llmemory
|
|
|
94
95
|
|
|
95
96
|
def save_item(category:, item:, source_resource_id:, importance: 0.7)
|
|
96
97
|
content = item.is_a?(Hash) ? item["content"] || item[:content] : item.to_s
|
|
97
|
-
|
|
98
|
+
provenance = Llmemory::Provenance.from_resource(
|
|
99
|
+
source_resource_id, method: "fact_extraction", confidence: importance
|
|
100
|
+
)
|
|
101
|
+
@storage.save_item(@user_id, category: category, content: content, source_resource_id: source_resource_id, importance: importance, provenance: provenance)
|
|
98
102
|
end
|
|
99
103
|
|
|
100
104
|
def append_to_daily_log(conversation_text)
|
|
@@ -30,7 +30,7 @@ module Llmemory
|
|
|
30
30
|
id
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
-
def save_item(user_id, category:, content:, source_resource_id:, importance: 0.7)
|
|
33
|
+
def save_item(user_id, category:, content:, source_resource_id:, importance: 0.7, provenance: nil)
|
|
34
34
|
id = "item_#{SecureRandom.hex(8)}"
|
|
35
35
|
attrs = {
|
|
36
36
|
id: id,
|
|
@@ -41,6 +41,7 @@ module Llmemory
|
|
|
41
41
|
created_at: Time.current
|
|
42
42
|
}
|
|
43
43
|
attrs[:importance] = importance if LlmemoryItem.column_names.include?("importance")
|
|
44
|
+
attrs[:provenance] = provenance if provenance && LlmemoryItem.column_names.include?("provenance")
|
|
44
45
|
LlmemoryItem.create!(attrs)
|
|
45
46
|
id
|
|
46
47
|
end
|
|
@@ -189,6 +190,7 @@ module Llmemory
|
|
|
189
190
|
created_at: r.created_at
|
|
190
191
|
}
|
|
191
192
|
h[:importance] = r.respond_to?(:importance) ? (r.importance || 0.7).to_f : 0.7
|
|
193
|
+
h[:provenance] = r.provenance if r.respond_to?(:provenance)
|
|
192
194
|
h
|
|
193
195
|
end
|
|
194
196
|
|
|
@@ -9,7 +9,7 @@ module Llmemory
|
|
|
9
9
|
raise NotImplementedError, "#{self.class}#save_resource must be implemented"
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
-
def save_item(user_id, category:, content:, source_resource_id:, importance: 0.7)
|
|
12
|
+
def save_item(user_id, category:, content:, source_resource_id:, importance: 0.7, provenance: nil)
|
|
13
13
|
raise NotImplementedError, "#{self.class}#save_item must be implemented"
|
|
14
14
|
end
|
|
15
15
|
|
|
@@ -24,12 +24,12 @@ module Llmemory
|
|
|
24
24
|
id
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
-
def save_item(user_id, category:, content:, source_resource_id:, importance: 0.7)
|
|
27
|
+
def save_item(user_id, category:, content:, source_resource_id:, importance: 0.7, provenance: nil)
|
|
28
28
|
ensure_tables!
|
|
29
29
|
id = "item_#{SecureRandom.hex(8)}"
|
|
30
30
|
conn.exec_params(
|
|
31
|
-
"INSERT INTO llmemory_items (id, user_id, category, content, source_resource_id, importance, created_at) VALUES ($1, $2, $3, $4, $5, $6, $7)",
|
|
32
|
-
[id, user_id, category, content, source_resource_id, importance.to_f, Time.now.utc.iso8601]
|
|
31
|
+
"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, provenance ? JSON.generate(provenance) : nil, Time.now.utc.iso8601]
|
|
33
33
|
)
|
|
34
34
|
id
|
|
35
35
|
end
|
|
@@ -67,7 +67,7 @@ module Llmemory
|
|
|
67
67
|
ensure_tables!
|
|
68
68
|
pattern = "%#{conn.escape_string(query.to_s.downcase)}%"
|
|
69
69
|
rows = conn.exec_params(
|
|
70
|
-
"SELECT id, category, content, source_resource_id, importance, created_at FROM llmemory_items WHERE user_id = $1 AND LOWER(content) LIKE $2",
|
|
70
|
+
"SELECT id, category, content, source_resource_id, importance, provenance, created_at FROM llmemory_items WHERE user_id = $1 AND LOWER(content) LIKE $2",
|
|
71
71
|
[user_id, pattern]
|
|
72
72
|
)
|
|
73
73
|
rows_to_items(rows)
|
|
@@ -97,7 +97,7 @@ module Llmemory
|
|
|
97
97
|
ensure_tables!
|
|
98
98
|
cutoff = (Time.now - (days * 86400)).utc.iso8601
|
|
99
99
|
rows = conn.exec_params(
|
|
100
|
-
"SELECT id, category, content, source_resource_id, importance, created_at FROM llmemory_items WHERE user_id = $1 AND created_at < $2 ORDER BY created_at",
|
|
100
|
+
"SELECT id, category, content, source_resource_id, importance, provenance, created_at FROM llmemory_items WHERE user_id = $1 AND created_at < $2 ORDER BY created_at",
|
|
101
101
|
[user_id, cutoff]
|
|
102
102
|
)
|
|
103
103
|
rows_to_items(rows)
|
|
@@ -106,7 +106,7 @@ module Llmemory
|
|
|
106
106
|
def get_all_items(user_id)
|
|
107
107
|
ensure_tables!
|
|
108
108
|
rows = conn.exec_params(
|
|
109
|
-
"SELECT id, category, content, source_resource_id, importance, created_at FROM llmemory_items WHERE user_id = $1 ORDER BY created_at",
|
|
109
|
+
"SELECT id, category, content, source_resource_id, importance, provenance, created_at FROM llmemory_items WHERE user_id = $1 ORDER BY created_at",
|
|
110
110
|
[user_id]
|
|
111
111
|
)
|
|
112
112
|
rows_to_items(rows)
|
|
@@ -125,7 +125,7 @@ module Llmemory
|
|
|
125
125
|
ensure_tables!
|
|
126
126
|
cutoff = (Time.now - (hours * 3600)).utc.iso8601
|
|
127
127
|
rows = conn.exec_params(
|
|
128
|
-
"SELECT id, category, content, source_resource_id, importance, created_at FROM llmemory_items WHERE user_id = $1 AND created_at >= $2 ORDER BY created_at",
|
|
128
|
+
"SELECT id, category, content, source_resource_id, importance, provenance, created_at FROM llmemory_items WHERE user_id = $1 AND created_at >= $2 ORDER BY created_at",
|
|
129
129
|
[user_id, cutoff]
|
|
130
130
|
)
|
|
131
131
|
rows_to_items(rows)
|
|
@@ -179,7 +179,7 @@ module Llmemory
|
|
|
179
179
|
|
|
180
180
|
def list_items(user_id:, category: nil, limit: nil)
|
|
181
181
|
ensure_tables!
|
|
182
|
-
sql = "SELECT id, category, content, source_resource_id, importance, created_at FROM llmemory_items WHERE user_id = $1"
|
|
182
|
+
sql = "SELECT id, category, content, source_resource_id, importance, provenance, created_at FROM llmemory_items WHERE user_id = $1"
|
|
183
183
|
params = [user_id]
|
|
184
184
|
if category
|
|
185
185
|
sql += " AND category = $2"
|
|
@@ -258,11 +258,13 @@ module Llmemory
|
|
|
258
258
|
content TEXT NOT NULL,
|
|
259
259
|
source_resource_id TEXT,
|
|
260
260
|
importance REAL DEFAULT 0.7,
|
|
261
|
+
provenance JSONB,
|
|
261
262
|
created_at TIMESTAMPTZ NOT NULL
|
|
262
263
|
);
|
|
263
264
|
CREATE INDEX IF NOT EXISTS idx_llmemory_items_user_id ON llmemory_items(user_id);
|
|
264
265
|
SQL
|
|
265
266
|
conn.exec("ALTER TABLE llmemory_items ADD COLUMN IF NOT EXISTS importance REAL DEFAULT 0.7") rescue nil
|
|
267
|
+
conn.exec("ALTER TABLE llmemory_items ADD COLUMN IF NOT EXISTS provenance JSONB") rescue nil
|
|
266
268
|
conn.exec(<<~SQL)
|
|
267
269
|
CREATE TABLE IF NOT EXISTS llmemory_categories (
|
|
268
270
|
user_id TEXT NOT NULL,
|
|
@@ -282,11 +284,19 @@ module Llmemory
|
|
|
282
284
|
content: r["content"],
|
|
283
285
|
source_resource_id: r["source_resource_id"],
|
|
284
286
|
importance: (r["importance"] || 0.7).to_f,
|
|
287
|
+
provenance: parse_provenance(r["provenance"]),
|
|
285
288
|
created_at: Time.parse(r["created_at"])
|
|
286
289
|
}
|
|
287
290
|
end
|
|
288
291
|
end
|
|
289
292
|
|
|
293
|
+
def parse_provenance(value)
|
|
294
|
+
return nil if value.nil? || value.to_s.strip.empty?
|
|
295
|
+
JSON.parse(value, symbolize_names: true)
|
|
296
|
+
rescue JSON::ParserError
|
|
297
|
+
nil
|
|
298
|
+
end
|
|
299
|
+
|
|
290
300
|
def rows_to_resources(rows)
|
|
291
301
|
rows.map do |r|
|
|
292
302
|
{
|
|
@@ -24,7 +24,7 @@ module Llmemory
|
|
|
24
24
|
id
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
-
def save_item(user_id, category:, content:, source_resource_id:, importance: 0.7)
|
|
27
|
+
def save_item(user_id, category:, content:, source_resource_id:, importance: 0.7, provenance: nil)
|
|
28
28
|
ensure_user_dir(user_id)
|
|
29
29
|
seq = next_seq(user_id, "item_id_seq")
|
|
30
30
|
id = "item_#{seq}"
|
|
@@ -35,6 +35,7 @@ module Llmemory
|
|
|
35
35
|
content: content,
|
|
36
36
|
source_resource_id: source_resource_id,
|
|
37
37
|
importance: importance,
|
|
38
|
+
provenance: provenance,
|
|
38
39
|
created_at: Time.now.iso8601
|
|
39
40
|
}
|
|
40
41
|
File.write(path, JSON.generate(data))
|
|
@@ -22,7 +22,7 @@ module Llmemory
|
|
|
22
22
|
id
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
def save_item(user_id, category:, content:, source_resource_id:, importance: 0.7)
|
|
25
|
+
def save_item(user_id, category:, content:, source_resource_id:, importance: 0.7, provenance: nil)
|
|
26
26
|
@item_id_seq += 1
|
|
27
27
|
id = "item_#{@item_id_seq}"
|
|
28
28
|
@items[user_id] << {
|
|
@@ -31,6 +31,7 @@ module Llmemory
|
|
|
31
31
|
content: content,
|
|
32
32
|
source_resource_id: source_resource_id,
|
|
33
33
|
importance: importance,
|
|
34
|
+
provenance: provenance,
|
|
34
35
|
created_at: Time.now
|
|
35
36
|
}
|
|
36
37
|
id
|
|
@@ -31,6 +31,13 @@ module Llmemory
|
|
|
31
31
|
!archived_at.nil?
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
+
# Lineage of this edge, stored within properties so it round-trips
|
|
35
|
+
# through every backend without a schema change. See Llmemory::Provenance.
|
|
36
|
+
def provenance
|
|
37
|
+
props = properties || {}
|
|
38
|
+
props[:provenance] || props["provenance"]
|
|
39
|
+
end
|
|
40
|
+
|
|
34
41
|
def to_h
|
|
35
42
|
{
|
|
36
43
|
id: id,
|
|
@@ -32,6 +32,7 @@ module Llmemory
|
|
|
32
32
|
|
|
33
33
|
return true if entities.empty? && relations.empty?
|
|
34
34
|
|
|
35
|
+
provenance = Llmemory::Provenance.from_text_fingerprint(text, method: "entity_relation_extraction")
|
|
35
36
|
name_to_id = {}
|
|
36
37
|
|
|
37
38
|
entities.each do |e|
|
|
@@ -39,7 +40,7 @@ module Llmemory
|
|
|
39
40
|
entity_type = e[:type] || e["type"] || "concept"
|
|
40
41
|
name = e[:name] || e["name"]
|
|
41
42
|
next if name.nil? || name.to_s.strip.empty?
|
|
42
|
-
id = @kg.add_node(entity_type: entity_type, name: name.to_s.strip, properties: {})
|
|
43
|
+
id = @kg.add_node(entity_type: entity_type, name: name.to_s.strip, properties: { "provenance" => provenance })
|
|
43
44
|
name_to_id[name.to_s.strip] ||= id
|
|
44
45
|
end
|
|
45
46
|
|
|
@@ -50,8 +51,8 @@ module Llmemory
|
|
|
50
51
|
object = (r[:object] || r["object"]).to_s.strip
|
|
51
52
|
next if subject.empty? || predicate.empty? || object.empty?
|
|
52
53
|
|
|
53
|
-
subject_id = name_to_id[subject] || @kg.add_node(entity_type: "concept", name: subject, properties: {})
|
|
54
|
-
object_id = name_to_id[object] || @kg.add_node(entity_type: "concept", name: object, properties: {})
|
|
54
|
+
subject_id = name_to_id[subject] || @kg.add_node(entity_type: "concept", name: subject, properties: { "provenance" => provenance })
|
|
55
|
+
object_id = name_to_id[object] || @kg.add_node(entity_type: "concept", name: object, properties: { "provenance" => provenance })
|
|
55
56
|
|
|
56
57
|
edge = Edge.new(
|
|
57
58
|
id: nil,
|
|
@@ -59,12 +60,12 @@ module Llmemory
|
|
|
59
60
|
subject_id: subject_id,
|
|
60
61
|
predicate: predicate,
|
|
61
62
|
target_id: object_id,
|
|
62
|
-
properties: {},
|
|
63
|
+
properties: { "provenance" => provenance },
|
|
63
64
|
created_at: Time.now,
|
|
64
65
|
archived_at: nil
|
|
65
66
|
)
|
|
66
67
|
@conflict_resolver.resolve(edge)
|
|
67
|
-
edge_id = @kg.add_edge(subject: subject_id, predicate: predicate, object: object_id, properties: {})
|
|
68
|
+
edge_id = @kg.add_edge(subject: subject_id, predicate: predicate, object: object_id, properties: { "provenance" => provenance })
|
|
68
69
|
|
|
69
70
|
text = "#{subject} #{predicate} #{object}"
|
|
70
71
|
embedding = @vector_store.respond_to?(:embed) ? @vector_store.embed(text) : nil
|
|
@@ -89,7 +90,8 @@ module Llmemory
|
|
|
89
90
|
{
|
|
90
91
|
text: r[:text],
|
|
91
92
|
timestamp: r[:created_at] || r[:timestamp],
|
|
92
|
-
score: r[:score] || 1.0
|
|
93
|
+
score: r[:score] || 1.0,
|
|
94
|
+
importance: r[:importance]
|
|
93
95
|
}
|
|
94
96
|
end
|
|
95
97
|
end
|
|
@@ -25,6 +25,13 @@ module Llmemory
|
|
|
25
25
|
)
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
+
# Lineage of this node, stored within properties so it round-trips
|
|
29
|
+
# through every backend without a schema change. See Llmemory::Provenance.
|
|
30
|
+
def provenance
|
|
31
|
+
props = properties || {}
|
|
32
|
+
props[:provenance] || props["provenance"]
|
|
33
|
+
end
|
|
34
|
+
|
|
28
35
|
def to_h
|
|
29
36
|
{
|
|
30
37
|
id: id,
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module Llmemory
|
|
6
|
+
# Provenance records the lineage of a long-term memory item: where it came
|
|
7
|
+
# from, how it was produced, and with what confidence. It is stored as a
|
|
8
|
+
# plain JSON-safe Hash so it round-trips through every storage backend
|
|
9
|
+
# (in-memory, JSON files, SQL columns, jsonb properties) without coupling.
|
|
10
|
+
#
|
|
11
|
+
# Shape: { sources: [{ type:, id: }], method:, confidence:, created_at: }
|
|
12
|
+
#
|
|
13
|
+
# `method` identifies the producing process (e.g. "fact_extraction",
|
|
14
|
+
# "entity_relation_extraction", and in the future "reflection"), so a
|
|
15
|
+
# semantic datum can always be traced back to its raw source.
|
|
16
|
+
module Provenance
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
def build(method:, sources: [], confidence: nil, created_at: nil)
|
|
20
|
+
{
|
|
21
|
+
sources: Array(sources).filter_map { |s| normalize_source(s) },
|
|
22
|
+
method: method&.to_s,
|
|
23
|
+
confidence: confidence.nil? ? nil : confidence.to_f,
|
|
24
|
+
created_at: normalize_time(created_at)
|
|
25
|
+
}
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Convenience for the file-based path, where the raw text is persisted as a
|
|
29
|
+
# Resource and referenced by id.
|
|
30
|
+
def from_resource(resource_id, method:, confidence: nil, created_at: nil)
|
|
31
|
+
sources = resource_id ? [{ type: "resource", id: resource_id }] : []
|
|
32
|
+
build(method: method, sources: sources, confidence: confidence, created_at: created_at)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Convenience for the graph-based path, which does not persist the raw text.
|
|
36
|
+
# We record a stable fingerprint of the source instead of the document
|
|
37
|
+
# itself, keeping lineage verifiable without exposing sensitive content.
|
|
38
|
+
def from_text_fingerprint(text, method:, confidence: nil, created_at: nil)
|
|
39
|
+
require "digest"
|
|
40
|
+
sources = []
|
|
41
|
+
unless text.to_s.strip.empty?
|
|
42
|
+
sources = [{ type: "text_sha256", id: Digest::SHA256.hexdigest(text.to_s)[0, 16] }]
|
|
43
|
+
end
|
|
44
|
+
build(method: method, sources: sources, confidence: confidence, created_at: created_at)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def normalize_source(source)
|
|
48
|
+
return nil if source.nil?
|
|
49
|
+
if source.is_a?(Hash)
|
|
50
|
+
type = source[:type] || source["type"]
|
|
51
|
+
id = source[:id] || source["id"]
|
|
52
|
+
return nil if id.nil?
|
|
53
|
+
{ type: type.nil? ? "unknown" : type.to_s, id: id }
|
|
54
|
+
else
|
|
55
|
+
{ type: "unknown", id: source }
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def normalize_time(value)
|
|
60
|
+
value ||= Time.now
|
|
61
|
+
value.respond_to?(:iso8601) ? value.iso8601 : value.to_s
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -54,6 +54,7 @@ module Llmemory
|
|
|
54
54
|
text: c[:text] || c["text"],
|
|
55
55
|
timestamp: parse_timestamp(c[:timestamp] || c["timestamp"] || c[:created_at] || c["created_at"]),
|
|
56
56
|
score: (c[:score] || c["score"] || 1.0).to_f,
|
|
57
|
+
importance: c[:importance] || c["importance"],
|
|
57
58
|
evergreen: c[:evergreen] || c["evergreen"]
|
|
58
59
|
}
|
|
59
60
|
end
|
|
@@ -3,12 +3,14 @@
|
|
|
3
3
|
module Llmemory
|
|
4
4
|
module Retrieval
|
|
5
5
|
class TemporalRanker
|
|
6
|
-
def initialize(half_life_days: nil)
|
|
6
|
+
def initialize(half_life_days: nil, importance_weight: nil)
|
|
7
7
|
@half_life_days = half_life_days || Llmemory.configuration.time_decay_half_life_days
|
|
8
|
+
@importance_weight = importance_weight || Llmemory.configuration.importance_weight
|
|
8
9
|
end
|
|
9
10
|
|
|
10
11
|
def rank(candidates, now: Time.now)
|
|
11
12
|
lambda_val = Math.log(2) / @half_life_days.to_f
|
|
13
|
+
weight = [@importance_weight.to_f, 0.0].max
|
|
12
14
|
|
|
13
15
|
candidates.map do |c|
|
|
14
16
|
score = (c[:score] || c["score"] || 1.0).to_f
|
|
@@ -22,10 +24,22 @@ module Llmemory
|
|
|
22
24
|
Math.exp(-lambda_val * age_days.to_f)
|
|
23
25
|
end
|
|
24
26
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
importance = normalize_importance(c[:importance] || c["importance"])
|
|
28
|
+
importance_factor = importance**weight
|
|
29
|
+
|
|
30
|
+
final_score = score * time_decay * importance_factor
|
|
31
|
+
c.merge(score: score, importance: importance, temporal_score: final_score, timestamp: timestamp)
|
|
27
32
|
end.sort_by { |c| -(c[:temporal_score] || 0) }
|
|
28
33
|
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
# Missing importance is neutral (1.0) so candidates that carry no
|
|
38
|
+
# importance signal (resources, graph edges) are never penalised.
|
|
39
|
+
def normalize_importance(value)
|
|
40
|
+
return 1.0 if value.nil?
|
|
41
|
+
[[value.to_f, 0.0].max, 1.0].min
|
|
42
|
+
end
|
|
29
43
|
end
|
|
30
44
|
end
|
|
31
45
|
end
|
data/lib/llmemory/version.rb
CHANGED
data/lib/llmemory.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: llmemory
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.16
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- llmemory
|
|
@@ -195,6 +195,7 @@ files:
|
|
|
195
195
|
- lib/llmemory/mcp/tools/memory_timeline_context.rb
|
|
196
196
|
- lib/llmemory/memory.rb
|
|
197
197
|
- lib/llmemory/noise_filter.rb
|
|
198
|
+
- lib/llmemory/provenance.rb
|
|
198
199
|
- lib/llmemory/retrieval.rb
|
|
199
200
|
- lib/llmemory/retrieval/bm25_scorer.rb
|
|
200
201
|
- lib/llmemory/retrieval/context_assembler.rb
|
|
@@ -240,7 +241,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
240
241
|
- !ruby/object:Gem::Version
|
|
241
242
|
version: '0'
|
|
242
243
|
requirements: []
|
|
243
|
-
rubygems_version: 4.0.
|
|
244
|
+
rubygems_version: 4.0.10
|
|
244
245
|
specification_version: 4
|
|
245
246
|
summary: Persistent memory system for LLM agents
|
|
246
247
|
test_files: []
|