lex-apollo 0.3.3 → 0.3.5

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: e5f9f60cb9a67bc5d4cd35af051b1abe3251787d0da55ac092c1898942bd2144
4
- data.tar.gz: 8b0cd86443469ae7a0d5d857a127f855cd0e68f8d967c94a727e8f166372e483
3
+ metadata.gz: 6156122baf8989f96a09685918e8ed6f6608730c8e19e442a632f5faa8853ee7
4
+ data.tar.gz: 478d0003003a76abc6ffdc0c39a7be3b06d8e75f91e1c88a6edfa5e46a05634a
5
5
  SHA512:
6
- metadata.gz: 3f239c19c6212f7c142aec1e0fe34aed651928f5a4c37acfcf54152d6719594f94b6ddb037bf48666eb93f9c747d341a477039e320e2e74fef17a8188519e0a9
7
- data.tar.gz: 6227ef5fffa37ad570940591d390ac594d71f10a49de1e75a6ce568b075ac6c0293ba13478130eebbc366c6195b383327dab2b7400398bc2fe6f1f274f4b61ca
6
+ metadata.gz: 898147c2dbe69430a0dc4606db21a82100de37a1865b9d56157499530c7e7a205e8cc44cd39f23410293a8f3874c9dadacf9c5aa47c89bd5b61a7a39b9067951
7
+ data.tar.gz: 8ed9fe61c718b12abfd8b5b2c6a75e249e47491c6bf8068df984f6f18c8943ee5d3d56b3b2def551703b4cd7b0463942e20a4803a740ca7da65d25a87b90c344
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.4] - 2026-03-20
4
+
5
+ ### Added
6
+ - `Helpers::EntityWatchdog`: regex-based entity detection for persons, services, repos, and configurable concepts
7
+ - GAIA `post_tick_reflection` handler for passive entity detection (enabled via `apollo.entity_watchdog.enabled`)
8
+ - Deduplication by type+value, configurable type filtering, and `link_or_create` for Apollo integration
9
+
3
10
  ## [0.3.3] - 2026-03-20
4
11
 
