lex-apollo 0.4.17 → 0.4.19

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: be66d0b628a4cb32bbc6c0b1c5bb31ee780ee22891713e860caddd324b61a507
4
- data.tar.gz: a6355f876338f4a13ada80c6c9757e0b278d5060e3ef1fc1c4a3ca01005d120e
3
+ metadata.gz: 1bf0ca8790d13fc3d262ede810ce50a47146dc37743e8806696de970b8a0385a
4
+ data.tar.gz: 1f1a9115e1a1bb36423150a7290a82352f07aef7aa41f0b4b7da8e0ff5b76f94
5
5
  SHA512:
6
- metadata.gz: a8f20b26534a6d71c82bdbc83a1e6cd0f9f6a058d938a488ea23185063d9f0f608e13df1751ac6c9f21bba744ff55607f658d152fe9aa77ead6d874cbc357920
7
- data.tar.gz: dfa3e978328c63519a850aedaa66a9a45941755e70dfd9e903498469dac0d43fc436ed76c49721fa76cfd0a22831d96376f985f0365442d6aeb0ee21df73ff0b
6
+ metadata.gz: 3d92269898b53825fff831ef253fc44ac9e9a45602da25628f32fc95c1e6a53edfd86add2f02f7131c449cd1b940258d2c8fc4df3b90ca6306affbf8a76d4ccb
7
+ data.tar.gz: d6c98fc7a7f351dc929e2d2dac4c9d7d119e32328706c9880e19a6bc3edb53448b008d8a8fefd7f7e8b8a201084f4ec78006088954cb7e56ee72cb312f0d5ce6
data/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.19] - 2026-04-24
4
+
5
+ ### Fixed
6
+ - `store_knowledge` no longer rejects LLM-provided content_type values — normalizes free-form strings (`"reasoning"`, `"text"`, `"text/plain"`, `":fact"`, `"inference"`) to valid symbols via alias map with `:observation` fallback
7
+ - `GaiaIntegration.publish_insight` now passes `:observation` instead of the domain string as content_type (was sending `"general"` or domain names which failed validation)
8
+ - `llm_detects_conflict?` truncates content to 4000 chars before sending to LLM to prevent context overflow errors (was passing full entry content, hitting 65536-token limit)
9
+
10
+ ## [0.4.18] - 2026-04-24
11
+
12
+ ### Fixed
13
+ - `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
14
+ - `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)
15
+
16
+ ### Changed
17
+ - Reduce `POWER_LAW_ALPHA` from 0.5 to 0.05 — decay was compounding hourly and crushing entry confidence within a day
18
+ - Reduce `DECAY_THRESHOLD` from 0.1 to 0.05 — entries were being archived too eagerly
19
+ - Decay cycle now operates on age in **days** instead of hours, preventing aggressive per-cycle compounding
20
+ - Add `DECAY_MIN_AGE_HOURS` (168h / 7 days) — entries younger than this are completely exempt from decay and archival
21
+ - `apply_decay` Ruby helper matches the new SQL behavior (days-based, minimum age guard)
22
+
3
23
  ## [0.4.17] - 2026-04-03
4
24
 
5
25
  ### Changed
@@ -15,7 +15,7 @@ module Legion
15
15
  client = Legion::Extensions::Apollo::Client.new(agent_id: agent_id)
16
16
  client.store_knowledge(
17
17
  content: insight[:content],
18
- content_type: insight[:domain] || 'general',
18
+ content_type: :observation,
19
19
  source_agent: agent_id,
20
20
  tags: Array(insight[:tags])
21
21
  )
@@ -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.5
12
- DECAY_THRESHOLD = 0.1
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
- [confidence * ((age_hours.clamp(0, Float::INFINITY) + 2.0)**(-alpha)) / ((age_hours.clamp(0, Float::INFINITY) + 1.0)**(-alpha)), 0.0].max
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
@@ -14,10 +14,20 @@ module Legion
14
14
  'general' => :all
15
15
  }.freeze
16
16
 
17
+ CONTENT_TYPE_ALIASES = {
18
+ reasoning: :concept, analysis: :concept, explanation: :concept,
19
+ text: :observation, general: :observation, note: :observation, summary: :observation,
20
+ rule: :procedure, step: :procedure, instruction: :procedure,
21
+ link: :association, relation: :association, connection: :association,
22
+ inference: :association, implication: :association
23
+ }.freeze
24
+
17
25
  def store_knowledge(content:, content_type:, tags: [], source_agent: nil, context: {}, **)
