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
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Llmemory
|
|
4
|
+
module ZeroMem
|
|
5
|
+
# Append-only trace storage contract (ZM1).
|
|
6
|
+
module Storage
|
|
7
|
+
def write_trace(trace)
|
|
8
|
+
raise NotImplementedError
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def get_trace(user_id, trace_id)
|
|
12
|
+
raise NotImplementedError
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def find_by_idempotency_key(user_id, idempotency_key)
|
|
16
|
+
raise NotImplementedError
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def next_sequence(user_id, session_id)
|
|
20
|
+
raise NotImplementedError
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def list_traces(user_id, session_id: nil, include_archived: false, limit: nil, offset: nil)
|
|
24
|
+
raise NotImplementedError
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def archive_trace(user_id, trace_id, archived_at: Time.now)
|
|
28
|
+
raise NotImplementedError
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def write_state_link(link)
|
|
32
|
+
raise NotImplementedError
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def close_open_state_links(user_id, state_key, valid_to:, except_trace_id: nil)
|
|
36
|
+
raise NotImplementedError
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def state_links_for(user_id, state_key)
|
|
40
|
+
raise NotImplementedError
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def current_state_trace_id(user_id, state_key, as_of: Time.now)
|
|
44
|
+
raise NotImplementedError
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def get_watermark(user_id, session_id)
|
|
48
|
+
raise NotImplementedError
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def set_watermark(user_id, session_id, last_sequence:, index_version:)
|
|
52
|
+
raise NotImplementedError
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def search_traces_by_tokens(user_id, query, limit: 20)
|
|
56
|
+
raise NotImplementedError
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def write_unit(unit)
|
|
60
|
+
raise NotImplementedError
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def list_units(user_id, kind: nil, session_id: nil, boundary_id: nil)
|
|
64
|
+
raise NotImplementedError
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def get_unit(user_id, unit_id)
|
|
68
|
+
raise NotImplementedError
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def open_episode(user_id, episode_key)
|
|
72
|
+
raise NotImplementedError
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def set_open_episode(user_id, episode_key, unit_id)
|
|
76
|
+
raise NotImplementedError
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def close_episode(unit_id)
|
|
80
|
+
raise NotImplementedError
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def upsert_entity(entity)
|
|
84
|
+
raise NotImplementedError
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def write_mention(mention)
|
|
88
|
+
raise NotImplementedError
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def entity_exists?(user_id, normalized_key)
|
|
92
|
+
raise NotImplementedError
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def trace_ids_for_entity_key(user_id, normalized_key)
|
|
96
|
+
raise NotImplementedError
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def entity_keys_for_trace(user_id, trace_id)
|
|
100
|
+
raise NotImplementedError
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def previous_trace(user_id, session_id, sequence)
|
|
104
|
+
raise NotImplementedError
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def link_adjacent_traces(user_id, prev_trace_id, trace_id)
|
|
108
|
+
raise NotImplementedError
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def adjacent_traces(user_id, trace_id)
|
|
112
|
+
raise NotImplementedError
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def store_turn_embedding(user_id, trace_id, vector, model:, dimensions:)
|
|
116
|
+
raise NotImplementedError
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def all_entity_keys(user_id)
|
|
120
|
+
raise NotImplementedError
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require_relative "../storage"
|
|
5
|
+
require_relative "../../crypto/field_helpers"
|
|
6
|
+
require_relative "../../active_record_helpers"
|
|
7
|
+
|
|
8
|
+
module Llmemory
|
|
9
|
+
module ZeroMem
|
|
10
|
+
module Storages
|
|
11
|
+
class ActiveRecordStorage
|
|
12
|
+
include Storage
|
|
13
|
+
include Llmemory::Crypto::FieldHelpers
|
|
14
|
+
include Llmemory::ActiveRecordHelpers
|
|
15
|
+
|
|
16
|
+
def initialize(cipher: nil)
|
|
17
|
+
@cipher = cipher || Llmemory.build_cipher
|
|
18
|
+
self.class.load_models!
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.load_models!
|
|
22
|
+
return if @models_loaded
|
|
23
|
+
|
|
24
|
+
require "active_record"
|
|
25
|
+
require_relative "active_record_models"
|
|
26
|
+
@models_loaded = true
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def write_trace(trace)
|
|
30
|
+
t = trace.is_a?(Trace) ? trace : Trace.new(**trace)
|
|
31
|
+
if t.idempotency_key && (existing = find_by_idempotency_key(t.user_id, t.idempotency_key))
|
|
32
|
+
return existing.id
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
rec = LlmemoryTrace.find_or_initialize_by(id: t.id)
|
|
36
|
+
rec.user_id = t.user_id
|
|
37
|
+
rec.session_id = t.session_id
|
|
38
|
+
rec.boundary_id = t.boundary_id
|
|
39
|
+
rec.sequence = t.sequence
|
|
40
|
+
rec.role = t.role.to_s
|
|
41
|
+
rec.content = enc(t.content)
|
|
42
|
+
rec.content_sha256 = t.content_sha256
|
|
43
|
+
rec.occurred_at = t.occurred_at
|
|
44
|
+
rec.ingested_at = t.ingested_at
|
|
45
|
+
rec.metadata = metadata_column(t.metadata)
|
|
46
|
+
rec.idempotency_key = t.idempotency_key
|
|
47
|
+
rec.archived_at = t.archived_at
|
|
48
|
+
rec.search_tokens = search_tokens_for(t.content) if LlmemoryTrace.column_names.include?("search_tokens")
|
|
49
|
+
with_unique_retry { rec.save! }
|
|
50
|
+
t.id
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def get_trace(user_id, trace_id)
|
|
54
|
+
rec = LlmemoryTrace.find_by(id: trace_id.to_s, user_id: user_id.to_s)
|
|
55
|
+
return nil unless rec
|
|
56
|
+
|
|
57
|
+
record_to_trace(rec)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def find_by_idempotency_key(user_id, idempotency_key)
|
|
61
|
+
return nil if idempotency_key.to_s.empty?
|
|
62
|
+
|
|
63
|
+
rec = LlmemoryTrace.find_by(user_id: user_id.to_s, idempotency_key: idempotency_key.to_s)
|
|
64
|
+
rec ? record_to_trace(rec) : nil
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def next_sequence(user_id, session_id)
|
|
68
|
+
max = LlmemoryTrace.where(user_id: user_id.to_s, session_id: session_id.to_s).maximum(:sequence)
|
|
69
|
+
(max || 0) + 1
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def list_traces(user_id, session_id: nil, include_archived: false, limit: nil, offset: nil)
|
|
73
|
+
scope = LlmemoryTrace.where(user_id: user_id.to_s)
|
|
74
|
+
scope = scope.where(session_id: session_id.to_s) if session_id
|
|
75
|
+
scope = scope.where(archived_at: nil) unless include_archived
|
|
76
|
+
scope = scope.order(:session_id, :sequence, :ingested_at)
|
|
77
|
+
scope = scope.offset(offset.to_i) if offset
|
|
78
|
+
scope = scope.limit(limit.to_i) if limit && limit.to_i.positive?
|
|
79
|
+
scope.map { |r| record_to_trace(r) }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def archive_trace(user_id, trace_id, archived_at: Time.now)
|
|
83
|
+
rec = LlmemoryTrace.find_by(id: trace_id.to_s, user_id: user_id.to_s, archived_at: nil)
|
|
84
|
+
return false unless rec
|
|
85
|
+
|
|
86
|
+
rec.update!(archived_at: archived_at)
|
|
87
|
+
true
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def write_state_link(link)
|
|
91
|
+
rec = LlmemoryTraceStateLink.new(
|
|
92
|
+
user_id: link.user_id,
|
|
93
|
+
state_key: link.state_key,
|
|
94
|
+
trace_id: link.trace_id,
|
|
95
|
+
valid_from: link.valid_from,
|
|
96
|
+
valid_to: link.valid_to,
|
|
97
|
+
supersedes_trace_id: link.supersedes_trace_id,
|
|
98
|
+
source: link.source.to_s,
|
|
99
|
+
created_at: link.created_at
|
|
100
|
+
)
|
|
101
|
+
with_unique_retry { rec.save! }
|
|
102
|
+
link.trace_id
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def close_open_state_links(user_id, state_key, valid_to:, except_trace_id: nil)
|
|
106
|
+
scope = LlmemoryTraceStateLink.where(user_id: user_id.to_s, state_key: state_key.to_s, valid_to: nil)
|
|
107
|
+
scope = scope.where.not(trace_id: except_trace_id.to_s) if except_trace_id
|
|
108
|
+
scope.update_all(valid_to: valid_to)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def state_links_for(user_id, state_key)
|
|
112
|
+
LlmemoryTraceStateLink.where(user_id: user_id.to_s, state_key: state_key.to_s)
|
|
113
|
+
.order(:valid_from, :created_at)
|
|
114
|
+
.map { |r| record_to_state_link(r) }
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def current_state_trace_id(user_id, state_key, as_of: Time.now)
|
|
118
|
+
t = as_of.is_a?(Time) ? as_of : Time.parse(as_of.to_s)
|
|
119
|
+
link = LlmemoryTraceStateLink.where(user_id: user_id.to_s, state_key: state_key.to_s)
|
|
120
|
+
.where("valid_from <= ?", t)
|
|
121
|
+
.where("valid_to IS NULL OR valid_to >= ?", t)
|
|
122
|
+
.order(valid_from: :desc, created_at: :desc)
|
|
123
|
+
.first
|
|
124
|
+
link&.trace_id
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def get_watermark(user_id, session_id)
|
|
128
|
+
rec = LlmemoryTrace.where(user_id: user_id.to_s, session_id: session_id.to_s)
|
|
129
|
+
.order(sequence: :desc)
|
|
130
|
+
.limit(1)
|
|
131
|
+
.pick(:sequence)
|
|
132
|
+
{
|
|
133
|
+
last_sequence: rec.to_i,
|
|
134
|
+
index_version: rec.to_i
|
|
135
|
+
}
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def set_watermark(user_id, session_id, last_sequence:, index_version:)
|
|
139
|
+
# ZM1: watermark derived from traces; noop placeholder for repair/backfill API.
|
|
140
|
+
{ last_sequence: last_sequence.to_i, index_version: index_version.to_i }
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def write_unit(unit)
|
|
144
|
+
return unit.id unless defined?(LlmemoryTraceUnit)
|
|
145
|
+
|
|
146
|
+
data = unit.to_h
|
|
147
|
+
rec = LlmemoryTraceUnit.find_or_initialize_by(id: unit.id)
|
|
148
|
+
rec.user_id = unit.user_id
|
|
149
|
+
rec.trace_id = unit.member_trace_ids.first
|
|
150
|
+
rec.unit_type = unit.kind.to_s
|
|
151
|
+
rec.data = cipher.enabled? ? enc_json(data) : data
|
|
152
|
+
with_unique_retry { rec.save! }
|
|
153
|
+
unit.id
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def list_units(user_id, kind: nil, session_id: nil, boundary_id: nil)
|
|
157
|
+
return [] unless defined?(LlmemoryTraceUnit)
|
|
158
|
+
|
|
159
|
+
scope = LlmemoryTraceUnit.where(user_id: user_id.to_s, archived_at: nil)
|
|
160
|
+
scope = scope.where(unit_type: kind.to_s) if kind
|
|
161
|
+
scope.map { |r| unit_from_record(r) }
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def get_unit(user_id, unit_id)
|
|
165
|
+
return nil unless defined?(LlmemoryTraceUnit)
|
|
166
|
+
|
|
167
|
+
rec = LlmemoryTraceUnit.find_by(id: unit_id.to_s, user_id: user_id.to_s)
|
|
168
|
+
rec ? unit_from_record(rec) : nil
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def open_episode(_user_id, _episode_key)
|
|
172
|
+
nil
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def set_open_episode(_user_id, _episode_key, _unit_id)
|
|
176
|
+
nil
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def close_episode(_unit_id)
|
|
180
|
+
nil
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def upsert_entity(_entity)
|
|
184
|
+
nil
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def write_mention(_mention)
|
|
188
|
+
nil
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def entity_exists?(_user_id, _normalized_key)
|
|
192
|
+
false
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def trace_ids_for_entity_key(_user_id, _normalized_key)
|
|
196
|
+
[]
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def entity_keys_for_trace(_user_id, _trace_id)
|
|
200
|
+
[]
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def previous_trace(user_id, session_id, sequence)
|
|
204
|
+
list_traces(user_id, session_id: session_id).reverse.find { |t| t.sequence < sequence.to_i }
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def link_adjacent_traces(_user_id, _prev_trace_id, _trace_id)
|
|
208
|
+
nil
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def adjacent_traces(_user_id, _trace_id)
|
|
212
|
+
[nil, nil]
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def store_turn_embedding(_user_id, _trace_id, _vector, model:, dimensions:)
|
|
216
|
+
nil
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def all_entity_keys(_user_id)
|
|
220
|
+
[]
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def search_traces_by_tokens(user_id, query, limit: 20)
|
|
224
|
+
scope = LlmemoryTrace.where(user_id: user_id.to_s, archived_at: nil)
|
|
225
|
+
tokens = Llmemory::Tokenizer.tokenize(query)
|
|
226
|
+
return [] if tokens.empty?
|
|
227
|
+
|
|
228
|
+
if cipher.enabled? && LlmemoryTrace.column_names.include?("search_tokens")
|
|
229
|
+
digests = tokens.map { |tok| cipher.blind_index(tok) }
|
|
230
|
+
clause = digests.map { "search_tokens LIKE ?" }.join(" OR ")
|
|
231
|
+
scope = scope.where(clause, *digests.map { |d| "% #{d} %" })
|
|
232
|
+
return scope.limit(limit).map { |r| record_to_trace(r) }
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
list_traces(user_id, limit: limit).select do |t|
|
|
236
|
+
down = t.content.downcase
|
|
237
|
+
tokens.all? { |tok| down.include?(tok) }
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
private
|
|
242
|
+
|
|
243
|
+
def cipher
|
|
244
|
+
@cipher
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def metadata_column(metadata)
|
|
248
|
+
meta = metadata || {}
|
|
249
|
+
cipher.enabled? ? enc_json(meta) : meta
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def decode_metadata(value)
|
|
253
|
+
return {} if value.nil?
|
|
254
|
+
return dec_json(value) if cipher.enabled? && value.is_a?(String)
|
|
255
|
+
|
|
256
|
+
value.is_a?(Hash) ? value.transform_keys(&:to_sym) : {}
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def record_to_trace(rec)
|
|
260
|
+
Trace.new(
|
|
261
|
+
id: rec.id,
|
|
262
|
+
user_id: rec.user_id,
|
|
263
|
+
session_id: rec.session_id,
|
|
264
|
+
boundary_id: rec.boundary_id,
|
|
265
|
+
sequence: rec.sequence,
|
|
266
|
+
role: rec.role.to_sym,
|
|
267
|
+
content: dec(rec.content),
|
|
268
|
+
occurred_at: rec.occurred_at,
|
|
269
|
+
ingested_at: rec.ingested_at,
|
|
270
|
+
metadata: decode_metadata(rec.metadata),
|
|
271
|
+
content_sha256: rec.content_sha256,
|
|
272
|
+
idempotency_key: rec.idempotency_key,
|
|
273
|
+
archived_at: rec.archived_at
|
|
274
|
+
)
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def record_to_state_link(rec)
|
|
278
|
+
TraceStateLink.new(
|
|
279
|
+
user_id: rec.user_id,
|
|
280
|
+
state_key: rec.state_key,
|
|
281
|
+
trace_id: rec.trace_id,
|
|
282
|
+
valid_from: rec.valid_from,
|
|
283
|
+
valid_to: rec.valid_to,
|
|
284
|
+
supersedes_trace_id: rec.supersedes_trace_id,
|
|
285
|
+
source: rec.source.to_sym,
|
|
286
|
+
created_at: rec.created_at
|
|
287
|
+
)
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def unit_from_record(rec)
|
|
291
|
+
data = rec.data.is_a?(Hash) ? rec.data : dec_json(rec.data)
|
|
292
|
+
data = data.transform_keys(&:to_sym)
|
|
293
|
+
TraceUnit.new(
|
|
294
|
+
id: data[:id] || rec.id,
|
|
295
|
+
user_id: data[:user_id] || rec.user_id,
|
|
296
|
+
kind: (data[:kind] || rec.unit_type).to_sym,
|
|
297
|
+
session_id: data[:session_id],
|
|
298
|
+
boundary_id: data[:boundary_id],
|
|
299
|
+
start_sequence: data[:start_sequence],
|
|
300
|
+
end_sequence: data[:end_sequence],
|
|
301
|
+
occurred_from: Time.parse(data[:occurred_from].to_s),
|
|
302
|
+
occurred_to: Time.parse(data[:occurred_to].to_s),
|
|
303
|
+
member_trace_ids: data[:member_trace_ids] || [],
|
|
304
|
+
embedding_ref: data[:embedding_ref],
|
|
305
|
+
index_version: data[:index_version] || 0
|
|
306
|
+
)
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Llmemory
|
|
4
|
+
module ZeroMem
|
|
5
|
+
module Storages
|
|
6
|
+
class LlmemoryTrace < ::ActiveRecord::Base
|
|
7
|
+
self.table_name = "llmemory_traces"
|
|
8
|
+
self.primary_key = "id"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
class LlmemoryTraceStateLink < ::ActiveRecord::Base
|
|
12
|
+
self.table_name = "llmemory_trace_state_links"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
class LlmemoryTraceUnit < ::ActiveRecord::Base
|
|
16
|
+
self.table_name = "llmemory_trace_units"
|
|
17
|
+
self.primary_key = "id"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "json"
|
|
5
|
+
require_relative "../storage"
|
|
6
|
+
require_relative "memory"
|
|
7
|
+
require_relative "snapshot_codec"
|
|
8
|
+
require_relative "../../crypto/field_helpers"
|
|
9
|
+
|
|
10
|
+
module Llmemory
|
|
11
|
+
module ZeroMem
|
|
12
|
+
module Storages
|
|
13
|
+
class FileStorage
|
|
14
|
+
include Storage
|
|
15
|
+
include Llmemory::Crypto::FieldHelpers
|
|
16
|
+
|
|
17
|
+
MUTATORS = %i[
|
|
18
|
+
write_trace archive_trace write_state_link close_open_state_links set_watermark
|
|
19
|
+
write_unit set_open_episode close_episode upsert_entity write_mention
|
|
20
|
+
link_adjacent_traces store_turn_embedding
|
|
21
|
+
].freeze
|
|
22
|
+
|
|
23
|
+
def initialize(base_path: nil, cipher: nil)
|
|
24
|
+
@base_path = File.expand_path(base_path || Llmemory.configuration.long_term_storage_path || "./llmemory_data")
|
|
25
|
+
@cipher = cipher || Llmemory.build_cipher
|
|
26
|
+
@inner = Memory.new
|
|
27
|
+
load!
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
Storage.instance_methods(false).each do |method_name|
|
|
31
|
+
next if MUTATORS.include?(method_name)
|
|
32
|
+
|
|
33
|
+
define_method(method_name) do |*args, **kwargs, &block|
|
|
34
|
+
@inner.public_send(method_name, *args, **kwargs, &block)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
MUTATORS.each do |method_name|
|
|
39
|
+
define_method(method_name) do |*args, **kwargs, &block|
|
|
40
|
+
result = @inner.public_send(method_name, *args, **kwargs, &block)
|
|
41
|
+
save!
|
|
42
|
+
result
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def snapshot_path
|
|
49
|
+
File.join(@base_path, "zero_mem", "snapshot.json")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def load!
|
|
53
|
+
path = snapshot_path
|
|
54
|
+
return unless File.file?(path)
|
|
55
|
+
|
|
56
|
+
data = read_encrypted_file(path)
|
|
57
|
+
SnapshotCodec.load!(@inner, data)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def save!
|
|
61
|
+
FileUtils.mkdir_p(File.dirname(snapshot_path))
|
|
62
|
+
write_encrypted_file(snapshot_path, SnapshotCodec.dump(@inner))
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|