engram 0.4.0 → 0.6.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/CHANGELOG.md +113 -0
- data/README.md +217 -11
- data/lib/engram/adapters/in_memory_processed_turns.rb +40 -8
- data/lib/engram/adapters/in_memory_store.rb +30 -11
- data/lib/engram/adapters/null_embedder.rb +9 -0
- data/lib/engram/adapters/pgvector_store.rb +50 -17
- data/lib/engram/adapters/ruby_llm_embedder.rb +45 -4
- data/lib/engram/consolidators/heuristic_consolidator.rb +7 -1
- data/lib/engram/consolidators/llm_consolidator.rb +37 -10
- data/lib/engram/embedding_metadata.rb +135 -0
- data/lib/engram/extraction.rb +30 -0
- data/lib/engram/extractors/llm_extractor.rb +4 -3
- data/lib/engram/internal/candidate_integrity.rb +510 -0
- data/lib/engram/internal/core_hash.rb +36 -0
- data/lib/engram/internal/scope.rb +31 -0
- data/lib/engram/memory.rb +28 -1
- data/lib/engram/persistence.rb +83 -8
- data/lib/engram/persistence_policy.rb +11 -1
- data/lib/engram/ports/consolidator.rb +7 -2
- data/lib/engram/ports/extractor.rb +1 -1
- data/lib/engram/ports/memory_store.rb +25 -7
- data/lib/engram/ports/processed_turns.rb +16 -8
- data/lib/engram/provenance.rb +588 -0
- data/lib/engram/rails/cache_processed_turns.rb +51 -10
- data/lib/engram/rails/observe_job.rb +5 -0
- data/lib/engram/rails/tasks.rake +26 -0
- data/lib/engram/railtie.rb +4 -0
- data/lib/engram/record.rb +12 -5
- data/lib/engram/reserved_metadata.rb +52 -0
- data/lib/engram/use_cases/forget.rb +6 -2
- data/lib/engram/use_cases/grounding_report.rb +44 -0
- data/lib/engram/use_cases/observe.rb +300 -26
- data/lib/engram/use_cases/rebuild_embeddings.rb +189 -0
- data/lib/engram/use_cases/recall.rb +12 -4
- data/lib/engram/use_cases/source_impact.rb +42 -0
- data/lib/engram/version.rb +1 -1
- data/lib/engram.rb +13 -0
- metadata +14 -3
|
@@ -28,7 +28,8 @@ module Engram
|
|
|
28
28
|
to_record(row)
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
-
def search(embedding:, scope:, limit:, kinds: nil)
|
|
31
|
+
def search(embedding:, scope:, limit:, kinds: nil, embedding_metadata: nil)
|
|
32
|
+
Engram::EmbeddingMetadata.validate_query!(embedding, embedding_metadata)
|
|
32
33
|
query = model.where(scope: scope)
|
|
33
34
|
normalized_kinds = normalize_kinds(kinds)
|
|
34
35
|
query = query.where(kind: normalized_kinds) if normalized_kinds
|
|
@@ -37,30 +38,56 @@ module Engram
|
|
|
37
38
|
.nearest_neighbors(:embedding, embedding, distance: "cosine")
|
|
38
39
|
.limit(limit)
|
|
39
40
|
.map { |row| to_record(row) }
|
|
41
|
+
.tap { |records| validate_query_records!(records, embedding, embedding_metadata) }
|
|
40
42
|
end
|
|
41
43
|
|
|
42
|
-
def all(scope:)
|
|
43
|
-
model.where(scope: scope).
|
|
44
|
+
def all(scope:, limit: nil, offset: 0, after_id: nil)
|
|
45
|
+
query = model.where(scope: scope).order(:id)
|
|
46
|
+
query = query.where("id > ?", after_id) if after_id
|
|
47
|
+
query = query.limit(limit) if limit
|
|
48
|
+
query = query.offset(offset) if offset > 0
|
|
49
|
+
query.map { |row| to_record(row) }
|
|
44
50
|
end
|
|
45
51
|
|
|
46
|
-
def
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
def existing_ids(scope:, ids:)
|
|
53
|
+
persisted_ids = model.where(scope: scope, id: ids).pluck(:id)
|
|
54
|
+
id_type = model.type_for_attribute("id")
|
|
55
|
+
persisted_lookup = persisted_ids.each_with_object({}) { |id, lookup| lookup[id] = true }
|
|
56
|
+
matched_ids = {}
|
|
57
|
+
|
|
58
|
+
ids.select do |id|
|
|
59
|
+
cast_id = id_type.cast(id)
|
|
60
|
+
next false unless persisted_lookup[cast_id]
|
|
61
|
+
next false if matched_ids[cast_id]
|
|
62
|
+
|
|
63
|
+
matched_ids[cast_id] = true
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def update(scope:, id:, record:)
|
|
68
|
+
raise Engram::Error, "cannot move memory across scopes" unless record.scope == scope
|
|
69
|
+
|
|
70
|
+
model.transaction do
|
|
71
|
+
row = model.lock.find_by!(id: id, scope: scope)
|
|
72
|
+
row.update!(
|
|
73
|
+
content: record.content,
|
|
74
|
+
kind: record.kind.to_s,
|
|
75
|
+
importance: record.importance,
|
|
76
|
+
metadata: record.metadata,
|
|
77
|
+
embedding: record.embedding
|
|
78
|
+
)
|
|
79
|
+
to_record(row)
|
|
80
|
+
end
|
|
81
|
+
rescue ActiveRecord::RecordNotFound
|
|
82
|
+
raise Engram::Error, "no memory with id #{id.inspect} in scope #{scope.inspect}"
|
|
56
83
|
end
|
|
57
84
|
|
|
58
|
-
def delete(id:)
|
|
59
|
-
model.where(id: id).delete_all
|
|
85
|
+
def delete(scope:, id:)
|
|
86
|
+
model.where(id: id, scope: scope).delete_all
|
|
60
87
|
end
|
|
61
88
|
|
|
62
|
-
def touch(id:, at: Time.now)
|
|
63
|
-
model.where(id: id).update_all(last_accessed_at: at)
|
|
89
|
+
def touch(scope:, id:, at: Time.now)
|
|
90
|
+
model.where(id: id, scope: scope).update_all(last_accessed_at: at)
|
|
64
91
|
end
|
|
65
92
|
|
|
66
93
|
private
|
|
@@ -69,6 +96,12 @@ module Engram
|
|
|
69
96
|
raise Engram::Error, "memory scope cannot be nil" if scope.nil?
|
|
70
97
|
end
|
|
71
98
|
|
|
99
|
+
def validate_query_records!(records, embedding, embedding_metadata)
|
|
100
|
+
records.each do |record|
|
|
101
|
+
Engram::EmbeddingMetadata.validate_record!(record, embedding, embedding_metadata)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
72
105
|
def model
|
|
73
106
|
@model ||= resolve_default_model
|
|
74
107
|
end
|
|
@@ -10,16 +10,32 @@ module Engram
|
|
|
10
10
|
DEFAULT_MODEL = "text-embedding-3-small"
|
|
11
11
|
DEFAULT_DIMENSIONS = 1536
|
|
12
12
|
|
|
13
|
-
def initialize(model: DEFAULT_MODEL, dimensions:
|
|
13
|
+
def initialize(model: DEFAULT_MODEL, dimensions: nil)
|
|
14
|
+
if dimensions && (!dimensions.is_a?(Integer) || !dimensions.positive?)
|
|
15
|
+
raise ArgumentError, "dimensions must be a positive integer"
|
|
16
|
+
end
|
|
17
|
+
|
|
14
18
|
@model = model
|
|
15
|
-
@
|
|
19
|
+
@requested_dimensions = dimensions
|
|
20
|
+
@dimensions = dimensions || DEFAULT_DIMENSIONS
|
|
16
21
|
end
|
|
17
22
|
|
|
18
|
-
attr_reader :dimensions
|
|
23
|
+
attr_reader :dimensions, :model
|
|
24
|
+
|
|
25
|
+
def embedding_metadata
|
|
26
|
+
Engram::EmbeddingMetadata.build(
|
|
27
|
+
adapter: self.class.name,
|
|
28
|
+
provider: "ruby_llm",
|
|
29
|
+
model: model,
|
|
30
|
+
dimensions: dimensions
|
|
31
|
+
)
|
|
32
|
+
end
|
|
19
33
|
|
|
20
34
|
def embed(text)
|
|
21
35
|
ensure_ruby_llm!
|
|
22
|
-
|
|
36
|
+
vectors = request_embedding(text)
|
|
37
|
+
validate_dimensions!(vectors)
|
|
38
|
+
vectors
|
|
23
39
|
end
|
|
24
40
|
|
|
25
41
|
private
|
|
@@ -30,6 +46,31 @@ module Engram
|
|
|
30
46
|
raise Engram::Error,
|
|
31
47
|
"RubyLLMEmbedder requires the `ruby_llm` gem. Add it to your Gemfile and configure it."
|
|
32
48
|
end
|
|
49
|
+
|
|
50
|
+
# Older RubyLLM versions do not accept the dimensions keyword.
|
|
51
|
+
def request_embedding(text)
|
|
52
|
+
if @requested_dimensions && embed_accepts_dimensions?
|
|
53
|
+
RubyLLM.embed(text, model: @model, dimensions: @requested_dimensions).vectors
|
|
54
|
+
else
|
|
55
|
+
RubyLLM.embed(text, model: @model).vectors
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def embed_accepts_dimensions?
|
|
60
|
+
RubyLLM.method(:embed).parameters.any? do |type, name|
|
|
61
|
+
type == :keyrest || ([:key, :keyreq].include?(type) && name == :dimensions)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def validate_dimensions!(vectors)
|
|
66
|
+
length = vectors.respond_to?(:length) ? vectors.length : nil
|
|
67
|
+
return if length == @dimensions
|
|
68
|
+
|
|
69
|
+
raise Engram::Error,
|
|
70
|
+
"embedding model #{@model.inspect} returned #{length ? "#{length}-dimension" : "non-vector"} output " \
|
|
71
|
+
"but the embedder is configured with dimensions: #{@dimensions}; align the dimensions option " \
|
|
72
|
+
"(and your vector column) with the model's output"
|
|
73
|
+
end
|
|
33
74
|
end
|
|
34
75
|
end
|
|
35
76
|
end
|
|
@@ -15,7 +15,13 @@ module Engram
|
|
|
15
15
|
|
|
16
16
|
def reconcile_all(candidates:, scope:)
|
|
17
17
|
Array(candidates).map do |candidate|
|
|
18
|
-
nearest =
|
|
18
|
+
nearest = Engram::EmbeddingMetadata.search(
|
|
19
|
+
@store,
|
|
20
|
+
embedding: candidate.embedding,
|
|
21
|
+
embedding_metadata: Engram::EmbeddingMetadata.extract(candidate.metadata),
|
|
22
|
+
scope: scope,
|
|
23
|
+
limit: 1
|
|
24
|
+
).first
|
|
19
25
|
similarity = nearest ? Engram::Math.cosine_similarity(candidate.embedding, nearest.embedding) : 0.0
|
|
20
26
|
|
|
21
27
|
if nearest && similarity >= @similarity_threshold
|
|
@@ -56,23 +56,35 @@ module Engram
|
|
|
56
56
|
candidates = Array(candidates)
|
|
57
57
|
return [] if candidates.empty?
|
|
58
58
|
|
|
59
|
+
neighbors = neighbor_map(candidates, scope)
|
|
59
60
|
result = @completion.complete(
|
|
60
61
|
system: SYSTEM,
|
|
61
|
-
user: JSON.generate(payload(candidates,
|
|
62
|
+
user: JSON.generate(payload(candidates, neighbors)),
|
|
62
63
|
schema: SCHEMA
|
|
63
64
|
)
|
|
64
|
-
map_decisions(decisions(result), candidates)
|
|
65
|
+
map_decisions(decisions(result), candidates, neighbors)
|
|
65
66
|
end
|
|
66
67
|
|
|
67
68
|
private
|
|
68
69
|
|
|
69
|
-
def
|
|
70
|
+
def neighbor_map(candidates, scope)
|
|
71
|
+
candidates.map do |candidate|
|
|
72
|
+
Engram::EmbeddingMetadata.search(
|
|
73
|
+
@store,
|
|
74
|
+
embedding: candidate.embedding,
|
|
75
|
+
embedding_metadata: Engram::EmbeddingMetadata.extract(candidate.metadata),
|
|
76
|
+
scope: scope,
|
|
77
|
+
limit: @neighbors
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def payload(candidates, neighbors)
|
|
70
83
|
items = candidates.each_with_index.map do |candidate, index|
|
|
71
|
-
existing = @store.search(embedding: candidate.embedding, scope: scope, limit: @neighbors)
|
|
72
84
|
{
|
|
73
85
|
index: index,
|
|
74
86
|
candidate: candidate.content,
|
|
75
|
-
existing:
|
|
87
|
+
existing: neighbors[index].map { |r| {id: r.id, content: r.content} }
|
|
76
88
|
}
|
|
77
89
|
end
|
|
78
90
|
{candidates: items}
|
|
@@ -81,19 +93,34 @@ module Engram
|
|
|
81
93
|
def decisions(result)
|
|
82
94
|
return [] unless result.is_a?(Hash)
|
|
83
95
|
|
|
84
|
-
result["decisions"] || result[:decisions] || []
|
|
96
|
+
decisions = result["decisions"] || result[:decisions] || []
|
|
97
|
+
decisions.is_a?(Array) ? decisions : []
|
|
85
98
|
end
|
|
86
99
|
|
|
87
|
-
def map_decisions(raw, candidates)
|
|
100
|
+
def map_decisions(raw, candidates, neighbors)
|
|
101
|
+
seen = Set.new
|
|
88
102
|
raw.filter_map do |decision|
|
|
103
|
+
next unless decision.is_a?(Hash)
|
|
104
|
+
|
|
89
105
|
decision = decision.transform_keys(&:to_s)
|
|
90
106
|
index = decision["index"]
|
|
91
|
-
next unless index && candidates[index]
|
|
107
|
+
next unless index.is_a?(Integer) && index >= 0 && candidates[index]
|
|
108
|
+
next if seen.include?(index)
|
|
109
|
+
|
|
110
|
+
action = (decision["action"] || "noop").to_s
|
|
111
|
+
next unless Engram::Decision::ACTIONS.include?(action.to_sym)
|
|
112
|
+
|
|
113
|
+
target_id = decision["target_id"]
|
|
114
|
+
if %w[update forget].include?(action)
|
|
115
|
+
next if target_id.nil?
|
|
116
|
+
next unless neighbors[index].any? { |neighbor| neighbor.id == target_id }
|
|
117
|
+
end
|
|
92
118
|
|
|
119
|
+
seen.add(index)
|
|
93
120
|
Engram::Decision.new(
|
|
94
|
-
action:
|
|
121
|
+
action: action.to_sym,
|
|
95
122
|
candidate: candidates[index],
|
|
96
|
-
target_id:
|
|
123
|
+
target_id: target_id,
|
|
97
124
|
reason: decision["reason"]
|
|
98
125
|
)
|
|
99
126
|
end
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
5
|
+
module Engram
|
|
6
|
+
# Helpers for storing and validating embedding provenance under Engram's
|
|
7
|
+
# reserved metadata namespace.
|
|
8
|
+
module EmbeddingMetadata
|
|
9
|
+
RESERVED_KEY = "_engram"
|
|
10
|
+
EMBEDDING_KEY = "embedding"
|
|
11
|
+
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def build(adapter:, dimensions:, model: nil, provider: nil)
|
|
15
|
+
data = {
|
|
16
|
+
"adapter" => adapter.to_s,
|
|
17
|
+
"dimensions" => dimensions
|
|
18
|
+
}
|
|
19
|
+
data["provider"] = provider.to_s if provider
|
|
20
|
+
data["model"] = model.to_s if model
|
|
21
|
+
data["fingerprint"] = fingerprint(data)
|
|
22
|
+
data
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def for_embedder(embedder, embedding: nil)
|
|
26
|
+
return nil unless embedder.respond_to?(:embedding_metadata)
|
|
27
|
+
|
|
28
|
+
metadata = stringify_keys(embedder.embedding_metadata || {})
|
|
29
|
+
return nil if metadata.empty?
|
|
30
|
+
|
|
31
|
+
dimensions = embedding.respond_to?(:length) ? embedding.length : metadata["dimensions"]
|
|
32
|
+
metadata = metadata.merge("dimensions" => dimensions) if dimensions
|
|
33
|
+
metadata["fingerprint"] = fingerprint(metadata)
|
|
34
|
+
metadata
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def attach(record, embedder: nil, embedding_metadata: nil)
|
|
38
|
+
metadata = stringify_keys(embedding_metadata || for_embedder(embedder, embedding: record.embedding)) || {}
|
|
39
|
+
return record if metadata.empty?
|
|
40
|
+
|
|
41
|
+
record.with(metadata: merge(record.metadata, metadata))
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def extract(metadata)
|
|
45
|
+
metadata = stringify_keys(metadata || {})
|
|
46
|
+
reserved = metadata[RESERVED_KEY]
|
|
47
|
+
return {} unless reserved.is_a?(Hash)
|
|
48
|
+
|
|
49
|
+
stringify_keys(reserved[EMBEDDING_KEY] || {})
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def merge(metadata, embedding_metadata)
|
|
53
|
+
Engram::ReservedMetadata.attach(
|
|
54
|
+
metadata, EMBEDDING_KEY, embedding_metadata || {}, namespace_description: "embedding metadata"
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def search(store, embedding:, embedding_metadata:, scope:, limit:, kinds: nil)
|
|
59
|
+
store.search(
|
|
60
|
+
embedding: embedding,
|
|
61
|
+
embedding_metadata: embedding_metadata,
|
|
62
|
+
scope: scope,
|
|
63
|
+
limit: limit,
|
|
64
|
+
kinds: kinds
|
|
65
|
+
)
|
|
66
|
+
rescue ArgumentError => error
|
|
67
|
+
raise unless unknown_embedding_metadata_keyword?(error)
|
|
68
|
+
|
|
69
|
+
store.search(embedding: embedding, scope: scope, limit: limit, kinds: kinds)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def validate_query!(embedding, embedding_metadata)
|
|
73
|
+
metadata = stringify_keys(embedding_metadata || {})
|
|
74
|
+
return if metadata.empty?
|
|
75
|
+
|
|
76
|
+
expected = metadata["dimensions"]
|
|
77
|
+
return unless expected && embedding.respond_to?(:length) && embedding.length != expected.to_i
|
|
78
|
+
|
|
79
|
+
raise Engram::Error,
|
|
80
|
+
"embedding dimension mismatch: query vector has #{embedding.length} dimensions, metadata declares #{expected}"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def validate_record!(record, query_embedding, query_metadata)
|
|
84
|
+
if query_embedding.respond_to?(:length) && record.embedding.respond_to?(:length) &&
|
|
85
|
+
query_embedding.length != record.embedding.length
|
|
86
|
+
raise Engram::Error,
|
|
87
|
+
"embedding dimension mismatch: query vector has #{query_embedding.length} dimensions, " \
|
|
88
|
+
"record #{record.id.inspect} has #{record.embedding.length}"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
stored = extract(record.metadata)
|
|
92
|
+
return if stored.empty?
|
|
93
|
+
|
|
94
|
+
stored_dimensions = stored["dimensions"]
|
|
95
|
+
if stored_dimensions && record.embedding.respond_to?(:length) && record.embedding.length != stored_dimensions.to_i
|
|
96
|
+
raise Engram::Error,
|
|
97
|
+
"embedding metadata mismatch: record #{record.id.inspect} has #{record.embedding.length} dimensions, " \
|
|
98
|
+
"metadata declares #{stored_dimensions}"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
query_metadata = stringify_keys(query_metadata || {})
|
|
102
|
+
return if query_metadata.empty?
|
|
103
|
+
|
|
104
|
+
conflicting_key = %w[adapter provider model dimensions fingerprint].find do |key|
|
|
105
|
+
stored.key?(key) && query_metadata.key?(key) && stored[key].to_s != query_metadata[key].to_s
|
|
106
|
+
end
|
|
107
|
+
return unless conflicting_key
|
|
108
|
+
|
|
109
|
+
raise Engram::Error,
|
|
110
|
+
"embedding metadata mismatch for record #{record.id.inspect}: #{conflicting_key} " \
|
|
111
|
+
"#{stored[conflicting_key].inspect} does not match query #{query_metadata[conflicting_key].inspect}"
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def fingerprint(metadata)
|
|
115
|
+
relevant = stringify_keys(metadata).values_at("adapter", "provider", "model", "dimensions")
|
|
116
|
+
Digest::SHA256.hexdigest(relevant.map { |value| value.nil? ? "" : value.to_s }.join("\0"))
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def stringify_keys(value)
|
|
120
|
+
case value
|
|
121
|
+
when Hash
|
|
122
|
+
value.each_with_object({}) do |(key, nested), out|
|
|
123
|
+
out[key.to_s] = stringify_keys(nested)
|
|
124
|
+
end
|
|
125
|
+
else
|
|
126
|
+
value
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def unknown_embedding_metadata_keyword?(error)
|
|
131
|
+
error.message.include?("unknown keyword: :embedding_metadata") ||
|
|
132
|
+
error.message.include?("unknown keyword: \"embedding_metadata\"")
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Engram
|
|
4
|
+
# An immutable extractor result that carries a candidate Record and its source provenance.
|
|
5
|
+
# Converting it creates a new record and leaves the caller-owned record and metadata untouched.
|
|
6
|
+
class Extraction
|
|
7
|
+
attr_reader :record, :provenance
|
|
8
|
+
|
|
9
|
+
def initialize(record:, provenance:)
|
|
10
|
+
is_record = Object.instance_method(:is_a?).bind_call(record, Engram::Record)
|
|
11
|
+
raise ArgumentError, "record must be an Engram::Record" unless is_record
|
|
12
|
+
|
|
13
|
+
is_provenance = Object.instance_method(:is_a?).bind_call(provenance, Engram::Provenance)
|
|
14
|
+
raise ArgumentError, "provenance must be an Engram::Provenance" unless is_provenance
|
|
15
|
+
|
|
16
|
+
@record = record
|
|
17
|
+
@provenance = provenance
|
|
18
|
+
freeze
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def to_record
|
|
22
|
+
source = Engram::Internal::CandidateIntegrity.new.detach(record)
|
|
23
|
+
attributes = Engram::Record::STATE_READERS.to_h do |attribute|
|
|
24
|
+
[attribute, Engram::Record.instance_method(attribute).bind_call(source)]
|
|
25
|
+
end
|
|
26
|
+
attributes[:metadata] = Engram::Provenance.attach(attributes.fetch(:metadata), provenance)
|
|
27
|
+
Engram::Record.new(**attributes)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -58,14 +58,15 @@ module Engram
|
|
|
58
58
|
next if content.empty?
|
|
59
59
|
next if (fact["confidence"] || 1.0).to_f < @min_confidence
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
embedding = @embedder.embed(content)
|
|
62
|
+
Engram::EmbeddingMetadata.attach(Engram::Record.new(
|
|
62
63
|
content: content,
|
|
63
64
|
scope: scope,
|
|
64
65
|
kind: fact["kind"] || "fact",
|
|
65
66
|
importance: (fact["importance"] || 1.0).to_f,
|
|
66
67
|
metadata: {confidence: (fact["confidence"] || 1.0).to_f},
|
|
67
|
-
embedding:
|
|
68
|
-
)
|
|
68
|
+
embedding: embedding
|
|
69
|
+
), embedder: @embedder)
|
|
69
70
|
end
|
|
70
71
|
end
|
|
71
72
|
|