llmemory 0.2.10 → 0.3.0
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 +48 -0
- data/lib/generators/llmemory/install/templates/create_llmemory_tables.rb +71 -0
- data/lib/generators/llmemory/upgrade_zero_mem/templates/create_llmemory_zero_mem_tables.rb +77 -0
- data/lib/generators/llmemory/upgrade_zero_mem/upgrade_zero_mem_generator.rb +19 -0
- data/lib/llmemory/cli/commands/zero_mem.rb +107 -0
- data/lib/llmemory/cli.rb +3 -0
- data/lib/llmemory/configuration.rb +67 -1
- data/lib/llmemory/consolidation/filter.rb +74 -0
- data/lib/llmemory/consolidation/policy.rb +17 -0
- data/lib/llmemory/consolidation/rule_based_policy.rb +63 -0
- data/lib/llmemory/consolidation/unit.rb +54 -0
- data/lib/llmemory/consolidation.rb +11 -0
- data/lib/llmemory/instrumentation.rb +10 -1
- data/lib/llmemory/long_term/file_based/memory.rb +18 -7
- data/lib/llmemory/long_term/graph_based/memory.rb +31 -8
- data/lib/llmemory/maintenance/cognitive_pass.rb +29 -1
- data/lib/llmemory/maintenance/policy_prune.rb +85 -0
- data/lib/llmemory/maintenance/zero_mem_ttl.rb +26 -0
- data/lib/llmemory/maintenance.rb +2 -0
- data/lib/llmemory/mcp/server.rb +2 -0
- data/lib/llmemory/mcp/store_helpers.rb +16 -0
- data/lib/llmemory/mcp/tools/memory_add_message.rb +17 -4
- data/lib/llmemory/mcp/tools/memory_consolidate.rb +9 -4
- data/lib/llmemory/mcp/tools/memory_maintain.rb +6 -1
- data/lib/llmemory/mcp/tools/memory_retrieve.rb +3 -1
- data/lib/llmemory/mcp/tools/memory_retrieve_evidence.rb +70 -0
- data/lib/llmemory/mcp/tools/memory_stats.rb +22 -0
- data/lib/llmemory/memory.rb +257 -9
- data/lib/llmemory/retrieval/bm25_scorer.rb +5 -0
- data/lib/llmemory/retrieval/context_assembler.rb +17 -1
- data/lib/llmemory/tokenizer.rb +18 -8
- data/lib/llmemory/vector_store/memory_store.rb +23 -1
- data/lib/llmemory/vector_store/openai_embeddings.rb +47 -21
- data/lib/llmemory/version.rb +1 -1
- data/lib/llmemory/zero_mem/answer_calibrator.rb +63 -0
- data/lib/llmemory/zero_mem/backfill.rb +43 -0
- data/lib/llmemory/zero_mem/budget_assembler.rb +62 -0
- data/lib/llmemory/zero_mem/circuit_breaker.rb +37 -0
- data/lib/llmemory/zero_mem/embedding_provider.rb +73 -0
- data/lib/llmemory/zero_mem/engine.rb +247 -0
- data/lib/llmemory/zero_mem/entity.rb +31 -0
- data/lib/llmemory/zero_mem/entity_extractor.rb +23 -0
- data/lib/llmemory/zero_mem/entity_index.rb +65 -0
- data/lib/llmemory/zero_mem/entity_mention.rb +35 -0
- data/lib/llmemory/zero_mem/errors.rb +7 -0
- data/lib/llmemory/zero_mem/evidence.rb +43 -0
- data/lib/llmemory/zero_mem/evidence_calibrator.rb +158 -0
- data/lib/llmemory/zero_mem/evidence_closure.rb +98 -0
- data/lib/llmemory/zero_mem/evidence_fusion.rb +57 -0
- data/lib/llmemory/zero_mem/evidence_set.rb +54 -0
- data/lib/llmemory/zero_mem/extractors/heuristic.rb +44 -0
- data/lib/llmemory/zero_mem/extractors/http.rb +58 -0
- data/lib/llmemory/zero_mem/graph_retriever.rb +107 -0
- data/lib/llmemory/zero_mem/hierarchy_builder.rb +123 -0
- data/lib/llmemory/zero_mem/hierarchy_retriever.rb +133 -0
- data/lib/llmemory/zero_mem/indexer.rb +73 -0
- data/lib/llmemory/zero_mem/mode.rb +30 -0
- data/lib/llmemory/zero_mem/pagerank.rb +42 -0
- data/lib/llmemory/zero_mem/query_profile.rb +48 -0
- data/lib/llmemory/zero_mem/query_profiler.rb +138 -0
- data/lib/llmemory/zero_mem/repair.rb +31 -0
- data/lib/llmemory/zero_mem/router.rb +69 -0
- data/lib/llmemory/zero_mem/sidecar_client.rb +82 -0
- data/lib/llmemory/zero_mem/storage.rb +124 -0
- data/lib/llmemory/zero_mem/storages/active_record.rb +311 -0
- data/lib/llmemory/zero_mem/storages/active_record_models.rb +21 -0
- data/lib/llmemory/zero_mem/storages/file.rb +67 -0
- data/lib/llmemory/zero_mem/storages/memory.rb +294 -0
- data/lib/llmemory/zero_mem/storages/postgres.rb +85 -0
- data/lib/llmemory/zero_mem/storages/snapshot_codec.rb +180 -0
- data/lib/llmemory/zero_mem/storages.rb +43 -0
- data/lib/llmemory/zero_mem/trace.rb +100 -0
- data/lib/llmemory/zero_mem/trace_state_link.rb +64 -0
- data/lib/llmemory/zero_mem/trace_unit.rb +83 -0
- data/lib/llmemory/zero_mem.rb +42 -0
- data/lib/llmemory.rb +8 -0
- data/lib/tasks/llmemory.rake +84 -2
- metadata +54 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 004e6d5f30c9eba38319382257934d0644635fd63555e854043f32d7ec4aa7b0
|
|
4
|
+
data.tar.gz: 9295714e79963b31a2c23eb6834a4c31b75c2d83a044ecce905726c5ad8a41b1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e56b9d1b8a537b1a134be39cc9e0f563eed3f7e20f0cef23bb8dc12aa7881c30512382e027ed5926f507faf09ee2ca691fc2c489fd16bdbf1e1562c7e5e74ca2
|
|
7
|
+
data.tar.gz: a34a15a7eab1202fe44ebcf85e828c81d99941b1102f88242b8efada40884328a22369dd7860f27449401bd8a123c80cdf02305c9d19b5a0215f1a30ebcd6f31
|
data/README.md
CHANGED
|
@@ -153,6 +153,54 @@ Llmemory.configure do |config|
|
|
|
153
153
|
end
|
|
154
154
|
```
|
|
155
155
|
|
|
156
|
+
### Zero-Mem (experimental, opt-in)
|
|
157
|
+
|
|
158
|
+
Zero-Mem keeps **immutable traces** as the source of truth and retrieves evidence with deterministic profiling, hierarchy, and entity-graph ranking — **without generative LLM calls** in memory operations (`memory_mode: :zero_mem`). Default remains `:classic`. This is experimental: it does not reproduce published benchmark numbers.
|
|
159
|
+
|
|
160
|
+
```ruby
|
|
161
|
+
Llmemory.configure do |config|
|
|
162
|
+
config.memory_mode = :zero_mem # or :hybrid (traces + classic long-term; not zero-token)
|
|
163
|
+
config.zero_mem_trace_store = :memory # or :active_record in Rails apps
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
memory = Llmemory::Memory.new(user_id: "u1", session_id: "s1")
|
|
167
|
+
memory.add_message(role: :user, content: "Project Atlas uses Nimbus")
|
|
168
|
+
context = memory.retrieve("Which database does Atlas use?")
|
|
169
|
+
evidence = memory.retrieve_evidence("Which database does Atlas use?", explain: true)
|
|
170
|
+
|
|
171
|
+
# Explicit generative ops raise in :zero_mem
|
|
172
|
+
# memory.consolidate! # => Llmemory::GenerativeOperationDisabled
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
CLI: `llmemory zero-mem traces USER`, `status`, `reindex`, `search USER "query"`. MCP: `memory_retrieve` (text, same as before) and `memory_retrieve_evidence` (structured JSON + context).
|
|
176
|
+
|
|
177
|
+
Rollout: enable `zero_mem_shadow_write` while staying on `:classic` for reads; see [docs/zero_mem_limitations.md](docs/zero_mem_limitations.md). Benchmarks: [benchmarks/zero_mem/README.md](benchmarks/zero_mem/README.md).
|
|
178
|
+
|
|
179
|
+
## Consolidation policy
|
|
180
|
+
|
|
181
|
+
By default, llmemory consolidates every extracted fact into long-term memory. For agents with **authoritative live data** (tools, DB snapshots), configure a `Consolidation::Policy` to **drop** or **mark volatile** facts that should not be treated as stable memory (e.g. current job, plan status, task lists).
|
|
182
|
+
|
|
183
|
+
```ruby
|
|
184
|
+
Llmemory.configure do |config|
|
|
185
|
+
config.consolidation_policy = Llmemory::Consolidation::RuleBasedPolicy.new(
|
|
186
|
+
drop_predicates: %w[current_job works_at job_title plan_status has_task],
|
|
187
|
+
drop_categories: %w[work_life]
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
# Optional: prefix volatile memories at retrieval time with "[verify live]"
|
|
191
|
+
config.retrieval_label_volatile = true
|
|
192
|
+
end
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Policy decisions per extracted unit: `:keep` (default), `:drop` (not persisted), or `:volatile` (persisted with a volatile marker in provenance/properties).
|
|
196
|
+
|
|
197
|
+
To clean **legacy** data consolidated before a policy was enabled:
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
USER_ID=user_123 LONG_TERM_TYPE=graph_based bundle exec rake llmemory:prune_by_policy
|
|
201
|
+
DRY_RUN=1 bundle exec rake llmemory:prune_by_policy
|
|
202
|
+
```
|
|
203
|
+
|
|
156
204
|
## Encryption at rest
|
|
157
205
|
|
|
158
206
|
Optional AES-256-GCM encryption protects persisted memory. Without the key, stored data is unreadable — useful for isolating agents or tenants.
|
|
@@ -94,9 +94,80 @@ class CreateLlmemoryTables < ActiveRecord::Migration[7.0]
|
|
|
94
94
|
t.string :source_type, null: false
|
|
95
95
|
t.string :source_id, null: false
|
|
96
96
|
t.vector :embedding, limit: 1536
|
|
97
|
+
t.string :embedding_model
|
|
98
|
+
t.integer :embedding_dimensions
|
|
97
99
|
t.text :text_content
|
|
98
100
|
t.timestamps
|
|
99
101
|
end
|
|
100
102
|
add_index :llmemory_embeddings, [:user_id, :source_type, :source_id], unique: true
|
|
103
|
+
|
|
104
|
+
create_table :llmemory_traces, id: false do |t|
|
|
105
|
+
t.string :id, null: false, primary_key: true
|
|
106
|
+
t.string :user_id, null: false
|
|
107
|
+
t.string :session_id, null: false
|
|
108
|
+
t.string :boundary_id
|
|
109
|
+
t.bigint :sequence, null: false
|
|
110
|
+
t.string :role, null: false
|
|
111
|
+
t.text :content, null: false
|
|
112
|
+
t.text :search_tokens
|
|
113
|
+
t.string :content_sha256, null: false
|
|
114
|
+
t.datetime :occurred_at, null: false
|
|
115
|
+
t.datetime :ingested_at, null: false
|
|
116
|
+
t.jsonb :metadata, null: false, default: {}
|
|
117
|
+
t.string :idempotency_key
|
|
118
|
+
t.datetime :archived_at
|
|
119
|
+
t.timestamps
|
|
120
|
+
end
|
|
121
|
+
add_index :llmemory_traces, [:user_id, :session_id, :sequence], unique: true
|
|
122
|
+
add_index :llmemory_traces, [:user_id, :idempotency_key], unique: true, where: "idempotency_key IS NOT NULL"
|
|
123
|
+
add_index :llmemory_traces, [:user_id, :session_id, :occurred_at]
|
|
124
|
+
add_index :llmemory_traces, [:user_id, :boundary_id]
|
|
125
|
+
add_index :llmemory_traces, [:user_id, :archived_at]
|
|
126
|
+
|
|
127
|
+
create_table :llmemory_trace_state_links do |t|
|
|
128
|
+
t.string :user_id, null: false
|
|
129
|
+
t.string :state_key, null: false
|
|
130
|
+
t.string :trace_id, null: false
|
|
131
|
+
t.datetime :valid_from, null: false
|
|
132
|
+
t.datetime :valid_to
|
|
133
|
+
t.string :supersedes_trace_id
|
|
134
|
+
t.string :source, null: false, default: "explicit"
|
|
135
|
+
t.datetime :created_at, null: false
|
|
136
|
+
end
|
|
137
|
+
add_index :llmemory_trace_state_links, [:user_id, :state_key, :valid_from, :valid_to]
|
|
138
|
+
add_index :llmemory_trace_state_links, :trace_id
|
|
139
|
+
|
|
140
|
+
create_table :llmemory_trace_units, id: false do |t|
|
|
141
|
+
t.string :id, null: false, primary_key: true
|
|
142
|
+
t.string :user_id, null: false
|
|
143
|
+
t.string :trace_id, null: false
|
|
144
|
+
t.string :unit_type, null: false
|
|
145
|
+
t.jsonb :data, null: false, default: {}
|
|
146
|
+
t.datetime :archived_at
|
|
147
|
+
t.timestamps
|
|
148
|
+
end
|
|
149
|
+
add_index :llmemory_trace_units, [:user_id, :trace_id]
|
|
150
|
+
|
|
151
|
+
create_table :llmemory_trace_entities, id: false do |t|
|
|
152
|
+
t.string :id, null: false, primary_key: true
|
|
153
|
+
t.string :user_id, null: false
|
|
154
|
+
t.string :name, null: false
|
|
155
|
+
t.string :entity_type
|
|
156
|
+
t.datetime :archived_at
|
|
157
|
+
t.timestamps
|
|
158
|
+
end
|
|
159
|
+
add_index :llmemory_trace_entities, [:user_id, :name]
|
|
160
|
+
|
|
161
|
+
create_table :llmemory_entity_mentions, id: false do |t|
|
|
162
|
+
t.string :id, null: false, primary_key: true
|
|
163
|
+
t.string :user_id, null: false
|
|
164
|
+
t.string :trace_id, null: false
|
|
165
|
+
t.string :entity_id, null: false
|
|
166
|
+
t.integer :offset_start
|
|
167
|
+
t.integer :offset_end
|
|
168
|
+
t.datetime :archived_at
|
|
169
|
+
t.timestamps
|
|
170
|
+
end
|
|
171
|
+
add_index :llmemory_entity_mentions, [:user_id, :trace_id]
|
|
101
172
|
end
|
|
102
173
|
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateLlmemoryZeroMemTables < ActiveRecord::Migration[7.0]
|
|
4
|
+
def change
|
|
5
|
+
create_table :llmemory_traces, id: false do |t|
|
|
6
|
+
t.string :id, null: false, primary_key: true
|
|
7
|
+
t.string :user_id, null: false
|
|
8
|
+
t.string :session_id, null: false
|
|
9
|
+
t.string :boundary_id
|
|
10
|
+
t.bigint :sequence, null: false
|
|
11
|
+
t.string :role, null: false
|
|
12
|
+
t.text :content, null: false
|
|
13
|
+
t.text :search_tokens
|
|
14
|
+
t.string :content_sha256, null: false
|
|
15
|
+
t.datetime :occurred_at, null: false
|
|
16
|
+
t.datetime :ingested_at, null: false
|
|
17
|
+
t.jsonb :metadata, null: false, default: {}
|
|
18
|
+
t.string :idempotency_key
|
|
19
|
+
t.datetime :archived_at
|
|
20
|
+
t.timestamps
|
|
21
|
+
end
|
|
22
|
+
add_index :llmemory_traces, [:user_id, :session_id, :sequence], unique: true, name: "idx_llmemory_traces_user_session_sequence"
|
|
23
|
+
add_index :llmemory_traces, [:user_id, :idempotency_key], unique: true, where: "idempotency_key IS NOT NULL",
|
|
24
|
+
name: "idx_llmemory_traces_user_idempotency"
|
|
25
|
+
add_index :llmemory_traces, [:user_id, :session_id, :occurred_at], name: "idx_llmemory_traces_user_session_occurred"
|
|
26
|
+
add_index :llmemory_traces, [:user_id, :boundary_id], name: "idx_llmemory_traces_user_boundary"
|
|
27
|
+
add_index :llmemory_traces, [:user_id, :archived_at], name: "idx_llmemory_traces_user_archived"
|
|
28
|
+
|
|
29
|
+
create_table :llmemory_trace_state_links do |t|
|
|
30
|
+
t.string :user_id, null: false
|
|
31
|
+
t.string :state_key, null: false
|
|
32
|
+
t.string :trace_id, null: false
|
|
33
|
+
t.datetime :valid_from, null: false
|
|
34
|
+
t.datetime :valid_to
|
|
35
|
+
t.string :supersedes_trace_id
|
|
36
|
+
t.string :source, null: false, default: "explicit"
|
|
37
|
+
t.datetime :created_at, null: false
|
|
38
|
+
end
|
|
39
|
+
add_index :llmemory_trace_state_links, [:user_id, :state_key, :valid_from, :valid_to],
|
|
40
|
+
name: "idx_llmemory_trace_state_links_validity"
|
|
41
|
+
add_index :llmemory_trace_state_links, :trace_id, name: "idx_llmemory_trace_state_links_trace"
|
|
42
|
+
|
|
43
|
+
# Reserved for ZM2/ZM3 — created now to avoid chained migrations later.
|
|
44
|
+
create_table :llmemory_trace_units, id: false do |t|
|
|
45
|
+
t.string :id, null: false, primary_key: true
|
|
46
|
+
t.string :user_id, null: false
|
|
47
|
+
t.string :trace_id, null: false
|
|
48
|
+
t.string :unit_type, null: false
|
|
49
|
+
t.jsonb :data, null: false, default: {}
|
|
50
|
+
t.datetime :archived_at
|
|
51
|
+
t.timestamps
|
|
52
|
+
end
|
|
53
|
+
add_index :llmemory_trace_units, [:user_id, :trace_id], name: "idx_llmemory_trace_units_user_trace"
|
|
54
|
+
|
|
55
|
+
create_table :llmemory_trace_entities, id: false do |t|
|
|
56
|
+
t.string :id, null: false, primary_key: true
|
|
57
|
+
t.string :user_id, null: false
|
|
58
|
+
t.string :name, null: false
|
|
59
|
+
t.string :entity_type
|
|
60
|
+
t.datetime :archived_at
|
|
61
|
+
t.timestamps
|
|
62
|
+
end
|
|
63
|
+
add_index :llmemory_trace_entities, [:user_id, :name], name: "idx_llmemory_trace_entities_user_name"
|
|
64
|
+
|
|
65
|
+
create_table :llmemory_entity_mentions, id: false do |t|
|
|
66
|
+
t.string :id, null: false, primary_key: true
|
|
67
|
+
t.string :user_id, null: false
|
|
68
|
+
t.string :trace_id, null: false
|
|
69
|
+
t.string :entity_id, null: false
|
|
70
|
+
t.integer :offset_start
|
|
71
|
+
t.integer :offset_end
|
|
72
|
+
t.datetime :archived_at
|
|
73
|
+
t.timestamps
|
|
74
|
+
end
|
|
75
|
+
add_index :llmemory_entity_mentions, [:user_id, :trace_id], name: "idx_llmemory_entity_mentions_user_trace"
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
|
|
5
|
+
module Llmemory
|
|
6
|
+
module Generators
|
|
7
|
+
class UpgradeZeroMemGenerator < Rails::Generators::Base
|
|
8
|
+
source_root File.expand_path("templates", __dir__)
|
|
9
|
+
|
|
10
|
+
desc "Add Zero-Mem trace tables (llmemory_traces, state links, reserved index tables)"
|
|
11
|
+
|
|
12
|
+
def copy_migration
|
|
13
|
+
timestamp = Time.now.utc.strftime("%Y%m%d%H%M%S")
|
|
14
|
+
template "create_llmemory_zero_mem_tables.rb",
|
|
15
|
+
"db/migrate/#{timestamp}_create_llmemory_zero_mem_tables.rb"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require_relative "base"
|
|
5
|
+
|
|
6
|
+
module Llmemory
|
|
7
|
+
module Cli
|
|
8
|
+
module Commands
|
|
9
|
+
class ZeroMem < Base
|
|
10
|
+
def option_parser(parser)
|
|
11
|
+
parser.on("--session SESSION", "Session id (default: default)") { |v| @session_id = v }
|
|
12
|
+
parser.on("--explain", "Include explain payload on search") { @explain = true }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def execute(argv, _opts)
|
|
16
|
+
sub = argv.shift
|
|
17
|
+
case sub
|
|
18
|
+
when "traces"
|
|
19
|
+
run_traces(argv)
|
|
20
|
+
when "reindex"
|
|
21
|
+
run_reindex(argv)
|
|
22
|
+
when "status"
|
|
23
|
+
run_status(argv)
|
|
24
|
+
when "search"
|
|
25
|
+
run_search(argv)
|
|
26
|
+
else
|
|
27
|
+
print_usage
|
|
28
|
+
exit 1
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def print_usage
|
|
35
|
+
$stderr.puts <<~HELP
|
|
36
|
+
Usage:
|
|
37
|
+
llmemory zero-mem traces USER_ID [--session SESSION]
|
|
38
|
+
llmemory zero-mem reindex USER_ID [--session SESSION]
|
|
39
|
+
llmemory zero-mem status USER_ID [--session SESSION]
|
|
40
|
+
llmemory zero-mem search USER_ID "query" [--explain]
|
|
41
|
+
HELP
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def build_memory(user_id)
|
|
45
|
+
trace_store = Llmemory::ZeroMem::Storages.build
|
|
46
|
+
Llmemory::Memory.new(
|
|
47
|
+
user_id: user_id,
|
|
48
|
+
session_id: @session_id || "default",
|
|
49
|
+
trace_store: trace_store,
|
|
50
|
+
memory_mode: :zero_mem
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def run_traces(argv)
|
|
55
|
+
user_id = argv.shift
|
|
56
|
+
unless user_id
|
|
57
|
+
print_usage
|
|
58
|
+
exit 1
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
memory = build_memory(user_id)
|
|
62
|
+
traces = memory.trace_store.list_traces(user_id, session_id: @session_id || "default")
|
|
63
|
+
traces.each do |t|
|
|
64
|
+
puts "[#{t.id}] seq=#{t.sequence} #{t.role}: #{t.content[0, 120]}"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def run_reindex(argv)
|
|
69
|
+
user_id = argv.shift
|
|
70
|
+
unless user_id
|
|
71
|
+
print_usage
|
|
72
|
+
exit 1
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
memory = build_memory(user_id)
|
|
76
|
+
status = memory.reindex_traces!(session_id: @session_id || "default")
|
|
77
|
+
puts JSON.generate(status)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def run_status(argv)
|
|
81
|
+
user_id = argv.shift
|
|
82
|
+
unless user_id
|
|
83
|
+
print_usage
|
|
84
|
+
exit 1
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
memory = build_memory(user_id)
|
|
88
|
+
puts JSON.generate(memory.zero_mem_status(session_id: @session_id || "default"))
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def run_search(argv)
|
|
92
|
+
user_id = argv.shift
|
|
93
|
+
query = argv.join(" ").strip
|
|
94
|
+
unless user_id && !query.empty?
|
|
95
|
+
print_usage
|
|
96
|
+
exit 1
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
memory = build_memory(user_id)
|
|
100
|
+
result = memory.retrieve_evidence(query, explain: @explain == true)
|
|
101
|
+
puts result.to_context
|
|
102
|
+
puts JSON.generate(metrics: result.metrics, route: result.route) if @explain
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
data/lib/llmemory/cli.rb
CHANGED
|
@@ -13,6 +13,7 @@ require_relative "cli/commands/mine_skills"
|
|
|
13
13
|
require_relative "cli/commands/maintain"
|
|
14
14
|
require_relative "cli/commands/stats"
|
|
15
15
|
require_relative "cli/commands/search"
|
|
16
|
+
require_relative "cli/commands/zero_mem"
|
|
16
17
|
require_relative "cli/commands/mcp"
|
|
17
18
|
|
|
18
19
|
module Llmemory
|
|
@@ -60,6 +61,7 @@ module Llmemory
|
|
|
60
61
|
"mine_skills" => Cli::Commands::MineSkills,
|
|
61
62
|
"maintain" => Cli::Commands::Maintain,
|
|
62
63
|
"search" => Cli::Commands::Search,
|
|
64
|
+
"zero_mem" => Cli::Commands::ZeroMem,
|
|
63
65
|
"stats" => Cli::Commands::Stats,
|
|
64
66
|
"mcp" => Cli::Commands::Mcp
|
|
65
67
|
}[normalized]
|
|
@@ -87,6 +89,7 @@ module Llmemory
|
|
|
87
89
|
mine-skills USER_ID Mine reusable skills from episodes (--register to save)
|
|
88
90
|
maintain USER_ID Run the cognitive maintenance pass (reflect/mine/expire)
|
|
89
91
|
search USER_ID "query" Search in memory
|
|
92
|
+
zero-mem SUB ... Zero-Mem traces, reindex, status, search (experimental)
|
|
90
93
|
stats [USER_ID] Show statistics
|
|
91
94
|
mcp [serve] Start MCP server for LLM agents
|
|
92
95
|
|
|
@@ -57,7 +57,28 @@ module Llmemory
|
|
|
57
57
|
:encryption_key,
|
|
58
58
|
:shared_memory_stores,
|
|
59
59
|
:dashboard_auth,
|
|
60
|
-
:dashboard_require_auth
|
|
60
|
+
:dashboard_require_auth,
|
|
61
|
+
:consolidation_policy,
|
|
62
|
+
:retrieval_label_volatile,
|
|
63
|
+
:zero_mem_index_mode,
|
|
64
|
+
:zero_mem_trace_store,
|
|
65
|
+
:zero_mem_top_k,
|
|
66
|
+
:zero_mem_max_top_k,
|
|
67
|
+
:zero_mem_local_span,
|
|
68
|
+
:zero_mem_window_size,
|
|
69
|
+
:zero_mem_window_overlap,
|
|
70
|
+
:zero_mem_episode_gap_seconds,
|
|
71
|
+
:zero_mem_entity_extractor,
|
|
72
|
+
:zero_mem_embedding_provider,
|
|
73
|
+
:zero_mem_require_local_encoders,
|
|
74
|
+
:zero_mem_pagerank_damping,
|
|
75
|
+
:zero_mem_graph_weight,
|
|
76
|
+
:zero_mem_ner_http_url,
|
|
77
|
+
:memory_mode,
|
|
78
|
+
:zero_mem_ttl_days,
|
|
79
|
+
:zero_mem_sidecar_timeout_seconds,
|
|
80
|
+
:zero_mem_sidecar_failure_threshold,
|
|
81
|
+
:zero_mem_shadow_write
|
|
61
82
|
|
|
62
83
|
def initialize
|
|
63
84
|
@llm_provider = :openai
|
|
@@ -116,6 +137,50 @@ module Llmemory
|
|
|
116
137
|
@shared_memory_stores = false
|
|
117
138
|
@dashboard_auth = nil
|
|
118
139
|
@dashboard_require_auth = nil
|
|
140
|
+
@consolidation_policy = nil
|
|
141
|
+
@retrieval_label_volatile = false
|
|
142
|
+
@zero_mem_index_mode = :sync
|
|
143
|
+
@zero_mem_trace_store = :memory
|
|
144
|
+
@zero_mem_top_k = 5
|
|
145
|
+
@zero_mem_max_top_k = 10
|
|
146
|
+
@zero_mem_local_span = 2
|
|
147
|
+
@zero_mem_window_size = 8
|
|
148
|
+
@zero_mem_window_overlap = 2
|
|
149
|
+
@zero_mem_episode_gap_seconds = 86_400
|
|
150
|
+
@zero_mem_entity_extractor = :heuristic
|
|
151
|
+
@zero_mem_embedding_provider = nil
|
|
152
|
+
@zero_mem_require_local_encoders = false
|
|
153
|
+
@zero_mem_pagerank_damping = 0.6
|
|
154
|
+
@zero_mem_graph_weight = 0.6
|
|
155
|
+
@zero_mem_ner_http_url = "http://127.0.0.1:8765"
|
|
156
|
+
@memory_mode = :classic
|
|
157
|
+
@zero_mem_ttl_days = nil
|
|
158
|
+
@zero_mem_sidecar_timeout_seconds = 5
|
|
159
|
+
@zero_mem_sidecar_failure_threshold = 3
|
|
160
|
+
@zero_mem_shadow_write = false
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def validate!
|
|
164
|
+
mode = @memory_mode.to_sym
|
|
165
|
+
unless ZeroMem::Mode.valid?(mode)
|
|
166
|
+
raise ConfigurationError, "memory_mode must be :classic, :zero_mem, or :hybrid (got #{@memory_mode.inspect})"
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
rho = zero_mem_graph_weight.to_f
|
|
170
|
+
gamma = zero_mem_pagerank_damping.to_f
|
|
171
|
+
raise ConfigurationError, "zero_mem_graph_weight must be in [0, 1]" unless rho.between?(0.0, 1.0)
|
|
172
|
+
raise ConfigurationError, "zero_mem_pagerank_damping must be in [0, 1]" unless gamma.between?(0.0, 1.0)
|
|
173
|
+
raise ConfigurationError, "zero_mem_top_k must be positive" unless zero_mem_top_k.to_i.positive?
|
|
174
|
+
raise ConfigurationError, "zero_mem_max_top_k must be >= zero_mem_top_k" if zero_mem_max_top_k.to_i < zero_mem_top_k.to_i
|
|
175
|
+
|
|
176
|
+
extractor = zero_mem_entity_extractor.to_sym
|
|
177
|
+
unless %i[heuristic http].include?(extractor)
|
|
178
|
+
raise ConfigurationError, "zero_mem_entity_extractor must be :heuristic or :http"
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def consolidation_policy
|
|
183
|
+
@consolidation_policy ||= Consolidation::NullPolicy.new
|
|
119
184
|
end
|
|
120
185
|
|
|
121
186
|
def dashboard_require_auth?
|
|
@@ -138,6 +203,7 @@ module Llmemory
|
|
|
138
203
|
|
|
139
204
|
def configure
|
|
140
205
|
yield configuration
|
|
206
|
+
configuration.validate!
|
|
141
207
|
end
|
|
142
208
|
|
|
143
209
|
def reset_configuration!
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Llmemory
|
|
4
|
+
module Consolidation
|
|
5
|
+
module Filter
|
|
6
|
+
VOLATILE_PROPERTY = "consolidation_volatile"
|
|
7
|
+
VOLATILE_PROVENANCE_FLAG = "volatile"
|
|
8
|
+
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def policy
|
|
12
|
+
Llmemory.configuration.consolidation_policy || NullPolicy.new
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# @return [Array<Hash>] relations with :volatile key
|
|
16
|
+
def apply_relations(relations)
|
|
17
|
+
Array(relations).filter_map do |relation|
|
|
18
|
+
next unless relation.is_a?(Hash)
|
|
19
|
+
|
|
20
|
+
unit = Unit.from_relation(relation)
|
|
21
|
+
case policy.decide(unit)
|
|
22
|
+
when :drop
|
|
23
|
+
next
|
|
24
|
+
when :volatile
|
|
25
|
+
relation.merge(volatile: true)
|
|
26
|
+
else
|
|
27
|
+
relation.merge(volatile: false)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @return [Array<Hash>] item hashes with :volatile key
|
|
33
|
+
def apply_items(items, classifications = {})
|
|
34
|
+
Array(items).filter_map do |item|
|
|
35
|
+
content = item.is_a?(Hash) ? (item["content"] || item[:content]).to_s : item.to_s
|
|
36
|
+
category = classifications[content] || classifications[content.to_sym]
|
|
37
|
+
unit = Unit.from_item(item, category: category)
|
|
38
|
+
|
|
39
|
+
case policy.decide(unit)
|
|
40
|
+
when :drop
|
|
41
|
+
next
|
|
42
|
+
when :volatile
|
|
43
|
+
item_hash = item.is_a?(Hash) ? item.dup : {"content" => content}
|
|
44
|
+
item_hash.merge(volatile: true)
|
|
45
|
+
else
|
|
46
|
+
item_hash = item.is_a?(Hash) ? item.dup : {"content" => content}
|
|
47
|
+
item_hash.merge(volatile: false)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def volatile_provenance(base_provenance)
|
|
53
|
+
prov = base_provenance.is_a?(Hash) ? base_provenance.dup : {}
|
|
54
|
+
prov[:consolidation] = VOLATILE_PROVENANCE_FLAG
|
|
55
|
+
prov["consolidation"] = VOLATILE_PROVENANCE_FLAG unless prov.key?(:consolidation)
|
|
56
|
+
prov
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def volatile_properties(base_properties)
|
|
60
|
+
props = base_properties.is_a?(Hash) ? base_properties.dup : {}
|
|
61
|
+
props[VOLATILE_PROPERTY] = true
|
|
62
|
+
props
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def volatile_marked?(properties_or_provenance)
|
|
66
|
+
hash = properties_or_provenance || {}
|
|
67
|
+
hash[VOLATILE_PROPERTY] == true ||
|
|
68
|
+
hash["consolidation_volatile"] == true ||
|
|
69
|
+
hash[:consolidation] == VOLATILE_PROVENANCE_FLAG ||
|
|
70
|
+
hash["consolidation"] == VOLATILE_PROVENANCE_FLAG
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Llmemory
|
|
4
|
+
module Consolidation
|
|
5
|
+
# Base policy: all extracted units are kept in long-term memory.
|
|
6
|
+
class Policy
|
|
7
|
+
DECISIONS = %i[keep drop volatile].freeze
|
|
8
|
+
|
|
9
|
+
def decide(_unit)
|
|
10
|
+
:keep
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
class NullPolicy < Policy
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Llmemory
|
|
4
|
+
module Consolidation
|
|
5
|
+
# Rule-based consolidation policy for agents with authoritative live data sources
|
|
6
|
+
# (tools, DB snapshots). Drops or marks volatile facts that should not be treated
|
|
7
|
+
# as stable long-term memory.
|
|
8
|
+
#
|
|
9
|
+
# Precedence (first match wins):
|
|
10
|
+
# 1. drop_if callable
|
|
11
|
+
# 2. volatile_if callable
|
|
12
|
+
# 3. drop_predicates (graph relations)
|
|
13
|
+
# 4. volatile_predicates (graph relations)
|
|
14
|
+
# 5. drop_categories (file-based items)
|
|
15
|
+
# 6. volatile_categories (file-based items)
|
|
16
|
+
# 7. :keep
|
|
17
|
+
class RuleBasedPolicy < Policy
|
|
18
|
+
def initialize(
|
|
19
|
+
drop_predicates: [],
|
|
20
|
+
volatile_predicates: [],
|
|
21
|
+
drop_categories: [],
|
|
22
|
+
volatile_categories: [],
|
|
23
|
+
drop_if: nil,
|
|
24
|
+
volatile_if: nil
|
|
25
|
+
)
|
|
26
|
+
@drop_predicates = normalize_list(drop_predicates)
|
|
27
|
+
@volatile_predicates = normalize_list(volatile_predicates)
|
|
28
|
+
@drop_categories = normalize_list(drop_categories)
|
|
29
|
+
@volatile_categories = normalize_list(volatile_categories)
|
|
30
|
+
@drop_if = drop_if
|
|
31
|
+
@volatile_if = volatile_if
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def decide(unit)
|
|
35
|
+
return :drop if callable_true?(@drop_if, unit)
|
|
36
|
+
return :volatile if callable_true?(@volatile_if, unit)
|
|
37
|
+
|
|
38
|
+
case unit.kind
|
|
39
|
+
when :relation
|
|
40
|
+
predicate = Unit.normalize_predicate(unit.predicate)
|
|
41
|
+
return :drop if @drop_predicates.include?(predicate)
|
|
42
|
+
return :volatile if @volatile_predicates.include?(predicate)
|
|
43
|
+
when :item
|
|
44
|
+
category = Unit.normalize_category(unit.category)
|
|
45
|
+
return :drop if @drop_categories.include?(category)
|
|
46
|
+
return :volatile if @volatile_categories.include?(category)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
:keep
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def normalize_list(values)
|
|
55
|
+
Array(values).map { |value| value.to_s.strip.downcase.gsub(/\s+/, "_") }.reject(&:empty?)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def callable_true?(callable, unit)
|
|
59
|
+
callable.respond_to?(:call) && callable.call(unit) == true
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Llmemory
|
|
4
|
+
module Consolidation
|
|
5
|
+
# Normalized fact unit passed to Consolidation::Policy.
|
|
6
|
+
# kind: :relation (graph) | :item (file-based)
|
|
7
|
+
Unit = Data.define(:kind, :subject, :predicate, :object, :content, :category, :importance) do
|
|
8
|
+
def self.from_relation(relation)
|
|
9
|
+
new(
|
|
10
|
+
kind: :relation,
|
|
11
|
+
subject: normalize_string(relation[:subject] || relation["subject"]),
|
|
12
|
+
predicate: normalize_predicate(relation[:predicate] || relation["predicate"]),
|
|
13
|
+
object: normalize_string(relation[:object] || relation["object"]),
|
|
14
|
+
content: nil,
|
|
15
|
+
category: nil,
|
|
16
|
+
importance: nil
|
|
17
|
+
)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.from_item(item, category: nil)
|
|
21
|
+
content = if item.is_a?(Hash)
|
|
22
|
+
(item["content"] || item[:content]).to_s
|
|
23
|
+
else
|
|
24
|
+
item.to_s
|
|
25
|
+
end
|
|
26
|
+
importance = if item.is_a?(Hash)
|
|
27
|
+
item["importance"] || item[:importance]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
new(
|
|
31
|
+
kind: :item,
|
|
32
|
+
subject: nil,
|
|
33
|
+
predicate: nil,
|
|
34
|
+
object: nil,
|
|
35
|
+
content: content,
|
|
36
|
+
category: normalize_category(category),
|
|
37
|
+
importance: importance
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.normalize_predicate(value)
|
|
42
|
+
normalize_string(value).downcase.gsub(/\s+/, "_")
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def self.normalize_category(value)
|
|
46
|
+
normalize_string(value).downcase.gsub(/\s+/, "_")
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def self.normalize_string(value)
|
|
50
|
+
value.to_s.strip
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|