5
12
  ### Added
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Apollo
6
+ module Helpers
7
+ module EntityWatchdog
8
+ ENTITY_PATTERNS = {
9
+ person: /\b[A-Z][a-z]+(?:\s[A-Z][a-z]+)+\b/,
10
+ service: %r{\bhttps?://[^\s]+\b},
11
+ repo: %r{\b[a-zA-Z0-9_-]+/[a-zA-Z0-9_.-]+\b}
12
+ }.freeze
13
+
14
+ class << self
15
+ def detect_entities(text:, types: nil)
16
+ return [] if text.nil? || text.empty?
17
+
18
+ types = (types || default_types).map(&:to_sym)
19
+ entities = []
20
+
21
+ types.each do |type_sym|
22
+ pattern = type_sym == :concept ? concept_pattern : ENTITY_PATTERNS[type_sym]
23
+ next unless pattern
24
+
25
+ text.scan(pattern).each do |match|
26
+ entities << { type: type_sym, value: match.strip, confidence: 0.5 }
27
+ end
28
+ end
29
+
30
+ entities.uniq { |e| [e[:type], e[:value].downcase] }
31
+ end
32
+
33
+ def link_or_create(entities:, source_context: nil)
34
+ return { success: true, linked: 0, created: 0 } if entities.nil? || entities.empty?
35
+
36
+ linked = 0
37
+ created = 0
38
+
39
+ entities.each do |entity|
40
+ existing = find_existing(entity)
41
+ if existing
42
+ bump_confidence(existing, source_context)
43
+ linked += 1
44
+ else
45
+ create_candidate(entity, source_context)
46
+ created += 1
47
+ end
48
+ end
49
+
50
+ { success: true, linked: linked, created: created }
51
+ end
52
+
53
+ def concept_pattern
54
+ keywords = if defined?(Legion::Settings)
55
+ Legion::Settings.dig(:apollo, :entity_watchdog, :concept_keywords) || []
56
+ else
57
+ []
58
+ end
59
+ return nil if keywords.empty?
60
+
61
+ Regexp.new("\\b(?:#{keywords.map { |k| Regexp.escape(k) }.join('|')})\\b", Regexp::IGNORECASE)
62
+ end
63
+
64
+ private
65
+
66
+ def default_types
67
+ if defined?(Legion::Settings)
68
+ Legion::Settings.dig(:apollo, :entity_watchdog, :types) || %w[person service repo concept]
69
+ else
70
+ %w[person service repo concept]
71
+ end
72
+ end
73
+
74
+ def find_existing(_entity)
75
+ return nil unless defined?(Runners::Knowledge) && respond_to?(:retrieve_relevant, true)
76
+
77
+ nil
78
+ end
79
+
80
+ def bump_confidence(_entry, _source_context)
81
+ # Increment retrieval confidence on existing Apollo entry
82
+ end
83
+
84
+ def create_candidate(entity, _source_context)
85
+ return unless defined?(Runners::Knowledge)
86
+
87
+ Legion::Logging.debug "[entity_watchdog] candidate: #{entity[:type]}=#{entity[:value]}" if defined?(Legion::Logging)
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -130,6 +130,39 @@ module Legion
130
130
  { success: false, error: e.message }
131
131
  end
132
132
 
133
+ def redistribute_knowledge(agent_id:, min_confidence: 0.5, **)
134
+ return { success: false, error: 'apollo_data_not_available' } unless defined?(Legion::Data::Model::ApolloEntry)
135
+
136
+ entries = Legion::Data::Model::ApolloEntry
137
+ .where(source_agent: agent_id, status: 'confirmed')
138
+ .where { confidence > min_confidence }
139
+ .all
140
+
141
+ return { success: true, redistributed: 0 } if entries.empty?
142
+
143
+ store = (Legion::Extensions::Agentic::Memory::Trace.shared_store if defined?(Legion::Extensions::Agentic::Memory::Trace))
144
+
145
+ redistributed = 0
146
+ entries.each do |entry|
147
+ if store
148
+ trace = Legion::Extensions::Agentic::Memory::Trace::Helpers::Trace.new_trace(
149
+ type: :semantic,
150
+ content_payload: { content: entry.content, source_agent: agent_id,
151
+ content_type: entry.content_type, tags: Array(entry.tags) },
152
+ strength: entry.confidence.to_f,
153
+ domain_tag: Array(entry.tags).first || 'general'
154
+ )
155
+ store.store(trace)
156
+ end
157
+ redistributed += 1
158
+ end
159
+
160
+ Legion::Logging.info "[apollo] redistributed #{redistributed} entries from departing agent=#{agent_id}"
161
+ { success: true, redistributed: redistributed, agent_id: agent_id }
162
+ rescue Sequel::Error => e
163
+ { success: false, error: e.message }
164
+ end
165
+
133
166
  def retrieve_relevant(query: nil, limit: 5, min_confidence: 0.3, tags: nil, skip: false, **)
134
167
  return { status: :skipped } if skip
135
168
 
@@ -3,7 +3,7 @@
3
3
  module Legion
4
4
  module Extensions
5
5
  module Apollo
6
- VERSION = '0.3.3'
6
+ VERSION = '0.3.5'
7
7
  end
8
8
  end
9
9
  end
@@ -24,3 +24,20 @@ module Legion
24
24
  end
25
25
  end
26
26
  end
27
+
28
+ # Entity watchdog on post_tick_reflection
29
+ if defined?(Legion::Gaia::PhaseWiring) && begin
30
+ Legion::Settings.dig(:apollo, :entity_watchdog, :enabled)
31
+ rescue StandardError
32
+ false
33
+ end
34
+ require 'legion/extensions/apollo/helpers/entity_watchdog'
35
+ Legion::Gaia::PhaseWiring.register_handler(:post_tick_reflection) do |tick_results|
36
+ text = tick_results.is_a?(Hash) ? (tick_results[:content] || tick_results[:output] || '').to_s : tick_results.to_s
37
+ entities = Legion::Extensions::Apollo::Helpers::EntityWatchdog.detect_entities(text: text)
38
+ if entities.any?
39
+ Legion::Extensions::Apollo::Helpers::EntityWatchdog.link_or_create(entities: entities,
40
+ source_context: tick_results[:tick_id])
41
+ end
42
+ end
43
+ end
@@ -45,4 +45,29 @@ RSpec.describe Legion::Extensions::Apollo::GaiaIntegration do
45
45
  expect(result).to eq({ success: true })
46
46
  end
47
47
  end
48
+
49
+ describe 'entity watchdog phase handler' do
50
+ it 'detects entities from tick results' do
51
+ require 'legion/extensions/apollo/helpers/entity_watchdog'
52
+ tick_results = { content: 'Jane Doe deployed to https://api.example.com', tick_id: 'tick-1' }
53
+ entities = Legion::Extensions::Apollo::Helpers::EntityWatchdog.detect_entities(text: tick_results[:content])
54
+ expect(entities.size).to be >= 2
55
+ end
56
+
57
+ it 'links or creates entities from tick results' do
58
+ require 'legion/extensions/apollo/helpers/entity_watchdog'
59
+ tick_results = { content: 'Jane Doe at LegionIO/lex-mesh', tick_id: 'tick-2' }
60
+ entities = Legion::Extensions::Apollo::Helpers::EntityWatchdog.detect_entities(text: tick_results[:content])
61
+ result = Legion::Extensions::Apollo::Helpers::EntityWatchdog.link_or_create(
62
+ entities: entities, source_context: tick_results[:tick_id]
63
+ )
64
+ expect(result[:success]).to be true
65
+ end
66
+
67
+ it 'entry point has entity watchdog registration block' do
68
+ entry_point = File.read(File.expand_path('../../../../lib/legion/extensions/apollo.rb', __dir__))
69
+ expect(entry_point).to include('entity_watchdog')
70
+ expect(entry_point).to include('PhaseWiring.register_handler(:post_tick_reflection)')
71
+ end
72
+ end
48
73
  end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'legion/extensions/apollo/helpers/entity_watchdog'
5
+
6
+ RSpec.describe Legion::Extensions::Apollo::Helpers::EntityWatchdog do
7
+ describe '.detect_entities' do
8
+ it 'detects person names (capitalized multi-word)' do
9
+ entities = described_class.detect_entities(text: 'Talked to Jane Doe about the project')
10
+ person = entities.find { |e| e[:type] == :person }
11
+ expect(person).not_to be_nil
12
+ expect(person[:value]).to eq('Jane Doe')
13
+ end
14
+
15
+ it 'detects service URLs' do
16
+ entities = described_class.detect_entities(text: 'Deployed to https://api.example.com/v1')
17
+ service = entities.find { |e| e[:type] == :service }
18
+ expect(service).not_to be_nil
19
+ expect(service[:value]).to include('example.com')
20
+ end
21
+
22
+ it 'detects repo references' do
23
+ entities = described_class.detect_entities(text: 'Check LegionIO/lex-mesh for the code')
24
+ repo = entities.find { |e| e[:type] == :repo }
25
+ expect(repo).not_to be_nil
26
+ expect(repo[:value]).to eq('LegionIO/lex-mesh')
27
+ end
28
+
29
+ it 'detects concept keywords from settings' do
30
+ allow(described_class).to receive(:concept_pattern).and_return(/\b(?:kubernetes|terraform)\b/i)
31
+ entities = described_class.detect_entities(text: 'Using Terraform to deploy Kubernetes')
32
+ concepts = entities.select { |e| e[:type] == :concept }
33
+ expect(concepts.size).to eq(2)
34
+ end
35
+
36
+ it 'deduplicates entities by type and lowercase value' do
37
+ entities = described_class.detect_entities(text: 'Jane Doe met Jane Doe again')
38
+ persons = entities.select { |e| e[:type] == :person }
39
+ expect(persons.size).to eq(1)
40
+ end
41
+
42
+ it 'returns empty array for text with no entities' do
43
+ entities = described_class.detect_entities(text: 'nothing special here')
44
+ expect(entities).to be_empty
45
+ end
46
+
47
+ it 'filters by specified types' do
48
+ entities = described_class.detect_entities(
49
+ text: 'Jane Doe at https://example.com with LegionIO/lex-mesh',
50
+ types: [:person]
51
+ )
52
+ expect(entities.all? { |e| e[:type] == :person }).to be true
53
+ end
54
+ end
55
+
56
+ describe '.link_or_create' do
57
+ it 'returns counts for empty entities' do
58
+ result = described_class.link_or_create(entities: [])
59
+ expect(result[:success]).to be true
60
+ expect(result[:linked]).to eq(0)
61
+ expect(result[:created]).to eq(0)
62
+ end
63
+ end
64
+ end
@@ -307,4 +307,88 @@ RSpec.describe Legion::Extensions::Apollo::Runners::Knowledge do
307
307
  end
308
308
  end
309
309
  end
310
+
311
+ describe '#redistribute_knowledge' do
312
+ let(:host) { Object.new.extend(described_class) }
313
+
314
+ context 'when Apollo data is not available' do
315
+ before { hide_const('Legion::Data::Model::ApolloEntry') if defined?(Legion::Data::Model::ApolloEntry) }
316
+
317
+ it 'returns a structured error' do
318
+ result = host.redistribute_knowledge(agent_id: 'agent-x')
319
+ expect(result[:success]).to be false
320
+ expect(result[:error]).to eq('apollo_data_not_available')
321
+ end
322
+ end
323
+
324
+ context 'when the departing agent has no confirmed entries' do
325
+ let(:mock_entry_class) { double('ApolloEntry') }
326
+
327
+ before do
328
+ stub_const('Legion::Data::Model::ApolloEntry', mock_entry_class)
329
+ chain = double('chain')
330
+ allow(mock_entry_class).to receive(:where).and_return(chain)
331
+ allow(chain).to receive(:where).and_return(double(all: []))
332
+ end
333
+
334
+ it 'returns success with zero redistributed' do
335
+ result = host.redistribute_knowledge(agent_id: 'departed-1')
336
+ expect(result[:success]).to be true
337
+ expect(result[:redistributed]).to eq(0)
338
+ end
339
+ end
340
+
341
+ context 'when the departing agent has confirmed entries' do
342
+ let(:mock_entry_class) { double('ApolloEntry') }
343
+ let(:mock_entry) do
344
+ double('entry', content: 'Ruby is fast', content_type: 'fact',
345
+ confidence: 0.8, tags: ['ruby'])
346
+ end
347
+
348
+ before do
349
+ stub_const('Legion::Data::Model::ApolloEntry', mock_entry_class)
350
+ chain = double('chain')
351
+ allow(mock_entry_class).to receive(:where).and_return(chain)
352
+ allow(chain).to receive(:where).and_return(double(all: [mock_entry]))
353
+ end
354
+
355
+ it 'returns the count of redistributed entries' do
356
+ result = host.redistribute_knowledge(agent_id: 'departed-2')
357
+ expect(result[:success]).to be true
358
+ expect(result[:redistributed]).to eq(1)
359
+ expect(result[:agent_id]).to eq('departed-2')
360
+ end
361
+
362
+ it 'stores into trace shared_store when Memory::Trace is available' do
363
+ mock_store = double('store')
364
+ mock_trace_helpers = Module.new do
365
+ def self.new_trace(type:, content_payload: nil, **kwargs) # rubocop:disable Lint/UnusedMethodArgument
366
+ { trace_id: 'trace-abc', trace_type: type, strength: kwargs[:strength] || 0.5 }
367
+ end
368
+ end
369
+ stub_const('Legion::Extensions::Agentic::Memory::Trace', Module.new)
370
+ stub_const('Legion::Extensions::Agentic::Memory::Trace::Helpers::Trace', mock_trace_helpers)
371
+ allow(Legion::Extensions::Agentic::Memory::Trace).to receive(:shared_store).and_return(mock_store)
372
+ allow(mock_store).to receive(:store)
373
+
374
+ result = host.redistribute_knowledge(agent_id: 'departed-3')
375
+ expect(result[:redistributed]).to eq(1)
376
+ expect(mock_store).to have_received(:store).once
377
+ end
378
+ end
379
+
380
+ context 'when Sequel raises an error' do
381
+ before do
382
+ stub_const('Legion::Data::Model::ApolloEntry', Class.new)
383
+ allow(Legion::Data::Model::ApolloEntry).to receive(:where)
384
+ .and_raise(Sequel::Error, 'db error')
385
+ end
386
+
387
+ it 'returns a structured error' do
388
+ result = host.redistribute_knowledge(agent_id: 'agent-x')
389
+ expect(result[:success]).to be false
390
+ expect(result[:error]).to eq('db error')
391
+ end
392
+ end
393
+ end
310
394
  end
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.3.3
4
+ version: 0.3.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -58,6 +58,7 @@ files:
58
58
  - lib/legion/extensions/apollo/gaia_integration.rb
59
59
  - lib/legion/extensions/apollo/helpers/confidence.rb
60
60
  - lib/legion/extensions/apollo/helpers/embedding.rb
61
+ - lib/legion/extensions/apollo/helpers/entity_watchdog.rb
61
62
  - lib/legion/extensions/apollo/helpers/graph_query.rb
62
63
  - lib/legion/extensions/apollo/helpers/similarity.rb
63
64
  - lib/legion/extensions/apollo/runners/entity_extractor.rb
@@ -80,6 +81,7 @@ files:
80
81
  - spec/legion/extensions/apollo/gaia_integration_spec.rb
81
82
  - spec/legion/extensions/apollo/helpers/confidence_spec.rb
82
83
  - spec/legion/extensions/apollo/helpers/embedding_spec.rb
84
+ - spec/legion/extensions/apollo/helpers/entity_watchdog_spec.rb
83
85
  - spec/legion/extensions/apollo/helpers/graph_query_spec.rb
84
86
  - spec/legion/extensions/apollo/helpers/similarity_spec.rb
85
87
  - spec/legion/extensions/apollo/runners/decay_cycle_spec.rb