lex-apollo 0.2.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.
Files changed (38) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +25 -0
  3. data/README.md +135 -0
  4. data/lib/legion/extensions/apollo/actors/corroboration_checker.rb +22 -0
  5. data/lib/legion/extensions/apollo/actors/decay.rb +22 -0
  6. data/lib/legion/extensions/apollo/actors/expertise_aggregator.rb +22 -0
  7. data/lib/legion/extensions/apollo/actors/ingest.rb +25 -0
  8. data/lib/legion/extensions/apollo/actors/query_responder.rb +25 -0
  9. data/lib/legion/extensions/apollo/client.rb +30 -0
  10. data/lib/legion/extensions/apollo/helpers/confidence.rb +46 -0
  11. data/lib/legion/extensions/apollo/helpers/embedding.rb +22 -0
  12. data/lib/legion/extensions/apollo/helpers/graph_query.rb +77 -0
  13. data/lib/legion/extensions/apollo/helpers/similarity.rb +36 -0
  14. data/lib/legion/extensions/apollo/runners/expertise.rb +71 -0
  15. data/lib/legion/extensions/apollo/runners/knowledge.rb +213 -0
  16. data/lib/legion/extensions/apollo/runners/maintenance.rb +72 -0
  17. data/lib/legion/extensions/apollo/transport/exchanges/apollo.rb +19 -0
  18. data/lib/legion/extensions/apollo/transport/messages/ingest.rb +43 -0
  19. data/lib/legion/extensions/apollo/transport/messages/query.rb +43 -0
  20. data/lib/legion/extensions/apollo/transport/queues/ingest.rb +23 -0
  21. data/lib/legion/extensions/apollo/transport/queues/query.rb +23 -0
  22. data/lib/legion/extensions/apollo/version.rb +9 -0
  23. data/lib/legion/extensions/apollo.rb +25 -0
  24. data/spec/legion/extensions/apollo/actors/decay_spec.rb +45 -0
  25. data/spec/legion/extensions/apollo/actors/expertise_aggregator_spec.rb +41 -0
  26. data/spec/legion/extensions/apollo/actors/ingest_spec.rb +33 -0
  27. data/spec/legion/extensions/apollo/client_spec.rb +75 -0
  28. data/spec/legion/extensions/apollo/helpers/confidence_spec.rb +109 -0
  29. data/spec/legion/extensions/apollo/helpers/embedding_spec.rb +68 -0
  30. data/spec/legion/extensions/apollo/helpers/graph_query_spec.rb +69 -0
  31. data/spec/legion/extensions/apollo/helpers/similarity_spec.rb +58 -0
  32. data/spec/legion/extensions/apollo/runners/expertise_spec.rb +111 -0
  33. data/spec/legion/extensions/apollo/runners/knowledge_spec.rb +308 -0
  34. data/spec/legion/extensions/apollo/runners/maintenance_spec.rb +133 -0
  35. data/spec/legion/extensions/apollo/transport/messages/ingest_spec.rb +65 -0
  36. data/spec/legion/extensions/apollo/transport/messages/query_spec.rb +75 -0
  37. data/spec/spec_helper.rb +30 -0
  38. metadata +108 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: fcb22d66eb9b08e01ececa39900c455be2aa64f358a2976878c0b2934b71670a