18
- content_type = content_type.to_sym
19
- unless Helpers::Confidence::CONTENT_TYPES.include?(content_type)
20
- raise ArgumentError, "invalid content_type: #{content_type}. Must be one of #{Helpers::Confidence::CONTENT_TYPES}"
26
+ content_type = normalize_content_type(content_type)
27
+
28
+ if defined?(Legion::Data::Model::ApolloEntry)
29
+ return handle_ingest(content: content, content_type: content_type,
30
+ tags: Array(tags), source_agent: source_agent, context: context, **)
21
31
  end
22
32
 
23
33
  {
@@ -30,7 +40,12 @@ module Legion
30
40
  }
31
41
  end
32
42
 
33
- def query_knowledge(query:, limit: Helpers::GraphQuery.default_query_limit, min_confidence: Helpers::GraphQuery.default_query_min_confidence, status: [:confirmed], tags: nil, **) # rubocop:disable Layout/LineLength
43
+ 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
44
+ if defined?(Legion::Data::Model::ApolloEntry)
45
+ return handle_query(query: query, limit: limit, min_confidence: min_confidence,
46
+ status: status, tags: tags, **)
47
+ end
48
+
34
49
  {
35
50
  action: :query,
36
51
  query: query,
@@ -42,6 +57,8 @@ module Legion
42
57
  end
43
58
 
44
59
  def related_entries(entry_id:, relation_types: nil, depth: Helpers::GraphQuery.default_depth, **)
60
+ return handle_traverse(entry_id: entry_id, depth: depth, relation_types: relation_types, **) if defined?(Legion::Data::Model::ApolloEntry)
61
+
45
62
  {
46
63
  action: :traverse,
47
64
  entry_id: entry_id,
@@ -235,7 +252,7 @@ module Legion
235
252
  embedding = embed_text(query)
236
253
  sql = Helpers::GraphQuery.build_semantic_search_sql(
237
254
  limit: limit, min_confidence: min_confidence,
238
- statuses: ['confirmed'], tags: tags, domain: domain
255
+ statuses: %w[confirmed candidate], tags: tags, domain: domain
239
256
  )
240
257
 
241
258
  db = Legion::Data::Model::ApolloEntry.db
@@ -312,8 +329,16 @@ module Legion
312
329
  { deleted: 0, redacted: 0, error: e.message }
313
330
  end
314
331
 
332
+ CONFLICT_CHECK_MAX_CHARS = 4000
333
+
315
334
  private
316
335
 
336
+ def normalize_content_type(raw)
337
+ sym = raw.to_s.delete_prefix(':').gsub(%r{[/\s]}, '_').strip.downcase.to_sym
338
+ sym = CONTENT_TYPE_ALIASES.fetch(sym, sym)
339
+ Helpers::Confidence::CONTENT_TYPES.include?(sym) ? sym : :observation
340
+ end
341
+
317
342
  def embed_text(text)
318
343
  text = normalize_text_input(text)
319
344
  result = Legion::LLM::Embeddings.generate(text: text)
@@ -387,10 +412,12 @@ module Legion
387
412
  def llm_detects_conflict?(content_a, content_b)
388
413
  return false unless defined?(Legion::LLM) && Legion::LLM.respond_to?(:structured)
389
414
 
415
+ a = content_a.to_s[0, CONFLICT_CHECK_MAX_CHARS]
416
+ b = content_b.to_s[0, CONFLICT_CHECK_MAX_CHARS]
390
417
  result = Legion::LLM.structured(
391
418
  messages: [
392
419
  { role: 'system', content: 'Do these two statements contradict each other? Return JSON.' },
393
- { role: 'user', content: "A: #{content_a}\n\nB: #{content_b}" }
420
+ { role: 'user', content: "A: #{a}\n\nB: #{b}" }
394
421
  ],
395
422
  schema: { type: 'object', properties: { contradicts: { type: 'boolean' } } },
396
423
  caller: { extension: 'lex-apollo', runner: 'knowledge' }
@@ -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
- hours_expr = Sequel.lit(
32
- 'GREATEST(EXTRACT(EPOCH FROM (NOW() - COALESCE(updated_at, created_at))) / 3600.0, 1.0)'
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), ?)', hours_expr, hours_expr, alpha
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
@@ -3,7 +3,7 @@
3
3
  module Legion
4
4
  module Extensions
5
5
  module Apollo
6
- VERSION = '0.4.17'
6
+ VERSION = '0.4.19'
7
7
  end
8
8
  end
9
9
  end
@@ -9,6 +9,27 @@ RSpec.describe 'Apollo Contradiction Detection' do
9
9
  it 'returns false when LLM unavailable' do
10
10
  expect(knowledge.send(:llm_detects_conflict?, 'sky is blue', 'sky is red')).to be false
11
11
  end
12
+
13
+ context 'when LLM is available' do
14
+ let(:llm_mod) do
15
+ Module.new do
16
+ def self.respond_to?(*) = true
17
+ def self.structured(**) = { data: { contradicts: true } }
18
+ end
19
+ end
20
+
21
+ before { stub_const('Legion::LLM', llm_mod) }
22
+
23
+ it 'truncates content longer than CONFLICT_CHECK_MAX_CHARS' do
24
+ long_text = 'x' * 10_000
25
+ allow(llm_mod).to receive(:structured).and_return({ data: { contradicts: false } })
26
+ knowledge.send(:llm_detects_conflict?, long_text, long_text)
27
+ expect(llm_mod).to have_received(:structured) do |**kwargs|
28
+ user_msg = kwargs[:messages].find { |m| m[:role] == 'user' }[:content]
29
+ expect(user_msg.length).to be < 10_000
30
+ end
31
+ end
32
+ end
12
33
  end
13
34
 
14
35
  describe '#detect_contradictions' do
@@ -44,6 +44,20 @@ RSpec.describe Legion::Extensions::Apollo::GaiaIntegration do
44
44
  )
45
45
  expect(result).to eq({ success: true })
46
46
  end
47
+
48
+ it 'passes :observation as content_type regardless of domain' do
49
+ client_double = instance_double(Legion::Extensions::Apollo::Client)
50
+ allow(Legion::Extensions::Apollo::Client).to receive(:new).and_return(client_double)
51
+ allow(client_double).to receive(:store_knowledge).and_return({ success: true })
52
+
53
+ described_class.publish_insight(
54
+ { confidence: 0.9, novelty: 0.5, content: 'insight', domain: 'clinical' },
55
+ agent_id: 'test-agent'
56
+ )
57
+ expect(client_double).to have_received(:store_knowledge).with(
58
+ hash_including(content_type: :observation)
59
+ )
60
+ end
47
61
  end
48
62
 
49
63
  describe 'entity watchdog phase handler' do
@@ -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.5)
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.1)
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.5) # ~0.6667
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 'applies age-based power-law decay when age_hours is provided' do
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.05)).to be true
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.1)).to be false
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.5)
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.1)
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
@@ -45,10 +45,41 @@ RSpec.describe Legion::Extensions::Apollo::Runners::Knowledge do
45
45
  expect(result[:source_agent]).to eq('worker-1')
