lex-apollo 0.3.3 → 0.3.4
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 +7 -0
- data/lib/legion/extensions/apollo/helpers/entity_watchdog.rb +94 -0
- data/lib/legion/extensions/apollo/version.rb +1 -1
- data/lib/legion/extensions/apollo.rb +17 -0
- data/spec/legion/extensions/apollo/gaia_integration_spec.rb +25 -0
- data/spec/legion/extensions/apollo/helpers/entity_watchdog_spec.rb +64 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e7d785a9fed9656eb22b307620a384f64e52646bca14728dc0de3ef2b1adb5bd
|
|
4
|
+
data.tar.gz: c64bcf1b75ee0ed43a747722a610c5cee8de823995d5f7455e31ae5c9174c56d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0ff89d33e4993dd8c71e392202c66add619ab04045fa674b5f8f0c4d891b727ca7401865d349c68bdc3a0344e1773db4f86651ed7e96ecbcaf0098a9d05cdbc9
|
|
7
|
+
data.tar.gz: 29b69744b9870bee8e1fbfc030652aede63a51c3a12cd02a3bc69bfc805b6267cf8045346cb37dc2565e1e103ae6f3b559972536b02f3a62dd4b6f12223361a5
|
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
|
|
@@ -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
|
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.
|
|
4
|
+
version: 0.3.4
|
|
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
|