4
+ data.tar.gz: 1d2d41cf8835c04e14827e22caaed2a848d0c8fc4c41cef3aeecc927041849b3
5
+ SHA512:
6
+ metadata.gz: 6bff39d97c42ca8085b7937066e38d52c50fe302cd1d04e80d8a4aca6762eb5c28b33eb05a0ea16adf391b86bffa3d0936f3d83caf3d936d5703e8c08bd0140c
7
+ data.tar.gz: 694442a97667f0355bba359938c7bd9317f9d2308a6ef169a54c59bc63e097c7786625a525b33defe44581bc20593cba259ff68be019c6c55708b6ad7f31e43a
data/CHANGELOG.md ADDED
@@ -0,0 +1,25 @@
1
+ # Changelog
2
+
3
+ ## [0.2.0] - 2026-03-16
4
+
5
+ ### Added
6
+ - `Helpers::Embedding` — embedding generation wrapper with legion-llm + zero-vector fallback
7
+ - `Knowledge.handle_ingest` — server-side ingest: embedding, corroboration check, entry creation, expertise upsert
8
+ - `Knowledge.handle_query` — server-side query: semantic search via pgvector, retrieval boost, access logging
9
+ - `Knowledge.retrieve_relevant` — GAIA tick phase handler for knowledge_retrieval (phase 4)
10
+ - `Maintenance.check_corroboration` — periodic scan promoting candidates to confirmed via similarity threshold
11
+ - `Expertise.aggregate` — periodic proficiency recalculation using log2-weighted average confidence
12
+
13
+ ## [0.1.0] - 2026-03-15
14
+
15
+ ### Added
16
+ - Initial scaffold with helpers, runners, actors, transport, and standalone client
17
+ - Confidence helper with decay, boost, and write gate logic
18
+ - Similarity helper with cosine distance and corroboration classification
19
+ - Graph query builder for recursive CTE traversal and pgvector semantic search
20
+ - Knowledge, Expertise, and Maintenance runners (client-side RMQ payloads)
21
+ - Ingest, QueryResponder, Decay, ExpertiseAggregator, CorroborationChecker actors
22
+ - Transport layer: apollo exchange, ingest/query queues, ingest/query messages
23
+ - Standalone Client with agent_id injection
24
+ - GAIA tick integration: knowledge_retrieval phase (phase 4)
25
+ - legion-data migration 012 with PostgreSQL+pgvector tables (guarded)
data/README.md ADDED
@@ -0,0 +1,135 @@
1
+ # lex-apollo
2
+
3
+ Shared durable knowledge store for the GAIA cognitive mesh. Agents publish confirmed knowledge via RabbitMQ; a dedicated Apollo service persists to PostgreSQL+pgvector. Supports semantic search, concept graph traversal, and expertise tracking.
4
+
5
+ ## Overview
6
+
7
+ `lex-apollo` operates in two modes:
8
+
9
+ - **Client mode**: Any agent loads this gem and calls runners. Runners publish to RabbitMQ — no direct database access required.
10
+ - **Service mode**: A dedicated Apollo process runs the actors, subscribes to queues, generates embeddings, and writes to PostgreSQL+pgvector.
11
+
12
+ The backing store is Azure Database for PostgreSQL Flexible Server with the pgvector extension.
13
+
14
+ ## Installation
15
+
16
+ Add to your Gemfile:
17
+
18
+ ```ruby
19
+ gem 'lex-apollo'
20
+ ```
21
+
22
+ ## Usage
23
+
24
+ ### Standalone Client
25
+
26
+ ```ruby
27
+ require 'legion/extensions/apollo'
28
+
29
+ client = Legion::Extensions::Apollo::Client.new(agent_id: 'my-agent-001')
30
+
31
+ # Store a confirmed knowledge entry
32
+ client.store_knowledge(
33
+ domain: 'networking',
34
+ content: 'BGP route reflectors reduce full-mesh IBGP complexity',
35
+ confidence: 0.9,
36
+ source_agent_id: 'my-agent-001',
37
+ tags: ['bgp', 'routing', 'ibgp']
38
+ )
39
+
40
+ # Query for relevant knowledge
41
+ client.query_knowledge(
42
+ query: 'BGP route reflector configuration',
43
+ domain: 'networking',
44
+ min_confidence: 0.6,
45
+ limit: 10
46
+ )
47
+
48
+ # Get related entries (concept graph traversal)
49
+ client.related_entries(entry_id: 'entry-uuid', max_hops: 2)
50
+
51
+ # Deprecate a stale entry
52
+ client.deprecate_entry(entry_id: 'entry-uuid', reason: 'superseded by RFC 7938')
53
+ ```
54
+
55
+ ### Expertise Queries
56
+
57
+ ```ruby
58
+ # Get proficiency scores for a domain
59
+ client.get_expertise(domain: 'networking', agent_id: 'my-agent-001')
60
+
61
+ # Find domains where knowledge coverage is thin
62
+ client.domains_at_risk(min_entries: 5, min_confidence: 0.7)
63
+
64
+ # Full agent knowledge profile
65
+ client.agent_profile(agent_id: 'my-agent-001')
66
+ ```
67
+
68
+ ### Maintenance
69
+
70
+ ```ruby
71
+ # Force confidence decay cycle
72
+ client.force_decay(domain: 'networking')
73
+
74
+ # Archive entries below confidence threshold
75
+ client.archive_stale(max_confidence: 0.2)
76
+
77
+ # Resolve a corroboration dispute
78
+ client.resolve_dispute(entry_id: 'entry-uuid', resolution: :accept)
79
+ ```
80
+
81
+ ## Architecture
82
+
83
+ ### Client Mode
84
+
85
+ Runners build structured payloads and publish to the `apollo` exchange via RabbitMQ. No PostgreSQL or pgvector dependency is needed in the calling agent. Transport requires `Legion::Transport` to be loaded (the `if defined?(Legion::Transport)` guard in the entry point handles this automatically).
86
+
87
+ ### Service Mode
88
+
89
+ Five actors run in the dedicated Apollo service process:
90
+
91
+ | Actor | Type | Interval | Purpose |
92
+ |---|---|---|---|
93
+ | `Ingest` | Subscription | on-message | Receive knowledge, generate embeddings, persist to PostgreSQL |
94
+ | `QueryResponder` | Subscription | on-message | Handle semantic queries, return results via RPC |
95
+ | `Decay` | Interval | 3600s | Confidence decay cycle across all entries |
96
+ | `ExpertiseAggregator` | Interval | 1800s | Recalculate domain proficiency scores |
97
+ | `CorroborationChecker` | Interval | 900s | Scan pending entries for auto-confirm threshold |
98
+
99
+ ### GAIA Tick Integration
100
+
101
+ Apollo is wired into the GAIA tick cycle at the `knowledge_retrieval` phase (phase 4), which fires after `memory_retrieval` and before `working_memory_integration`. It activates only when local memory lacks high-confidence matches for the current tick context.
102
+
103
+ ## Confidence Model
104
+
105
+ Entries have a confidence score between 0.0 and 1.0:
106
+
107
+ - New entries start at the caller-supplied confidence value
108
+ - Corroboration from multiple agents boosts confidence
109
+ - Entries below `WRITE_GATE_THRESHOLD` are rejected on ingest
110
+ - Confidence decays hourly; entries below `ARCHIVE_THRESHOLD` are archived
111
+
112
+ See `helpers/confidence.rb` for decay constants and boost logic.
113
+
114
+ ## Requirements
115
+
116
+ ### Client mode
117
+ - Ruby >= 3.4
118
+ - RabbitMQ (via `legion-transport`)
119
+
120
+ ### Service mode
121
+ - PostgreSQL with pgvector extension
122
+ - RabbitMQ
123
+ - `legion-data` for database connection management
124
+
125
+ ## Development
126
+
127
+ ```bash
128
+ bundle install
129
+ bundle exec rspec
130
+ bundle exec rubocop
131
+ ```
132
+
133
+ ## License
134
+
135
+ MIT
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/actors/every'
4
+ require_relative '../runners/maintenance'
5
+
6
+ module Legion
7
+ module Extensions
8
+ module Apollo
9
+ module Actor
10
+ class CorroborationChecker < Legion::Extensions::Actors::Every
11
+ def runner_class = Legion::Extensions::Apollo::Runners::Maintenance
12
+ def runner_function = 'check_corroboration'
13
+ def time = 900
14
+ def run_now? = false
15
+ def use_runner? = false
16
+ def check_subtask? = false
17
+ def generate_task? = false
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/actors/every'
4
+ require_relative '../runners/maintenance'
5
+
6
+ module Legion
7
+ module Extensions
8
+ module Apollo
9
+ module Actor
10
+ class Decay < Legion::Extensions::Actors::Every
11
+ def runner_class = Legion::Extensions::Apollo::Runners::Maintenance
12
+ def runner_function = 'force_decay'
13
+ def time = 3600
14
+ def run_now? = false
15
+ def use_runner? = false
16
+ def check_subtask? = false
17
+ def generate_task? = false
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/actors/every'
4
+ require_relative '../runners/expertise'
5
+
6
+ module Legion
7
+ module Extensions
8
+ module Apollo
9
+ module Actor
10
+ class ExpertiseAggregator < Legion::Extensions::Actors::Every
11
+ def runner_class = Legion::Extensions::Apollo::Runners::Expertise
12
+ def runner_function = 'aggregate'
13
+ def time = 1800
14
+ def run_now? = false
15
+ def use_runner? = false
16
+ def check_subtask? = false
17
+ def generate_task? = false
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/actors/subscription' if defined?(Legion::Extensions::Actors::Subscription)
4
+
5
+ module Legion
6
+ module Extensions
7
+ module Apollo
8
+ module Actor
9
+ class Ingest < Legion::Extensions::Actors::Subscription
10
+ def runner_class = 'Legion::Extensions::Apollo::Runners::Knowledge'
11
+ def runner_function = 'handle_ingest'
12
+ def check_subtask? = false
13
+ def generate_task? = false
14
+
15
+ def enabled?
16
+ defined?(Legion::Extensions::Apollo::Runners::Knowledge) &&
17
+ defined?(Legion::Transport)
18
+ rescue StandardError
19
+ false
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/actors/subscription' if defined?(Legion::Extensions::Actors::Subscription)
4
+
5
+ module Legion
6
+ module Extensions
7
+ module Apollo
8
+ module Actor
9
+ class QueryResponder < Legion::Extensions::Actors::Subscription
10
+ def runner_class = 'Legion::Extensions::Apollo::Runners::Knowledge'
11
+ def runner_function = 'handle_query'
12
+ def check_subtask? = false
13
+ def generate_task? = false
14
+
15
+ def enabled?
16
+ defined?(Legion::Extensions::Apollo::Runners::Knowledge) &&
17
+ defined?(Legion::Transport)
18
+ rescue StandardError
19
+ false
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'helpers/confidence'
4
+ require_relative 'helpers/similarity'
5
+ require_relative 'helpers/graph_query'
6
+ require_relative 'runners/knowledge'
7
+ require_relative 'runners/expertise'
8
+ require_relative 'runners/maintenance'
9
+
10
+ module Legion
11
+ module Extensions
12
+ module Apollo
13
+ class Client
14
+ include Runners::Knowledge
15
+ include Runners::Expertise
16
+ include Runners::Maintenance
17
+
18
+ attr_reader :agent_id
19
+
20
+ def initialize(agent_id: 'unknown', **)
21
+ @agent_id = agent_id
22
+ end
23
+
24
+ def store_knowledge(source_agent: nil, **)
25
+ super(**, source_agent: source_agent || @agent_id)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Apollo
6
+ module Helpers
7
+ module Confidence
8
+ INITIAL_CONFIDENCE = 0.5
9
+ CORROBORATION_BOOST = 0.3
10
+ RETRIEVAL_BOOST = 0.02
11
+ HOURLY_DECAY_FACTOR = 0.998
12
+ DECAY_THRESHOLD = 0.1
13
+ CORROBORATION_SIMILARITY_THRESHOLD = 0.9
14
+ WRITE_CONFIDENCE_GATE = 0.6
15
+ WRITE_NOVELTY_GATE = 0.3
16
+ STALE_DAYS = 90
17
+ CONTENT_TYPES = %i[fact concept procedure association observation].freeze
18
+ STATUSES = %w[candidate confirmed disputed decayed archived].freeze
19
+ RELATION_TYPES = %w[is_a has_a part_of causes similar_to contradicts supersedes depends_on].freeze
20
+
21
+ module_function
22
+
23
+ def apply_decay(confidence:, factor: HOURLY_DECAY_FACTOR, **)
24
+ [confidence * factor, 0.0].max
25
+ end
26
+
27
+ def apply_retrieval_boost(confidence:, **)
28
+ [confidence + RETRIEVAL_BOOST, 1.0].min
29
+ end
30
+
31
+ def apply_corroboration_boost(confidence:, **)
32
+ [confidence + CORROBORATION_BOOST, 1.0].min
33
+ end
34
+
35
+ def decayed?(confidence:, **)
36
+ confidence < DECAY_THRESHOLD
37
+ end
38
+
39
+ def meets_write_gate?(confidence:, novelty:, **)
40
+ confidence > WRITE_CONFIDENCE_GATE && novelty > WRITE_NOVELTY_GATE
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Apollo
6
+ module Helpers
7
+ module Embedding
8
+ DIMENSION = 1536
9
+
10
+ module_function
11
+
12
+ def generate(text:, **)
13
+ return Array.new(DIMENSION, 0.0) unless defined?(Legion::LLM) && Legion::LLM.started?
14
+
15
+ result = Legion::LLM.embed(text: text)
16
+ result.is_a?(Array) && result.size == DIMENSION ? result : Array.new(DIMENSION, 0.0)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Apollo
6
+ module Helpers
7
+ module GraphQuery
8
+ SPREAD_FACTOR = 0.6
9
+ DEFAULT_DEPTH = 2
10
+ MIN_ACTIVATION = 0.1
11
+
12
+ module_function
13
+
14
+ def build_traversal_sql(depth: DEFAULT_DEPTH, relation_types: nil, min_activation: MIN_ACTIVATION, **)
15
+ type_filter = if relation_types&.any?
16
+ types = relation_types.map { |t| "'#{t}'" }.join(', ')
17
+ "AND r.relation_type IN (#{types})"
18
+ else
19
+ ''
20
+ end
21
+
22
+ <<~SQL
23
+ WITH RECURSIVE graph AS (
24
+ SELECT e.id, e.content, e.content_type, e.confidence, e.tags, e.source_agent,
25
+ 0 AS depth, 1.0::float AS activation
26
+ FROM apollo_entries e
27
+ WHERE e.id = $entry_id
28
+
29
+ UNION ALL
30
+
31
+ SELECT e.id, e.content, e.content_type, e.confidence, e.tags, e.source_agent,
32
+ g.depth + 1,
33
+ (g.activation * #{SPREAD_FACTOR} * r.weight)::float
34
+ FROM graph g
35
+ JOIN apollo_relations r ON r.from_entry_id = g.id #{type_filter}
36
+ JOIN apollo_entries e ON e.id = r.to_entry_id
37
+ WHERE g.depth < #{depth}
38
+ AND g.activation * #{SPREAD_FACTOR} * r.weight > #{min_activation}
39
+ )
40
+ SELECT DISTINCT ON (id) id, content, content_type, confidence, tags, source_agent,
41
+ depth, activation
42
+ FROM graph
43
+ ORDER BY id, activation DESC
44
+ SQL
45
+ end
46
+
47
+ def build_semantic_search_sql(limit: 10, min_confidence: 0.3, statuses: nil, tags: nil, **)
48
+ conditions = ["e.confidence >= #{min_confidence}"]
49
+
50
+ if statuses&.any?
51
+ status_list = statuses.map { |s| "'#{s}'" }.join(', ')
52
+ conditions << "e.status IN (#{status_list})"
53
+ end
54
+
55
+ if tags&.any?
56
+ tag_list = tags.map { |t| "'#{t}'" }.join(', ')
57
+ conditions << "e.tags && ARRAY[#{tag_list}]::text[]"
58
+ end
59
+
60
+ where_clause = conditions.join(' AND ')
61
+
62
+ <<~SQL
63
+ SELECT e.id, e.content, e.content_type, e.confidence, e.tags, e.source_agent,
64
+ e.access_count, e.created_at,
65
+ (e.embedding <=> $embedding) AS distance
66
+ FROM apollo_entries e
67
+ WHERE #{where_clause}
68
+ AND e.embedding IS NOT NULL
69
+ ORDER BY e.embedding <=> $embedding
70
+ LIMIT #{limit}
71
+ SQL
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'confidence'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module Apollo
8
+ module Helpers
9
+ module Similarity
10
+ module_function
11
+
12
+ def cosine_similarity(vec_a:, vec_b:, **)
13
+ dot = vec_a.zip(vec_b).sum { |x, y| x * y }
14
+ mag_a = Math.sqrt(vec_a.sum { |x| x**2 })
15
+ mag_b = Math.sqrt(vec_b.sum { |x| x**2 })
16
+ return 0.0 if mag_a.zero? || mag_b.zero?
17
+
18
+ dot / (mag_a * mag_b)
19
+ end
20
+
21
+ def above_corroboration_threshold?(similarity:, **)
22
+ similarity >= Confidence::CORROBORATION_SIMILARITY_THRESHOLD
23
+ end
24
+
25
+ def classify_match(similarity:, same_content_type: true, contradicts: false, **)
26
+ if above_corroboration_threshold?(similarity: similarity) && same_content_type
27
+ contradicts ? :contradiction : :corroboration
28
+ else
29
+ :novel
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Apollo
6
+ module Runners
7
+ module Expertise
8
+ def get_expertise(domain:, min_proficiency: 0.0, **)
9
+ { action: :expertise_query, domain: domain, min_proficiency: min_proficiency }
10
+ end
11
+
12
+ def domains_at_risk(min_agents: 2, **)
13
+ { action: :domains_at_risk, min_agents: min_agents }
14
+ end
15
+
16
+ def agent_profile(agent_id:, **)
17
+ { action: :agent_profile, agent_id: agent_id }
18
+ end
19
+
20
+ def aggregate(**)
21
+ return { success: false, error: 'apollo_data_not_available' } unless defined?(Legion::Data::Model::ApolloEntry)
22
+
23
+ entries = Legion::Data::Model::ApolloEntry
24
+ .select(:source_agent, :tags, :confidence)
25
+ .exclude(source_agent: nil)
26
+ .all
27
+
28
+ groups = {}
29
+ entries.each do |entry|
30
+ agent = entry.source_agent
31
+ domain = entry.tags.is_a?(Array) ? (entry.tags.first || 'general') : 'general'
32
+ key = "#{agent}:#{domain}"
33
+ groups[key] ||= { agent_id: agent, domain: domain, confidences: [] }
34
+ groups[key][:confidences] << entry.confidence.to_f
35
+ end
36
+
37
+ agent_set = Set.new
38
+ domain_set = Set.new
39
+
40
+ groups.each_value do |group|
41
+ avg = group[:confidences].sum / group[:confidences].size
42
+ count = group[:confidences].size
43
+ proficiency = [avg * Math.log2(count + 1), 1.0].min
44
+
45
+ existing = Legion::Data::Model::ApolloExpertise
46
+ .where(agent_id: group[:agent_id], domain: group[:domain]).first
47
+
48
+ if existing
49
+ existing.update(proficiency: proficiency, entry_count: count, last_active_at: Time.now)
50
+ else
51
+ Legion::Data::Model::ApolloExpertise.create(
52
+ agent_id: group[:agent_id], domain: group[:domain],
53
+ proficiency: proficiency, entry_count: count, last_active_at: Time.now
54
+ )
55
+ end
56
+
57
+ agent_set << group[:agent_id]
58
+ domain_set << group[:domain]
59
+ end
60
+
61
+ { success: true, agents: agent_set.size, domains: domain_set.size }
62
+ rescue Sequel::Error => e
63
+ { success: false, error: e.message }
64
+ end
65
+
66
+ include Legion::Extensions::Helpers::Lex if defined?(Legion::Extensions::Helpers::Lex)
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end