lex-apollo 0.4.16 → 0.4.18
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 +18 -0
- data/lib/legion/extensions/apollo/helpers/confidence.rb +8 -3
- data/lib/legion/extensions/apollo/runners/knowledge.rb +27 -3
- data/lib/legion/extensions/apollo/runners/maintenance.rb +13 -4
- data/lib/legion/extensions/apollo/version.rb +1 -1
- data/spec/legion/extensions/apollo/helpers/confidence_spec.rb +15 -6
- data/spec/legion/extensions/apollo/runners/decay_cycle_spec.rb +6 -2
- data/spec/legion/extensions/apollo/runners/maintenance_spec.rb +1 -1
- metadata +9 -9
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 87724ea9011349f5f7c008a360728975567903827b5cdcf9a42e2678ca84b134
|
|
4
|
+
data.tar.gz: ae10f48baf9522bd7087a20e1886fc26170c1f5e0a0c36a3ccac26c836f11885
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b21c5413da8dfe9f60d9845a578bcfe8337982d128a5041df214004e144b9913dd34cd2da1514a30ea67e21d52604a11218c362a46f9c990c472ac4ec14a0fa9
|
|
7
|
+
data.tar.gz: ae07a4aba39ce05f20b6a495310b6a20c1c468e0d627393ab931af76d9cd57da5b52075a33706a8b94fadae01c32b65d818d4f5c1018f9eac08a8ffdc10c7ace
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.4.18] - 2026-04-24
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- `store_knowledge`, `query_knowledge`, `related_entries` now execute directly when PostgreSQL is available instead of returning unexecuted command hashes — MCP tool calls were returning dispatch payloads (`{action: :query, ...}`) as their result, making the knowledge base unsearchable via LLM tools
|
|
7
|
+
- `query_knowledge` and `retrieve_relevant` now include `candidate` status in default search filters so newly stored entries are immediately retrievable (previously only `confirmed` entries were returned)
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
- Reduce `POWER_LAW_ALPHA` from 0.5 to 0.05 — decay was compounding hourly and crushing entry confidence within a day
|
|
11
|
+
- Reduce `DECAY_THRESHOLD` from 0.1 to 0.05 — entries were being archived too eagerly
|
|
12
|
+
- Decay cycle now operates on age in **days** instead of hours, preventing aggressive per-cycle compounding
|
|
13
|
+
- Add `DECAY_MIN_AGE_HOURS` (168h / 7 days) — entries younger than this are completely exempt from decay and archival
|
|
14
|
+
- `apply_decay` Ruby helper matches the new SQL behavior (days-based, minimum age guard)
|
|
15
|
+
|
|
16
|
+
## [0.4.17] - 2026-04-03
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- resolve rubocop Style/IfUnlessModifier offense in runners/knowledge.rb
|
|
20
|
+
|
|
3
21
|
## [0.4.16] - 2026-03-30
|
|
4
22
|
|
|
5
23
|
### Changed
|
|
@@ -8,12 +8,13 @@ module Legion
|
|
|
8
8
|
INITIAL_CONFIDENCE = 0.5
|
|
9
9
|
CORROBORATION_BOOST = 0.3
|
|
10
10
|
RETRIEVAL_BOOST = 0.02
|
|
11
|
-
POWER_LAW_ALPHA = 0.
|
|
12
|
-
DECAY_THRESHOLD = 0.
|
|
11
|
+
POWER_LAW_ALPHA = 0.05
|
|
12
|
+
DECAY_THRESHOLD = 0.05
|
|
13
13
|
CORROBORATION_SIMILARITY_THRESHOLD = 0.9
|
|
14
14
|
WRITE_CONFIDENCE_GATE = 0.6
|
|
15
15
|
WRITE_NOVELTY_GATE = 0.3
|
|
16
16
|
STALE_DAYS = 90
|
|
17
|
+
DECAY_MIN_AGE_HOURS = 168
|
|
17
18
|
CONTENT_TYPES = %i[fact concept procedure association observation].freeze
|
|
18
19
|
STATUSES = %w[candidate confirmed disputed decayed archived].freeze
|
|
19
20
|
RELATION_TYPES = %w[is_a has_a part_of causes similar_to contradicts supersedes depends_on].freeze
|
|
@@ -41,14 +42,18 @@ module Legion
|
|
|
41
42
|
def write_confidence_gate = apollo_setting(:confidence, :write_gate, default: WRITE_CONFIDENCE_GATE)
|
|
42
43
|
def write_novelty_gate = apollo_setting(:confidence, :novelty_gate, default: WRITE_NOVELTY_GATE)
|
|
43
44
|
def stale_days = apollo_setting(:stale_days, default: STALE_DAYS)
|
|
45
|
+
def decay_min_age_hours = apollo_setting(:decay_min_age_hours, default: DECAY_MIN_AGE_HOURS)
|
|
44
46
|
|
|
45
47
|
def corroboration_similarity_threshold
|
|
46
48
|
apollo_setting(:confidence, :corroboration_similarity, default: CORROBORATION_SIMILARITY_THRESHOLD)
|
|
47
49
|
end
|
|
48
50
|
|
|
49
51
|
def apply_decay(confidence:, age_hours: nil, alpha: power_law_alpha, **)
|
|
52
|
+
return confidence if age_hours && age_hours < decay_min_age_hours
|
|
53
|
+
|
|
50
54
|
if age_hours
|
|
51
|
-
|
|
55
|
+
age_days = age_hours / 24.0
|
|
56
|
+
[confidence * ((age_days.clamp(1, Float::INFINITY) + 1.0)**(-alpha)) / (age_days.clamp(1, Float::INFINITY)**(-alpha)), 0.0].max
|
|
52
57
|
else
|
|
53
58
|
factor = 1.0 / (1.0 + alpha)
|
|
54
59
|
[confidence * factor, 0.0].max
|
|
@@ -20,6 +20,11 @@ module Legion
|
|
|
20
20
|
raise ArgumentError, "invalid content_type: #{content_type}. Must be one of #{Helpers::Confidence::CONTENT_TYPES}"
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
+
if defined?(Legion::Data::Model::ApolloEntry)
|
|
24
|
+
return handle_ingest(content: content, content_type: content_type,
|
|
25
|
+
tags: Array(tags), source_agent: source_agent, context: context, **)
|
|
26
|
+
end
|
|
27
|
+
|
|
23
28
|
{
|
|
24
29
|
action: :store,
|
|
25
30
|
content: content,
|
|
@@ -30,7 +35,12 @@ module Legion
|
|
|
30
35
|
}
|
|
31
36
|
end
|
|
32
37
|
|
|
33
|
-
def query_knowledge(query:, limit: Helpers::GraphQuery.default_query_limit, min_confidence: Helpers::GraphQuery.default_query_min_confidence, status: [
|
|
38
|
+
def query_knowledge(query:, limit: Helpers::GraphQuery.default_query_limit, min_confidence: Helpers::GraphQuery.default_query_min_confidence, status: %i[confirmed candidate], tags: nil, **) # rubocop:disable Layout/LineLength
|
|
39
|
+
if defined?(Legion::Data::Model::ApolloEntry)
|
|
40
|
+
return handle_query(query: query, limit: limit, min_confidence: min_confidence,
|
|
41
|
+
status: status, tags: tags, **)
|
|
42
|
+
end
|
|
43
|
+
|
|
34
44
|
{
|
|
35
45
|
action: :query,
|
|
36
46
|
query: query,
|
|
@@ -42,6 +52,8 @@ module Legion
|
|
|
42
52
|
end
|
|
43
53
|
|
|
44
54
|
def related_entries(entry_id:, relation_types: nil, depth: Helpers::GraphQuery.default_depth, **)
|
|
55
|
+
return handle_traverse(entry_id: entry_id, depth: depth, relation_types: relation_types, **) if defined?(Legion::Data::Model::ApolloEntry)
|
|
56
|
+
|
|
45
57
|
{
|
|
46
58
|
action: :traverse,
|
|
47
59
|
entry_id: entry_id,
|
|
@@ -121,6 +133,7 @@ module Legion
|
|
|
121
133
|
def handle_query(query:, limit: Helpers::GraphQuery.default_query_limit, min_confidence: Helpers::GraphQuery.default_query_min_confidence, status: [:confirmed], tags: nil, domain: nil, agent_id: 'unknown', **) # rubocop:disable Layout/LineLength
|
|
122
134
|
return { success: false, error: 'apollo_data_not_available' } unless defined?(Legion::Data::Model::ApolloEntry)
|
|
123
135
|
|
|
136
|
+
query = normalize_text_input(query)
|
|
124
137
|
embedding = embed_text(query)
|
|
125
138
|
sql = Helpers::GraphQuery.build_semantic_search_sql(
|
|
126
139
|
limit: limit, min_confidence: min_confidence,
|
|
@@ -228,12 +241,13 @@ module Legion
|
|
|
228
241
|
|
|
229
242
|
return { success: false, error: 'apollo_data_not_available' } unless defined?(Legion::Data::Model::ApolloEntry)
|
|
230
243
|
|
|
244
|
+
query = normalize_text_input(query)
|
|
231
245
|
return { success: true, entries: [], count: 0 } if query.nil? || query.to_s.strip.empty?
|
|
232
246
|
|
|
233
|
-
embedding = embed_text(query
|
|
247
|
+
embedding = embed_text(query)
|
|
234
248
|
sql = Helpers::GraphQuery.build_semantic_search_sql(
|
|
235
249
|
limit: limit, min_confidence: min_confidence,
|
|
236
|
-
statuses: [
|
|
250
|
+
statuses: %w[confirmed candidate], tags: tags, domain: domain
|
|
237
251
|
)
|
|
238
252
|
|
|
239
253
|
db = Legion::Data::Model::ApolloEntry.db
|
|
@@ -313,6 +327,7 @@ module Legion
|
|
|
313
327
|
private
|
|
314
328
|
|
|
315
329
|
def embed_text(text)
|
|
330
|
+
text = normalize_text_input(text)
|
|
316
331
|
result = Legion::LLM::Embeddings.generate(text: text)
|
|
317
332
|
vector = result.is_a?(Hash) ? result[:vector] : result
|
|
318
333
|
vector.is_a?(Array) && vector.any? ? vector : Array.new(1024, 0.0)
|
|
@@ -321,6 +336,15 @@ module Legion
|
|
|
321
336
|
Array.new(1024, 0.0)
|
|
322
337
|
end
|
|
323
338
|
|
|
339
|
+
def normalize_text_input(value)
|
|
340
|
+
return Legion::Apollo.send(:normalize_text_input, value) if defined?(Legion::Apollo) && Legion::Apollo.respond_to?(:normalize_text_input, true)
|
|
341
|
+
|
|
342
|
+
value.to_s
|
|
343
|
+
rescue StandardError => e
|
|
344
|
+
log.warn("Apollo Knowledge.normalize_text_input failed: #{e.message}")
|
|
345
|
+
value.to_s
|
|
346
|
+
end
|
|
347
|
+
|
|
324
348
|
def allowed_domains_for(target_domain)
|
|
325
349
|
rules = if defined?(Legion::Settings) && Legion::Settings.dig(:apollo, :domain_isolation)
|
|
326
350
|
Legion::Settings.dig(:apollo, :domain_isolation)
|
|
@@ -23,28 +23,37 @@ module Legion
|
|
|
23
23
|
def run_decay_cycle(alpha: nil, min_confidence: nil, **)
|
|
24
24
|
alpha ||= Helpers::Confidence.power_law_alpha
|
|
25
25
|
min_confidence ||= Helpers::Confidence.decay_threshold
|
|
26
|
+
min_age_hours = Helpers::Confidence.decay_min_age_hours
|
|
26
27
|
|
|
27
28
|
return { decayed: 0, archived: 0 } unless defined?(Legion::Data) && Legion::Data.respond_to?(:connection) && Legion::Data.connection
|
|
28
29
|
|
|
29
30
|
conn = Legion::Data.connection
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
'GREATEST(EXTRACT(EPOCH FROM (NOW() - COALESCE(updated_at, created_at))) /
|
|
32
|
+
age_days_expr = Sequel.lit(
|
|
33
|
+
'GREATEST(EXTRACT(EPOCH FROM (NOW() - COALESCE(updated_at, created_at))) / 86400.0, 1.0)'
|
|
33
34
|
)
|
|
34
35
|
decay_factor = Sequel.lit(
|
|
35
|
-
'POWER(CAST(? AS double precision) / (CAST(? AS double precision) + 1.0), ?)',
|
|
36
|
+
'POWER(CAST(? AS double precision) / (CAST(? AS double precision) + 1.0), ?)',
|
|
37
|
+
age_days_expr, age_days_expr, alpha
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
min_age_filter = Sequel.lit(
|
|
41
|
+
"COALESCE(updated_at, created_at) < NOW() - INTERVAL '? hours'", min_age_hours
|
|
36
42
|
)
|
|
37
43
|
|
|
38
44
|
decayed = conn[:apollo_entries]
|
|
39
45
|
.exclude(status: 'archived')
|
|
46
|
+
.where(min_age_filter)
|
|
40
47
|
.update(confidence: Sequel[:confidence] * decay_factor)
|
|
41
48
|
|
|
42
49
|
archived = conn[:apollo_entries]
|
|
43
50
|
.where { confidence < min_confidence }
|
|
51
|
+
.where(min_age_filter)
|
|
44
52
|
.exclude(status: 'archived')
|
|
45
53
|
.update(status: 'archived')
|
|
46
54
|
|
|
47
|
-
{ decayed: decayed, archived: archived, alpha: alpha, threshold: min_confidence
|
|
55
|
+
{ decayed: decayed, archived: archived, alpha: alpha, threshold: min_confidence,
|
|
56
|
+
min_age_hours: min_age_hours }
|
|
48
57
|
rescue Sequel::Error => e
|
|
49
58
|
{ decayed: 0, archived: 0, error: e.message }
|
|
50
59
|
end
|
|
@@ -18,11 +18,15 @@ RSpec.describe Legion::Extensions::Apollo::Helpers::Confidence do
|
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
it 'defines POWER_LAW_ALPHA' do
|
|
21
|
-
expect(described_class::POWER_LAW_ALPHA).to eq(0.
|
|
21
|
+
expect(described_class::POWER_LAW_ALPHA).to eq(0.05)
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
it 'defines DECAY_THRESHOLD' do
|
|
25
|
-
expect(described_class::DECAY_THRESHOLD).to eq(0.
|
|
25
|
+
expect(described_class::DECAY_THRESHOLD).to eq(0.05)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it 'defines DECAY_MIN_AGE_HOURS' do
|
|
29
|
+
expect(described_class::DECAY_MIN_AGE_HOURS).to eq(168)
|
|
26
30
|
end
|
|
27
31
|
|
|
28
32
|
it 'defines CORROBORATION_SIMILARITY_THRESHOLD' do
|
|
@@ -45,12 +49,17 @@ RSpec.describe Legion::Extensions::Apollo::Helpers::Confidence do
|
|
|
45
49
|
describe '.apply_decay' do
|
|
46
50
|
it 'applies power-law decay with default alpha when no age given' do
|
|
47
51
|
result = described_class.apply_decay(confidence: 1.0)
|
|
48
|
-
expected = 1.0 / (1.0 + 0.
|
|
52
|
+
expected = 1.0 / (1.0 + 0.05) # ~0.9524
|
|
49
53
|
expect(result).to be_within(0.0001).of(expected)
|
|
50
54
|
end
|
|
51
55
|
|
|
52
|
-
it '
|
|
56
|
+
it 'skips decay when age_hours is below minimum age' do
|
|
53
57
|
result = described_class.apply_decay(confidence: 1.0, age_hours: 10)
|
|
58
|
+
expect(result).to eq(1.0)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it 'applies age-based power-law decay when age_hours exceeds minimum' do
|
|
62
|
+
result = described_class.apply_decay(confidence: 1.0, age_hours: 500)
|
|
54
63
|
expect(result).to be > 0.0
|
|
55
64
|
expect(result).to be < 1.0
|
|
56
65
|
end
|
|
@@ -98,11 +107,11 @@ RSpec.describe Legion::Extensions::Apollo::Helpers::Confidence do
|
|
|
98
107
|
|
|
99
108
|
describe '.decayed?' do
|
|
100
109
|
it 'returns true when confidence below threshold' do
|
|
101
|
-
expect(described_class.decayed?(confidence: 0.
|
|
110
|
+
expect(described_class.decayed?(confidence: 0.01)).to be true
|
|
102
111
|
end
|
|
103
112
|
|
|
104
113
|
it 'returns false when confidence at or above threshold' do
|
|
105
|
-
expect(described_class.decayed?(confidence: 0.
|
|
114
|
+
expect(described_class.decayed?(confidence: 0.05)).to be false
|
|
106
115
|
end
|
|
107
116
|
end
|
|
108
117
|
|
|
@@ -14,11 +14,15 @@ RSpec.describe 'Apollo Decay Cycle' do
|
|
|
14
14
|
|
|
15
15
|
describe 'configurable decay parameters' do
|
|
16
16
|
it 'returns POWER_LAW_ALPHA as default' do
|
|
17
|
-
expect(Legion::Extensions::Apollo::Helpers::Confidence.power_law_alpha).to eq(0.
|
|
17
|
+
expect(Legion::Extensions::Apollo::Helpers::Confidence.power_law_alpha).to eq(0.05)
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
it 'returns default decay threshold' do
|
|
21
|
-
expect(Legion::Extensions::Apollo::Helpers::Confidence.decay_threshold).to eq(0.
|
|
21
|
+
expect(Legion::Extensions::Apollo::Helpers::Confidence.decay_threshold).to eq(0.05)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it 'returns default decay minimum age hours' do
|
|
25
|
+
expect(Legion::Extensions::Apollo::Helpers::Confidence.decay_min_age_hours).to eq(168)
|
|
22
26
|
end
|
|
23
27
|
end
|
|
24
28
|
end
|
|
@@ -71,7 +71,7 @@ RSpec.describe Legion::Extensions::Apollo::Runners::Maintenance do
|
|
|
71
71
|
|
|
72
72
|
it 'returns alpha in result hash' do
|
|
73
73
|
result = host.run_decay_cycle
|
|
74
|
-
expect(result[:alpha]).to eq(0.
|
|
74
|
+
expect(result[:alpha]).to eq(0.05)
|
|
75
75
|
expect(result).not_to have_key(:rate)
|
|
76
76
|
end
|
|
77
77
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lex-apollo
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.4.
|
|
4
|
+
version: 0.4.18
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -125,30 +125,30 @@ dependencies:
|
|
|
125
125
|
name: rubocop
|
|
126
126
|
requirement: !ruby/object:Gem::Requirement
|
|
127
127
|
requirements:
|
|
128
|
-
- - "
|
|
128
|
+
- - ">="
|
|
129
129
|
- !ruby/object:Gem::Version
|
|
130
|
-
version: '
|
|
130
|
+
version: '0'
|
|
131
131
|
type: :development
|
|
132
132
|
prerelease: false
|
|
133
133
|
version_requirements: !ruby/object:Gem::Requirement
|
|
134
134
|
requirements:
|
|
135
|
-
- - "
|
|
135
|
+
- - ">="
|
|
136
136
|
- !ruby/object:Gem::Version
|
|
137
|
-
version: '
|
|
137
|
+
version: '0'
|
|
138
138
|
- !ruby/object:Gem::Dependency
|
|
139
139
|
name: rubocop-legion
|
|
140
140
|
requirement: !ruby/object:Gem::Requirement
|
|
141
141
|
requirements:
|
|
142
|
-
- - "
|
|
142
|
+
- - ">="
|
|
143
143
|
- !ruby/object:Gem::Version
|
|
144
|
-
version: '0
|
|
144
|
+
version: '0'
|
|
145
145
|
type: :development
|
|
146
146
|
prerelease: false
|
|
147
147
|
version_requirements: !ruby/object:Gem::Requirement
|
|
148
148
|
requirements:
|
|
149
|
-
- - "
|
|
149
|
+
- - ">="
|
|
150
150
|
- !ruby/object:Gem::Version
|
|
151
|
-
version: '0
|
|
151
|
+
version: '0'
|
|
152
152
|
description: Durable shared knowledge with vector search, concept graph, and expertise
|
|
153
153
|
tracking
|
|
154
154
|
email:
|