llmemory 0.2.1 → 0.2.2
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 +47 -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 +7 -1
- data/lib/llmemory/instrumentation.rb +33 -0
- data/lib/llmemory/llm/anthropic.rb +19 -15
- data/lib/llmemory/llm/openai.rb +16 -12
- data/lib/llmemory/long_term/episodic/memory.rb +23 -10
- data/lib/llmemory/long_term/episodic/storages/active_record_storage.rb +14 -4
- data/lib/llmemory/long_term/episodic/storages/base.rb +15 -2
- data/lib/llmemory/long_term/episodic/storages/database_storage.rb +26 -5
- data/lib/llmemory/long_term/episodic/storages/file_storage.rb +27 -6
- 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/storages/active_record_storage.rb +4 -2
- data/lib/llmemory/long_term/file_based/storages/base.rb +2 -2
- data/lib/llmemory/long_term/file_based/storages/database_storage.rb +4 -2
- data/lib/llmemory/long_term/file_based/storages/file_storage.rb +4 -2
- data/lib/llmemory/long_term/file_based/storages/memory_storage.rb +4 -2
- data/lib/llmemory/long_term/graph_based/memory.rb +12 -4
- data/lib/llmemory/long_term/graph_based/storages/active_record_storage.rb +4 -2
- 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 +26 -13
- data/lib/llmemory/long_term/procedural/skill.rb +6 -2
- data/lib/llmemory/long_term/procedural/storages/active_record_storage.rb +15 -5
- data/lib/llmemory/long_term/procedural/storages/base.rb +14 -1
- data/lib/llmemory/long_term/procedural/storages/database_storage.rb +27 -6
- data/lib/llmemory/long_term/procedural/storages/file_storage.rb +28 -7
- 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 +20 -0
- 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/skill_mining/miner.rb +163 -0
- data/lib/llmemory/skill_mining.rb +8 -0
- data/lib/llmemory/vector_store/openai_embeddings.rb +11 -7
- data/lib/llmemory/version.rb +1 -1
- data/lib/llmemory.rb +2 -0
- metadata +22 -1
|
@@ -123,17 +123,19 @@ module Llmemory
|
|
|
123
123
|
LlmemoryCategory.distinct.pluck(:user_id)).uniq
|
|
124
124
|
end
|
|
125
125
|
|
|
126
|
-
def list_resources(user_id:, limit: nil)
|
|
126
|
+
def list_resources(user_id:, limit: nil, offset: nil)
|
|
127
127
|
scope = LlmemoryResource.where(user_id: user_id).order(:created_at)
|
|
128
128
|
scope = scope.limit(limit) if limit && limit.to_i.positive?
|
|
129
|
+
scope = scope.offset(offset) if offset && offset.to_i.positive?
|
|
129
130
|
scope.map { |r| row_to_resource(r) }
|
|
130
131
|
end
|
|
131
132
|
|
|
132
|
-
def list_items(user_id:, category: nil, limit: nil)
|
|
133
|
+
def list_items(user_id:, category: nil, limit: nil, offset: nil)
|
|
133
134
|
scope = LlmemoryItem.where(user_id: user_id)
|
|
134
135
|
scope = scope.where(category: category) if category
|
|
135
136
|
scope = scope.order(:created_at)
|
|
136
137
|
scope = scope.limit(limit) if limit && limit.to_i.positive?
|
|
138
|
+
scope = scope.offset(offset) if offset && offset.to_i.positive?
|
|
137
139
|
scope.map { |r| row_to_item(r) }
|
|
138
140
|
end
|
|
139
141
|
|
|
@@ -69,11 +69,11 @@ module Llmemory
|
|
|
69
69
|
raise NotImplementedError, "#{self.class}#list_users must be implemented"
|
|
70
70
|
end
|
|
71
71
|
|
|
72
|
-
def list_resources(user_id:, limit: nil)
|
|
72
|
+
def list_resources(user_id:, limit: nil, offset: nil)
|
|
73
73
|
raise NotImplementedError, "#{self.class}#list_resources must be implemented"
|
|
74
74
|
end
|
|
75
75
|
|
|
76
|
-
def list_items(user_id:, category: nil, limit: nil)
|
|
76
|
+
def list_items(user_id:, category: nil, limit: nil, offset: nil)
|
|
77
77
|
raise NotImplementedError, "#{self.class}#list_items must be implemented"
|
|
78
78
|
end
|
|
79
79
|
|
|
@@ -169,15 +169,16 @@ module Llmemory
|
|
|
169
169
|
conn.exec("SELECT DISTINCT user_id FROM llmemory_categories").map { |r| r["user_id"] }).uniq
|
|
170
170
|
end
|
|
171
171
|
|
|
172
|
-
def list_resources(user_id:, limit: nil)
|
|
172
|
+
def list_resources(user_id:, limit: nil, offset: nil)
|
|
173
173
|
ensure_tables!
|
|
174
174
|
sql = "SELECT id, text, created_at FROM llmemory_resources WHERE user_id = $1 ORDER BY created_at"
|
|
175
175
|
sql += " LIMIT #{limit.to_i}" if limit && limit.to_i.positive?
|
|
176
|
+
sql += " OFFSET #{offset.to_i}" if offset && offset.to_i.positive?
|
|
176
177
|
rows = conn.exec_params(sql, [user_id])
|
|
177
178
|
rows_to_resources(rows)
|
|
178
179
|
end
|
|
179
180
|
|
|
180
|
-
def list_items(user_id:, category: nil, limit: nil)
|
|
181
|
+
def list_items(user_id:, category: nil, limit: nil, offset: nil)
|
|
181
182
|
ensure_tables!
|
|
182
183
|
sql = "SELECT id, category, content, source_resource_id, importance, provenance, created_at FROM llmemory_items WHERE user_id = $1"
|
|
183
184
|
params = [user_id]
|
|
@@ -187,6 +188,7 @@ module Llmemory
|
|
|
187
188
|
end
|
|
188
189
|
sql += " ORDER BY created_at"
|
|
189
190
|
sql += " LIMIT #{limit.to_i}" if limit && limit.to_i.positive?
|
|
191
|
+
sql += " OFFSET #{offset.to_i}" if offset && offset.to_i.positive?
|
|
190
192
|
rows = params.size == 1 ? conn.exec_params(sql, params) : conn.exec_params(sql, params)
|
|
191
193
|
rows_to_items(rows)
|
|
192
194
|
end
|
|
@@ -152,14 +152,16 @@ module Llmemory
|
|
|
152
152
|
Dir.children(@base_path).select { |e| File.directory?(File.join(@base_path, e)) && !e.start_with?(".") }
|
|
153
153
|
end
|
|
154
154
|
|
|
155
|
-
def list_resources(user_id:, limit: nil)
|
|
155
|
+
def list_resources(user_id:, limit: nil, offset: nil)
|
|
156
156
|
list = get_all_resources(user_id)
|
|
157
|
+
list = list.drop(offset.to_i) if offset && offset.to_i.positive?
|
|
157
158
|
limit ? list.take(limit) : list
|
|
158
159
|
end
|
|
159
160
|
|
|
160
|
-
def list_items(user_id:, category: nil, limit: nil)
|
|
161
|
+
def list_items(user_id:, category: nil, limit: nil, offset: nil)
|
|
161
162
|
list = get_all_items(user_id)
|
|
162
163
|
list = list.select { |i| (i[:category] || i["category"]).to_s == category.to_s } if category
|
|
164
|
+
list = list.drop(offset.to_i) if offset && offset.to_i.positive?
|
|
163
165
|
list = list.take(limit) if limit
|
|
164
166
|
list
|
|
165
167
|
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
|
|
@@ -78,11 +78,15 @@ module Llmemory
|
|
|
78
78
|
# --- MemoryModule uniform interface ---
|
|
79
79
|
|
|
80
80
|
def write(payload, **_meta)
|
|
81
|
-
|
|
81
|
+
result = nil
|
|
82
|
+
Llmemory::Instrumentation.instrument(:memory_write, memory_type: "graph_based", user_id: @user_id) do
|
|
83
|
+
result = memorize(payload)
|
|
84
|
+
end
|
|
85
|
+
result
|
|
82
86
|
end
|
|
83
87
|
|
|
84
|
-
def list(user_id: nil, limit: nil)
|
|
85
|
-
@graph_storage.list_nodes(user_id || @user_id, limit: limit)
|
|
88
|
+
def list(user_id: nil, limit: nil, offset: nil)
|
|
89
|
+
@graph_storage.list_nodes(user_id || @user_id, limit: limit, offset: offset)
|
|
86
90
|
end
|
|
87
91
|
|
|
88
92
|
def stats(user_id: nil)
|
|
@@ -95,9 +99,13 @@ module Llmemory
|
|
|
95
99
|
# removal in the audit log. Edges are soft-archived (archived_at) so they
|
|
96
100
|
# no longer appear in retrieval; nodes are left in place (a node may still
|
|
97
101
|
# be referenced by other active edges). Returns the number archived.
|
|
98
|
-
def forget(ids:, reason: nil)
|
|
102
|
+
def forget(ids:, reason: nil, mode: :soft)
|
|
103
|
+
# `:hard` would physically delete edge rows; not yet wired (the graph
|
|
104
|
+
# store only exposes soft archive_edge). Both modes route to archive
|
|
105
|
+
# for now; behavior is the same — kept for API uniformity.
|
|
99
106
|
archived = Array(ids).map(&:to_s).select { |edge_id| @kg.archive_edge(edge_id) }
|
|
100
107
|
forget_log.record(@user_id, memory_type: "graph_based", ids: archived, reason: reason)
|
|
108
|
+
Llmemory::Instrumentation.instrument(:memory_forget, memory_type: "graph_based", user_id: @user_id, count: archived.size, mode: mode)
|
|
101
109
|
archived.size
|
|
102
110
|
end
|
|
103
111
|
|
|
@@ -52,10 +52,11 @@ module Llmemory
|
|
|
52
52
|
record_to_node(rec) if rec
|
|
53
53
|
end
|
|
54
54
|
|
|
55
|
-
def list_nodes(user_id, entity_type: nil, limit: nil)
|
|
55
|
+
def list_nodes(user_id, entity_type: nil, limit: nil, offset: nil)
|
|
56
56
|
scope = LlmemoryGraphNode.where(user_id: user_id)
|
|
57
57
|
scope = scope.where(entity_type: entity_type) if entity_type
|
|
58
58
|
scope = scope.limit(limit) if limit && limit.to_i.positive?
|
|
59
|
+
scope = scope.offset(offset) if offset && offset.to_i.positive?
|
|
59
60
|
scope.map { |r| record_to_node(r) }
|
|
60
61
|
end
|
|
61
62
|
|
|
@@ -106,12 +107,13 @@ module Llmemory
|
|
|
106
107
|
(LlmemoryGraphNode.distinct.pluck(:user_id) + LlmemoryGraphEdge.distinct.pluck(:user_id)).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
|
scope = LlmemoryGraphEdge.where(user_id: user_id, archived_at: nil)
|
|
111
112
|
scope = scope.where(subject_id: subject_id) if subject_id
|
|
112
113
|
scope = scope.where(predicate: predicate) if predicate
|
|
113
114
|
scope = scope.order(created_at: :desc) if limit && limit.to_i.positive?
|
|
114
115
|
scope = scope.limit(limit) if limit && limit.to_i.positive?
|
|
116
|
+
scope = scope.offset(offset) if offset && offset.to_i.positive?
|
|
115
117
|
scope.map { |r| record_to_edge(r) }
|
|
116
118
|
end
|
|
117
119
|
|
|
@@ -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
|
|
|
@@ -31,11 +31,11 @@ module Llmemory
|
|
|
31
31
|
|
|
32
32
|
# Registers a skill. If `version` is omitted and a skill with the same
|
|
33
33
|
# name exists, the version auto-increments (skill evolution).
|
|
34
|
-
def register_skill(name:, body:, description: nil, kind: Skill::DEFAULT_KIND, version: nil)
|
|
34
|
+
def register_skill(name:, body:, description: nil, kind: Skill::DEFAULT_KIND, version: nil, provenance: nil)
|
|
35
35
|
version ||= next_version_for(name)
|
|
36
36
|
skill = Skill.new(
|
|
37
37
|
id: nil, user_id: @user_id, name: name, body: body,
|
|
38
|
-
description: description, kind: kind, version: version
|
|
38
|
+
description: description, kind: kind, version: version, provenance: provenance
|
|
39
39
|
)
|
|
40
40
|
id = @storage.save_skill(@user_id, skill.to_h)
|
|
41
41
|
index_vector(id, skill.searchable_text)
|
|
@@ -52,8 +52,8 @@ module Llmemory
|
|
|
52
52
|
raw && Skill.from_h(raw)
|
|
53
53
|
end
|
|
54
54
|
|
|
55
|
-
def skills(limit: nil)
|
|
56
|
-
@storage.list_skills(@user_id, limit: limit).map { |s| Skill.from_h(s) }
|
|
55
|
+
def skills(limit: nil, offset: nil)
|
|
56
|
+
@storage.list_skills(@user_id, limit: limit, offset: offset).map { |s| Skill.from_h(s) }
|
|
57
57
|
end
|
|
58
58
|
|
|
59
59
|
def count
|
|
@@ -83,25 +83,38 @@ module Llmemory
|
|
|
83
83
|
|
|
84
84
|
# --- MemoryModule uniform interface ---
|
|
85
85
|
|
|
86
|
-
def write(name:, body:, description: nil, kind: Skill::DEFAULT_KIND, version: nil, **_meta)
|
|
87
|
-
|
|
86
|
+
def write(name:, body:, description: nil, kind: Skill::DEFAULT_KIND, version: nil, provenance: nil, **_meta)
|
|
87
|
+
result = nil
|
|
88
|
+
Llmemory::Instrumentation.instrument(:memory_write, memory_type: "procedural", user_id: @user_id) do
|
|
89
|
+
result = register_skill(name: name, body: body, description: description, kind: kind, version: version, provenance: provenance)
|
|
90
|
+
end
|
|
91
|
+
result
|
|
88
92
|
end
|
|
89
93
|
|
|
90
|
-
def list(user_id: nil, limit: nil)
|
|
91
|
-
skills(limit: limit)
|
|
94
|
+
def list(user_id: nil, limit: nil, offset: nil)
|
|
95
|
+
skills(limit: limit, offset: offset)
|
|
92
96
|
end
|
|
93
97
|
|
|
94
98
|
def stats(user_id: nil)
|
|
95
99
|
{ skills: count }
|
|
96
100
|
end
|
|
97
101
|
|
|
98
|
-
def forget(ids:, reason: nil)
|
|
102
|
+
def forget(ids:, reason: nil, mode: :soft)
|
|
99
103
|
requested = Array(ids).map(&:to_s)
|
|
100
104
|
existing = @storage.list_skills(@user_id).map { |s| (s[:id] || s["id"]).to_s }
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
+
targeted = requested & existing
|
|
106
|
+
count = case mode
|
|
107
|
+
when :hard then @storage.delete_skills(@user_id, targeted).to_i
|
|
108
|
+
else @storage.archive_skills(@user_id, targeted).to_i
|
|
109
|
+
end
|
|
110
|
+
forget_log.record(@user_id, memory_type: "procedural", ids: targeted, reason: reason)
|
|
111
|
+
Llmemory::Instrumentation.instrument(:memory_forget, memory_type: "procedural", user_id: @user_id, count: count, mode: mode)
|
|
112
|
+
count
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Storage accessor for the TTL maintenance job.
|
|
116
|
+
def expired_ids(cutoff:)
|
|
117
|
+
@storage.expired_skill_ids(@user_id, cutoff: cutoff)
|
|
105
118
|
end
|
|
106
119
|
|
|
107
120
|
private
|
|
@@ -17,10 +17,11 @@ module Llmemory
|
|
|
17
17
|
DEFAULT_KIND = "prompt"
|
|
18
18
|
|
|
19
19
|
attr_reader :id, :user_id, :name, :description, :body, :kind, :version,
|
|
20
|
-
:success_count, :failure_count, :created_at, :updated_at
|
|
20
|
+
:success_count, :failure_count, :provenance, :created_at, :updated_at
|
|
21
21
|
|
|
22
22
|
def initialize(id:, user_id:, name:, body:, description: nil, kind: DEFAULT_KIND,
|
|
23
|
-
version: 1, success_count: 0, failure_count: 0,
|
|
23
|
+
version: 1, success_count: 0, failure_count: 0, provenance: nil,
|
|
24
|
+
created_at: nil, updated_at: nil)
|
|
24
25
|
@id = id
|
|
25
26
|
@user_id = user_id
|
|
26
27
|
@name = name.to_s
|
|
@@ -30,6 +31,7 @@ module Llmemory
|
|
|
30
31
|
@version = version.to_i
|
|
31
32
|
@success_count = success_count.to_i
|
|
32
33
|
@failure_count = failure_count.to_i
|
|
34
|
+
@provenance = provenance
|
|
33
35
|
@created_at = created_at || Time.now
|
|
34
36
|
@updated_at = updated_at || @created_at
|
|
35
37
|
end
|
|
@@ -60,6 +62,7 @@ module Llmemory
|
|
|
60
62
|
version: hash[:version] || hash["version"] || 1,
|
|
61
63
|
success_count: hash[:success_count] || hash["success_count"] || 0,
|
|
62
64
|
failure_count: hash[:failure_count] || hash["failure_count"] || 0,
|
|
65
|
+
provenance: hash[:provenance] || hash["provenance"],
|
|
63
66
|
created_at: parse_time(hash[:created_at] || hash["created_at"]),
|
|
64
67
|
updated_at: parse_time(hash[:updated_at] || hash["updated_at"])
|
|
65
68
|
)
|
|
@@ -83,6 +86,7 @@ module Llmemory
|
|
|
83
86
|
version: version,
|
|
84
87
|
success_count: success_count,
|
|
85
88
|
failure_count: failure_count,
|
|
89
|
+
provenance: provenance,
|
|
86
90
|
created_at: created_at.respond_to?(:iso8601) ? created_at.iso8601(6) : created_at,
|
|
87
91
|
updated_at: updated_at.respond_to?(:iso8601) ? updated_at.iso8601(6) : updated_at
|
|
88
92
|
}
|
|
@@ -41,19 +41,20 @@ module Llmemory
|
|
|
41
41
|
LlmemorySkill.find_by(user_id: user_id, id: id)&.data
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
-
def list_skills(user_id, limit: nil)
|
|
45
|
-
scope = LlmemorySkill.where(user_id: user_id).order(created_at: :desc)
|
|
44
|
+
def list_skills(user_id, limit: nil, offset: nil)
|
|
45
|
+
scope = LlmemorySkill.where(user_id: user_id, archived_at: nil).order(created_at: :desc)
|
|
46
46
|
scope = scope.limit(limit) if limit && limit.to_i.positive?
|
|
47
|
+
scope = scope.offset(offset) if offset && offset.to_i.positive?
|
|
47
48
|
scope.map(&:data)
|
|
48
49
|
end
|
|
49
50
|
|
|
50
51
|
def search_skills(user_id, query)
|
|
51
|
-
token_scope(LlmemorySkill.where(user_id: user_id), "search_text", query)
|
|
52
|
+
token_scope(LlmemorySkill.where(user_id: user_id, archived_at: nil), "search_text", query)
|
|
52
53
|
.order(created_at: :desc).map(&:data)
|
|
53
54
|
end
|
|
54
55
|
|
|
55
56
|
def find_skills_by_name(user_id, name)
|
|
56
|
-
LlmemorySkill.where(user_id: user_id).where("data->>'name' = ?", name.to_s).map(&:data)
|
|
57
|
+
LlmemorySkill.where(user_id: user_id, archived_at: nil).where("data->>'name' = ?", name.to_s).map(&:data)
|
|
57
58
|
end
|
|
58
59
|
|
|
59
60
|
def record_outcome(user_id, skill_id, success:)
|
|
@@ -70,13 +71,22 @@ module Llmemory
|
|
|
70
71
|
end
|
|
71
72
|
|
|
72
73
|
def count_skills(user_id)
|
|
73
|
-
LlmemorySkill.where(user_id: user_id).count
|
|
74
|
+
LlmemorySkill.where(user_id: user_id, archived_at: nil).count
|
|
74
75
|
end
|
|
75
76
|
|
|
76
77
|
def delete_skills(user_id, ids)
|
|
77
78
|
LlmemorySkill.where(user_id: user_id, id: Array(ids).map(&:to_s)).delete_all
|
|
78
79
|
end
|
|
79
80
|
|
|
81
|
+
def archive_skills(user_id, ids)
|
|
82
|
+
LlmemorySkill.where(user_id: user_id, id: Array(ids).map(&:to_s), archived_at: nil)
|
|
83
|
+
.update_all(archived_at: Time.current)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def expired_skill_ids(user_id, cutoff:)
|
|
87
|
+
LlmemorySkill.where(user_id: user_id, archived_at: nil).where("created_at < ?", cutoff).pluck(:id)
|
|
88
|
+
end
|
|
89
|
+
|
|
80
90
|
def list_users
|
|
81
91
|
LlmemorySkill.distinct.pluck(:user_id)
|
|
82
92
|
end
|
|
@@ -16,7 +16,7 @@ module Llmemory
|
|
|
16
16
|
raise NotImplementedError, "#{self.class}#get_skill must be implemented"
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
def list_skills(user_id, limit: nil)
|
|
19
|
+
def list_skills(user_id, limit: nil, offset: nil)
|
|
20
20
|
raise NotImplementedError, "#{self.class}#list_skills must be implemented"
|
|
21
21
|
end
|
|
22
22
|
|
|
@@ -43,6 +43,19 @@ module Llmemory
|
|
|
43
43
|
raise NotImplementedError, "#{self.class}#delete_skills must be implemented"
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
+
# Soft-archives skills by id. Archived skills are excluded from
|
|
47
|
+
# list_skills / search_skills / count_skills but remain accessible via
|
|
48
|
+
# get_skill. Returns the number archived.
|
|
49
|
+
def archive_skills(user_id, ids)
|
|
50
|
+
raise NotImplementedError, "#{self.class}#archive_skills must be implemented"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Returns skills whose created_at is older than the cutoff and that are
|
|
54
|
+
# not already archived. Used by the TTL maintenance job.
|
|
55
|
+
def expired_skill_ids(user_id, cutoff:)
|
|
56
|
+
raise NotImplementedError, "#{self.class}#expired_skill_ids must be implemented"
|
|
57
|
+
end
|
|
58
|
+
|
|
46
59
|
def list_users
|
|
47
60
|
raise NotImplementedError, "#{self.class}#list_users must be implemented"
|
|
48
61
|
end
|
|
@@ -38,10 +38,11 @@ module Llmemory
|
|
|
38
38
|
rows.any? ? parse_data(rows.first["data"]) : nil
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
-
def list_skills(user_id, limit: nil)
|
|
41
|
+
def list_skills(user_id, limit: nil, offset: nil)
|
|
42
42
|
ensure_tables!
|
|
43
|
-
sql = "SELECT data FROM llmemory_skills WHERE user_id = $1 ORDER BY created_at DESC"
|
|
43
|
+
sql = "SELECT data FROM llmemory_skills WHERE user_id = $1 AND archived_at IS NULL ORDER BY created_at DESC"
|
|
44
44
|
sql += " LIMIT #{limit.to_i}" if limit && limit.to_i.positive?
|
|
45
|
+
sql += " OFFSET #{offset.to_i}" if offset && offset.to_i.positive?
|
|
45
46
|
conn.exec_params(sql, [user_id]).map { |r| parse_data(r["data"]) }
|
|
46
47
|
end
|
|
47
48
|
|
|
@@ -49,7 +50,7 @@ module Llmemory
|
|
|
49
50
|
ensure_tables!
|
|
50
51
|
suffix, params = token_filter("search_text", query, 2)
|
|
51
52
|
conn.exec_params(
|
|
52
|
-
"SELECT data FROM llmemory_skills WHERE user_id = $1#{suffix} ORDER BY created_at DESC",
|
|
53
|
+
"SELECT data FROM llmemory_skills WHERE user_id = $1 AND archived_at IS NULL#{suffix} ORDER BY created_at DESC",
|
|
53
54
|
[user_id, *params]
|
|
54
55
|
).map { |r| parse_data(r["data"]) }
|
|
55
56
|
end
|
|
@@ -57,7 +58,7 @@ module Llmemory
|
|
|
57
58
|
def find_skills_by_name(user_id, name)
|
|
58
59
|
ensure_tables!
|
|
59
60
|
conn.exec_params(
|
|
60
|
-
"SELECT data FROM llmemory_skills WHERE user_id = $1 AND data->>'name' = $2",
|
|
61
|
+
"SELECT data FROM llmemory_skills WHERE user_id = $1 AND archived_at IS NULL AND data->>'name' = $2",
|
|
61
62
|
[user_id, name.to_s]
|
|
62
63
|
).map { |r| parse_data(r["data"]) }
|
|
63
64
|
end
|
|
@@ -78,7 +79,7 @@ module Llmemory
|
|
|
78
79
|
|
|
79
80
|
def count_skills(user_id)
|
|
80
81
|
ensure_tables!
|
|
81
|
-
conn.exec_params("SELECT COUNT(*) AS c FROM llmemory_skills WHERE user_id = $1", [user_id]).first["c"].to_i
|
|
82
|
+
conn.exec_params("SELECT COUNT(*) AS c FROM llmemory_skills WHERE user_id = $1 AND archived_at IS NULL", [user_id]).first["c"].to_i
|
|
82
83
|
end
|
|
83
84
|
|
|
84
85
|
def delete_skills(user_id, ids)
|
|
@@ -88,6 +89,24 @@ module Llmemory
|
|
|
88
89
|
end
|
|
89
90
|
end
|
|
90
91
|
|
|
92
|
+
def archive_skills(user_id, ids)
|
|
93
|
+
ensure_tables!
|
|
94
|
+
Array(ids).sum do |id|
|
|
95
|
+
conn.exec_params(
|
|
96
|
+
"UPDATE llmemory_skills SET archived_at = NOW() WHERE user_id = $1 AND id = $2 AND archived_at IS NULL",
|
|
97
|
+
[user_id, id]
|
|
98
|
+
).cmd_tuples
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def expired_skill_ids(user_id, cutoff:)
|
|
103
|
+
ensure_tables!
|
|
104
|
+
conn.exec_params(
|
|
105
|
+
"SELECT id FROM llmemory_skills WHERE user_id = $1 AND archived_at IS NULL AND created_at < $2",
|
|
106
|
+
[user_id, cutoff.iso8601]
|
|
107
|
+
).map { |r| r["id"] }
|
|
108
|
+
end
|
|
109
|
+
|
|
91
110
|
def list_users
|
|
92
111
|
ensure_tables!
|
|
93
112
|
conn.exec("SELECT DISTINCT user_id FROM llmemory_skills").map { |r| r["user_id"] }
|
|
@@ -109,9 +128,11 @@ module Llmemory
|
|
|
109
128
|
user_id TEXT NOT NULL,
|
|
110
129
|
data JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
111
130
|
search_text TEXT,
|
|
112
|
-
created_at TIMESTAMPTZ NOT NULL
|
|
131
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
132
|
+
archived_at TIMESTAMPTZ
|
|
113
133
|
);
|
|
114
134
|
CREATE INDEX IF NOT EXISTS idx_llmemory_skills_user_id ON llmemory_skills(user_id);
|
|
135
|
+
ALTER TABLE llmemory_skills ADD COLUMN IF NOT EXISTS archived_at TIMESTAMPTZ;
|
|
115
136
|
SQL
|
|
116
137
|
end
|
|
117
138
|
|
|
@@ -29,18 +29,19 @@ module Llmemory
|
|
|
29
29
|
load_skill(path)
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
-
def list_skills(user_id, limit: nil)
|
|
33
|
-
sorted =
|
|
32
|
+
def list_skills(user_id, limit: nil, offset: nil)
|
|
33
|
+
sorted = active_skills(user_id).sort_by { |s| s[:created_at] }.reverse
|
|
34
|
+
sorted = sorted.drop(offset.to_i) if offset && offset.to_i.positive?
|
|
34
35
|
limit && limit.to_i.positive? ? sorted.first(limit.to_i) : sorted
|
|
35
36
|
end
|
|
36
37
|
|
|
37
38
|
def search_skills(user_id, query)
|
|
38
39
|
return list_skills(user_id) if query.to_s.strip.empty?
|
|
39
|
-
|
|
40
|
+
active_skills(user_id).select { |s| Llmemory::Tokenizer.matches?(skill_text(s), query) }
|
|
40
41
|
end
|
|
41
42
|
|
|
42
43
|
def find_skills_by_name(user_id, name)
|
|
43
|
-
|
|
44
|
+
active_skills(user_id).select { |s| s[:name].to_s == name.to_s }
|
|
44
45
|
end
|
|
45
46
|
|
|
46
47
|
def record_outcome(user_id, skill_id, success:)
|
|
@@ -54,9 +55,7 @@ module Llmemory
|
|
|
54
55
|
end
|
|
55
56
|
|
|
56
57
|
def count_skills(user_id)
|
|
57
|
-
|
|
58
|
-
return 0 unless Dir.exist?(dir)
|
|
59
|
-
Dir.children(dir).count { |f| f.end_with?(".json") }
|
|
58
|
+
active_skills(user_id).size
|
|
60
59
|
end
|
|
61
60
|
|
|
62
61
|
def delete_skills(user_id, ids)
|
|
@@ -68,6 +67,24 @@ module Llmemory
|
|
|
68
67
|
end
|
|
69
68
|
end
|
|
70
69
|
|
|
70
|
+
def archive_skills(user_id, ids)
|
|
71
|
+
Array(ids).map(&:to_s).count do |id|
|
|
72
|
+
path = skill_path(user_id, id)
|
|
73
|
+
next false unless File.file?(path)
|
|
74
|
+
data = JSON.parse(File.read(path))
|
|
75
|
+
next false if data["archived_at"]
|
|
76
|
+
data["archived_at"] = Time.now.iso8601
|
|
77
|
+
File.write(path, JSON.generate(data))
|
|
78
|
+
true
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def expired_skill_ids(user_id, cutoff:)
|
|
83
|
+
active_skills(user_id)
|
|
84
|
+
.select { |s| (s[:created_at] || Time.now) < cutoff }
|
|
85
|
+
.map { |s| s[:id].to_s }
|
|
86
|
+
end
|
|
87
|
+
|
|
71
88
|
def list_users
|
|
72
89
|
return [] unless Dir.exist?(@base_path)
|
|
73
90
|
Dir.children(@base_path).select { |d| Dir.exist?(File.join(@base_path, d, "skills")) }
|
|
@@ -75,6 +92,10 @@ module Llmemory
|
|
|
75
92
|
|
|
76
93
|
private
|
|
77
94
|
|
|
95
|
+
def active_skills(user_id)
|
|
96
|
+
all_skills(user_id).reject { |s| s[:archived_at] }
|
|
97
|
+
end
|
|
98
|
+
|
|
78
99
|
def all_skills(user_id)
|
|
79
100
|
dir = user_path(user_id, "skills")
|
|
80
101
|
return [] unless Dir.exist?(dir)
|
|
@@ -25,18 +25,19 @@ module Llmemory
|
|
|
25
25
|
@skills[user_id].find { |s| s[:id] == id }
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
def list_skills(user_id, limit: nil)
|
|
29
|
-
sorted =
|
|
28
|
+
def list_skills(user_id, limit: nil, offset: nil)
|
|
29
|
+
sorted = active_skills(user_id).sort_by { |s| as_time(s[:created_at]) }.reverse
|
|
30
|
+
sorted = sorted.drop(offset.to_i) if offset && offset.to_i.positive?
|
|
30
31
|
limit && limit.to_i.positive? ? sorted.first(limit.to_i) : sorted
|
|
31
32
|
end
|
|
32
33
|
|
|
33
34
|
def search_skills(user_id, query)
|
|
34
35
|
return list_skills(user_id) if query.to_s.strip.empty?
|
|
35
|
-
|
|
36
|
+
active_skills(user_id).select { |s| Llmemory::Tokenizer.matches?(skill_text(s), query) }
|
|
36
37
|
end
|
|
37
38
|
|
|
38
39
|
def find_skills_by_name(user_id, name)
|
|
39
|
-
|
|
40
|
+
active_skills(user_id).select { |s| s[:name].to_s == name.to_s }
|
|
40
41
|
end
|
|
41
42
|
|
|
42
43
|
def record_outcome(user_id, skill_id, success:)
|
|
@@ -49,7 +50,7 @@ module Llmemory
|
|
|
49
50
|
end
|
|
50
51
|
|
|
51
52
|
def count_skills(user_id)
|
|
52
|
-
|
|
53
|
+
active_skills(user_id).size
|
|
53
54
|
end
|
|
54
55
|
|
|
55
56
|
def delete_skills(user_id, ids)
|
|
@@ -59,12 +60,42 @@ module Llmemory
|
|
|
59
60
|
before - @skills[user_id].size
|
|
60
61
|
end
|
|
61
62
|
|
|
63
|
+
def archive_skills(user_id, ids)
|
|
64
|
+
ids = Array(ids).map(&:to_s)
|
|
65
|
+
count = 0
|
|
66
|
+
@skills[user_id].each do |s|
|
|
67
|
+
next unless ids.include?(s[:id].to_s)
|
|
68
|
+
next if s[:archived_at]
|
|
69
|
+
s[:archived_at] = Time.now
|
|
70
|
+
count += 1
|
|
71
|
+
end
|
|
72
|
+
count
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def expired_skill_ids(user_id, cutoff:)
|
|
76
|
+
active_skills(user_id)
|
|
77
|
+
.select { |s| as_time(s[:created_at]) < cutoff }
|
|
78
|
+
.map { |s| s[:id].to_s }
|
|
79
|
+
end
|
|
80
|
+
|
|
62
81
|
def list_users
|
|
63
82
|
@skills.keys
|
|
64
83
|
end
|
|
65
84
|
|
|
66
85
|
private
|
|
67
86
|
|
|
87
|
+
def active_skills(user_id)
|
|
88
|
+
@skills[user_id].reject { |s| s[:archived_at] }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def as_time(value)
|
|
92
|
+
return Time.now if value.nil?
|
|
93
|
+
return value if value.is_a?(Time)
|
|
94
|
+
Time.parse(value.to_s)
|
|
95
|
+
rescue ArgumentError
|
|
96
|
+
Time.now
|
|
97
|
+
end
|
|
98
|
+
|
|
68
99
|
def symbolize(hash)
|
|
69
100
|
hash.each_with_object({}) { |(k, v), acc| acc[k.to_sym] = v }
|
|
70
101
|
end
|