46
46
  end
47
47
 
48
- it 'rejects invalid content_type' do
49
- expect do
50
- runner.store_knowledge(content: 'test', content_type: :invalid)
51
- end.to raise_error(ArgumentError, /content_type/)
48
+ it 'falls back to :observation for unrecognized content_type' do
49
+ result = runner.store_knowledge(content: 'test', content_type: 'invalid_type')
50
+ expect(result[:content_type]).to eq(:observation)
51
+ end
52
+
53
+ it 'normalizes LLM-provided content_type "reasoning" to :concept' do
54
+ result = runner.store_knowledge(content: 'test', content_type: 'reasoning')
55
+ expect(result[:content_type]).to eq(:concept)
56
+ end
57
+
58
+ it 'normalizes "text" to :observation' do
59
+ result = runner.store_knowledge(content: 'test', content_type: 'text')
60
+ expect(result[:content_type]).to eq(:observation)
61
+ end
62
+
63
+ it 'normalizes "text/plain" to :observation' do
64
+ result = runner.store_knowledge(content: 'test', content_type: 'text/plain')
65
+ expect(result[:content_type]).to eq(:observation)
66
+ end
67
+
68
+ it 'strips leading colon from ":fact"' do
69
+ result = runner.store_knowledge(content: 'test', content_type: ':fact')
70
+ expect(result[:content_type]).to eq(:fact)
71
+ end
72
+
73
+ it 'normalizes "inference" to :association' do
74
+ result = runner.store_knowledge(content: 'test', content_type: 'inference')
75
+ expect(result[:content_type]).to eq(:association)
76
+ end
77
+
78
+ it 'accepts all valid CONTENT_TYPES unchanged' do
79
+ %i[fact concept procedure association observation].each do |ct|
80
+ result = runner.store_knowledge(content: 'test', content_type: ct)
81
+ expect(result[:content_type]).to eq(ct)
82
+ end
52
83
  end
53
84
  end
54
85
 
@@ -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.5)
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.17
4
+ version: 0.4.19
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity