lex-apollo 0.4.22 → 0.4.24

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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +16 -0
  3. data/lib/legion/extensions/apollo/actors/corroboration_checker.rb +3 -1
  4. data/lib/legion/extensions/apollo/actors/decay.rb +3 -1
  5. data/lib/legion/extensions/apollo/actors/entity_watchdog.rb +10 -21
  6. data/lib/legion/extensions/apollo/actors/expertise_aggregator.rb +3 -1
  7. data/lib/legion/extensions/apollo/actors/gas_subscriber.rb +1 -1
  8. data/lib/legion/extensions/apollo/actors/ingest.rb +1 -1
  9. data/lib/legion/extensions/apollo/actors/query_responder.rb +1 -1
  10. data/lib/legion/extensions/apollo/actors/writeback_store.rb +1 -1
  11. data/lib/legion/extensions/apollo/actors/writeback_vectorize.rb +2 -2
  12. data/lib/legion/extensions/apollo/api.rb +28 -22
  13. data/lib/legion/extensions/apollo/gaia_integration.rb +16 -13
  14. data/lib/legion/extensions/apollo/helpers/capability.rb +19 -17
  15. data/lib/legion/extensions/apollo/helpers/confidence.rb +5 -8
  16. data/lib/legion/extensions/apollo/helpers/data_models.rb +61 -0
  17. data/lib/legion/extensions/apollo/helpers/entity_watchdog.rb +8 -15
  18. data/lib/legion/extensions/apollo/helpers/similarity.rb +5 -6
  19. data/lib/legion/extensions/apollo/helpers/writeback.rb +13 -14
  20. data/lib/legion/extensions/apollo/runners/expertise.rb +10 -8
  21. data/lib/legion/extensions/apollo/runners/gas.rb +47 -27
  22. data/lib/legion/extensions/apollo/runners/knowledge.rb +95 -80
  23. data/lib/legion/extensions/apollo/runners/maintenance.rb +7 -5
  24. data/lib/legion/extensions/apollo/runners/request.rb +7 -1
  25. data/lib/legion/extensions/apollo/version.rb +1 -1
  26. data/lib/legion/extensions/apollo.rb +96 -0
  27. data/spec/legion/extensions/apollo/actors/writeback_vectorize_spec.rb +3 -3
  28. data/spec/legion/extensions/apollo/api_spec.rb +20 -0
  29. data/spec/legion/extensions/apollo/helpers/capability_spec.rb +4 -4
  30. data/spec/legion/extensions/apollo/runners/gas_anticipate_spec.rb +0 -3
  31. data/spec/legion/extensions/apollo/runners/gas_relate_spec.rb +0 -4
  32. data/spec/legion/extensions/apollo/runners/gas_synthesize_spec.rb +0 -11
  33. data/spec/legion/extensions/apollo/runners/knowledge_spec.rb +25 -15
  34. data/spec/legion/extensions/apollo/runners/maintenance_spec.rb +8 -8
  35. data/spec/legion/extensions/apollo/runners/request_spec.rb +8 -8
  36. data/spec/spec_helper.rb +4 -0
  37. metadata +2 -1
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'json'
3
+ require 'legion/json'
4
+ require 'legion/settings'
4
5
  require_relative '../helpers/confidence'
6
+ require_relative '../helpers/data_models'
5
7
 
6
8
  module Legion
7
9
  module Extensions
@@ -26,9 +28,9 @@ module Legion
26
28
 
27
29
  def store_knowledge(content:, content_type:, tags: [], source_agent: nil, context: {}, **)
28
30
  content_type = normalize_content_type(content_type)
29
- log.debug("Apollo Knowledge.store_knowledge content_type=#{content_type} tags=#{Array(tags).size} source_agent=#{source_agent || 'nil'} data_available=#{defined?(Legion::Data::Model::ApolloEntry) ? true : false}") # rubocop:disable Layout/LineLength
31
+ log.debug("Apollo Knowledge.store_knowledge content_type=#{content_type} tags=#{Array(tags).size} source_agent=#{source_agent || 'nil'} data_available=#{Helpers::DataModels.apollo_entry_available?}") # rubocop:disable Layout/LineLength
30
32
 
31
- if defined?(Legion::Data::Model::ApolloEntry)
33
+ if Helpers::DataModels.apollo_entry_available?
32
34
  return handle_ingest(content: content, content_type: content_type,
33
35
  tags: Array(tags), source_agent: source_agent, context: context, **)
34
36
  end
@@ -44,8 +46,8 @@ module Legion
44
46
  end
45
47
 
46
48
  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
47
- log.debug("Apollo Knowledge.query_knowledge query_length=#{query.to_s.length} limit=#{limit} statuses=#{Array(status).join(',')} tags=#{Array(tags).size} data_available=#{defined?(Legion::Data::Model::ApolloEntry) ? true : false}") # rubocop:disable Layout/LineLength
48
- if defined?(Legion::Data::Model::ApolloEntry)
49
+ log.debug("Apollo Knowledge.query_knowledge query_length=#{query.to_s.length} limit=#{limit} statuses=#{Array(status).join(',')} tags=#{Array(tags).size} data_available=#{Helpers::DataModels.apollo_entry_available?}") # rubocop:disable Layout/LineLength
50
+ if Helpers::DataModels.apollo_entry_available?
49
51
  return handle_query(query: query, limit: limit, min_confidence: min_confidence,
50
52
  status: status, tags: tags, **)
51
53
  end
@@ -61,8 +63,8 @@ module Legion
61
63
  end
62
64
 
63
65
  def related_entries(entry_id:, relation_types: nil, depth: Helpers::GraphQuery.default_depth, **)
64
- log.debug("Apollo Knowledge.related_entries entry_id=#{entry_id} depth=#{depth} relation_types=#{Array(relation_types).join(',')} data_available=#{defined?(Legion::Data::Model::ApolloEntry) ? true : false}") # rubocop:disable Layout/LineLength
65
- return handle_traverse(entry_id: entry_id, depth: depth, relation_types: relation_types, **) if defined?(Legion::Data::Model::ApolloEntry)
66
+ log.debug("Apollo Knowledge.related_entries entry_id=#{entry_id} depth=#{depth} relation_types=#{Array(relation_types).join(',')} data_available=#{Helpers::DataModels.apollo_entry_available?}") # rubocop:disable Layout/LineLength
67
+ return handle_traverse(entry_id: entry_id, depth: depth, relation_types: relation_types, **) if Helpers::DataModels.apollo_entry_available?
66
68
 
67
69
  {
68
70
  action: :traverse,
@@ -84,6 +86,7 @@ module Legion
84
86
  return { status: :skipped } if skip
85
87
 
86
88
  content = normalize_text_input(content)
89
+ content_type = normalize_content_type(content_type.nil? ? :observation : content_type)
87
90
  log.debug("Apollo Knowledge.handle_ingest content_length=#{content.length} content_type=#{content_type} tags=#{Array(tags).size} source_agent=#{source_agent} source_channel=#{source_channel || 'nil'}") # rubocop:disable Layout/LineLength
88
91
  early_error = ingest_early_return_error(content: content, content_type: content_type, tags: tags)
89
92
  return early_error if early_error
@@ -116,7 +119,7 @@ module Legion
116
119
 
117
120
  upsert_expertise(source_agent: metadata[:source_agent], domain: metadata[:domain])
118
121
 
119
- Legion::Data::Model::ApolloAccessLog.create(
122
+ Helpers::DataModels.apollo_access_log.create(
120
123
  entry_id: existing_id, agent_id: metadata[:source_agent], action: 'ingest'
121
124
  )
122
125
 
@@ -130,9 +133,10 @@ module Legion
130
133
  { success: false, error: e.message }
131
134
  end
132
135
 
133
- def handle_query(query:, limit: Helpers::GraphQuery.default_query_limit, min_confidence: Helpers::GraphQuery.default_query_min_confidence, status: UNSET, tags: nil, domain: nil, agent_id: 'unknown', **) # rubocop:disable Layout/LineLength
134
- return { success: false, error: 'apollo_data_not_available' } unless defined?(Legion::Data::Model::ApolloEntry)
136
+ def handle_query(query:, limit: Helpers::GraphQuery.default_query_limit, min_confidence: Helpers::GraphQuery.default_query_min_confidence, status: UNSET, tags: nil, domain: nil, agent_id: 'unknown', **) # rubocop:disable Layout/LineLength, Metrics/CyclomaticComplexity
137
+ return { success: false, error: 'apollo_data_not_available' } unless Helpers::DataModels.apollo_entry_available?
135
138
 
139
+ entry_model = Helpers::DataModels.apollo_entry
136
140
  query = normalize_text_input(query)
137
141
  status_defaulted = status.equal?(UNSET)
138
142
  requested_status = status_defaulted ? DEFAULT_QUERY_STATUS : status
@@ -143,31 +147,27 @@ module Legion
143
147
  end
144
148
 
145
149
  embedding = embed_text(query)
150
+ if embedding.nil?
151
+ log.warn('Apollo Knowledge.handle_query embedding unavailable; falling back to browse query')
152
+ return list_entries_chronologically(query: query, limit: limit, status: requested_status,
153
+ status_defaulted: status_defaulted, tags: tags, domain: domain)
154
+ end
155
+
146
156
  sql = Helpers::GraphQuery.build_semantic_search_sql(
147
157
  limit: limit, min_confidence: min_confidence,
148
158
  statuses: Array(requested_status).map(&:to_s), tags: tags, domain: domain
149
159
  )
150
160
 
151
- db = Legion::Data::Model::ApolloEntry.db
161
+ db = entry_model.db
152
162
  entries = db.fetch(sql, embedding: Sequel.lit("'[#{embedding.join(',')}]'::vector")).all
153
163
 
154
164
  entries = entries.reject { |e| e[:distance].respond_to?(:nan?) && e[:distance].nan? }
155
165
 
156
166
  entries.each do |entry|
157
- Legion::Data::Model::ApolloEntry.where(id: entry[:id]).update(
158
- access_count: Sequel.expr(:access_count) + 1,
159
- confidence: Helpers::Confidence.apply_retrieval_boost(
160
- confidence: entry[:confidence]
161
- ),
162
- updated_at: Time.now
163
- )
167
+ boost_entry_after_query(entry_model, entry)
164
168
  end
165
169
 
166
- if entries.any?
167
- Legion::Data::Model::ApolloAccessLog.create(
168
- entry_id: entries.first&.dig(:id), agent_id: agent_id, action: 'query'
169
- )
170
- end
170
+ record_query_access(entry_id: entries.first&.dig(:id), agent_id: agent_id) if entries.any?
171
171
 
172
172
  formatted = entries.map do |entry|
173
173
  { id: entry[:id], content: entry[:content], content_type: entry[:content_type],
@@ -184,7 +184,7 @@ module Legion
184
184
  end
185
185
 
186
186
  def handle_traverse(entry_id:, depth: Helpers::GraphQuery.default_depth, relation_types: nil, agent_id: 'unknown', **)
187
- return { success: false, error: 'apollo_data_not_available' } unless defined?(Legion::Data::Model::ApolloEntry)
187
+ return { success: false, error: 'apollo_data_not_available' } unless Helpers::DataModels.apollo_entry_available?
188
188
 
189
189
  log.debug("Apollo Knowledge.handle_traverse entry_id=#{entry_id} depth=#{depth} relation_types=#{Array(relation_types).join(',')} agent_id=#{agent_id}") # rubocop:disable Layout/LineLength
190
190
  # Whitelist relation_types to prevent SQL injection (they are string-interpolated in build_traversal_sql)
@@ -194,11 +194,11 @@ module Legion
194
194
  end
195
195
 
196
196
  sql = Helpers::GraphQuery.build_traversal_sql(depth: depth, relation_types: relation_types)
197
- db = Legion::Data::Model::ApolloEntry.db
197
+ db = Helpers::DataModels.apollo_entry.db
198
198
  entries = db.fetch(sql, entry_id: entry_id).all
199
199
 
200
200
  if entries.any? && agent_id != 'unknown'
201
- Legion::Data::Model::ApolloAccessLog.create(
201
+ Helpers::DataModels.apollo_access_log.create(
202
202
  entry_id: entry_id, agent_id: agent_id, action: 'query'
203
203
  )
204
204
  end
@@ -217,13 +217,13 @@ module Legion
217
217
  end
218
218
 
219
219
  def redistribute_knowledge(agent_id:, min_confidence: Helpers::Confidence.apollo_setting(:query, :redistribute_min_confidence, default: 0.5), **)
220
- return { success: false, error: 'apollo_data_not_available' } unless defined?(Legion::Data::Model::ApolloEntry)
220
+ return { success: false, error: 'apollo_data_not_available' } unless Helpers::DataModels.apollo_entry_available?
221
221
 
222
222
  log.debug("Apollo Knowledge.redistribute_knowledge agent_id=#{agent_id} min_confidence=#{min_confidence}")
223
- entries = Legion::Data::Model::ApolloEntry
224
- .where(source_agent: agent_id, status: 'confirmed')
225
- .where { confidence > min_confidence }
226
- .all
223
+ entries = Helpers::DataModels.apollo_entry
224
+ .where(source_agent: agent_id, status: 'confirmed')
225
+ .where { confidence > min_confidence }
226
+ .all
227
227
 
228
228
  return { success: true, redistributed: 0 } if entries.empty?
229
229
 
@@ -254,24 +254,29 @@ module Legion
254
254
  def retrieve_relevant(query: nil, limit: Helpers::Confidence.apollo_setting(:query, :retrieval_limit, default: 5), min_confidence: Helpers::GraphQuery.default_query_min_confidence, tags: nil, domain: nil, skip: false, **) # rubocop:disable Layout/LineLength
255
255
  return { status: :skipped } if skip
256
256
 
257
- return { success: false, error: 'apollo_data_not_available' } unless defined?(Legion::Data::Model::ApolloEntry)
257
+ return { success: false, error: 'apollo_data_not_available' } unless Helpers::DataModels.apollo_entry_available?
258
258
 
259
259
  query = normalize_text_input(query)
260
260
  log.debug("Apollo Knowledge.retrieve_relevant query_length=#{query.length} limit=#{limit} min_confidence=#{min_confidence} tags=#{Array(tags).size} domain=#{domain || 'nil'}") # rubocop:disable Layout/LineLength
261
261
  return { success: true, entries: [], count: 0 } if query.nil? || query.to_s.strip.empty?
262
262
 
263
263
  embedding = embed_text(query)
264
+ if embedding.nil?
265
+ log.warn('Apollo Knowledge.retrieve_relevant embedding unavailable; returning empty result')
266
+ return { success: true, entries: [], count: 0, degraded: :no_embedding }
267
+ end
268
+
264
269
  sql = Helpers::GraphQuery.build_semantic_search_sql(
265
270
  limit: limit, min_confidence: min_confidence,
266
271
  statuses: %w[confirmed candidate], tags: tags, domain: domain
267
272
  )
268
273
 
269
- db = Legion::Data::Model::ApolloEntry.db
274
+ db = Helpers::DataModels.apollo_entry.db
270
275
  entries = db.fetch(sql, embedding: Sequel.lit("'[#{embedding.join(',')}]'::vector")).all
271
276
  entries = entries.reject { |e| e[:distance].respond_to?(:nan?) && e[:distance].nan? }
272
277
 
273
278
  entries.each do |entry|
274
- Legion::Data::Model::ApolloEntry.where(id: entry[:id]).update(
279
+ Helpers::DataModels.apollo_entry.where(id: entry[:id]).update(
275
280
  confidence: Helpers::Confidence.apply_retrieval_boost(confidence: entry[:confidence]),
276
281
  updated_at: Time.now
277
282
  )
@@ -360,13 +365,7 @@ module Legion
360
365
  return { success: false, error: 'content is required' }
361
366
  end
362
367
 
363
- if content_type.nil?
364
- log.warn('[apollo][handle_ingest] early-return: content_type is required ' \
365
- "content_length=#{content.to_s.length}")
366
- return { success: false, error: 'content_type is required' }
367
- end
368
-
369
- return nil if defined?(Legion::Data::Model::ApolloEntry)
368
+ return nil if Helpers::DataModels.apollo_entry_available?
370
369
 
371
370
  log.warn('[apollo][handle_ingest] early-return: apollo_data_not_available ' \
372
371
  "content_type=#{content_type}")
@@ -382,18 +381,18 @@ module Legion
382
381
  def embed_text(text)
383
382
  text = normalize_text_input(text)
384
383
  log.debug("Apollo Knowledge.embed_text text_length=#{text.length}")
385
- result = Legion::LLM::Embeddings.generate(text: text)
384
+ result = Legion::LLM::Call::Embeddings.generate(text: text)
386
385
  vector = result.is_a?(Hash) ? result[:vector] : result
387
- if vector.is_a?(Array) && vector.any?
386
+ if vector.is_a?(Array) && vector.any? && vector.any? { |v| v != 0.0 }
388
387
  log.debug("Apollo Knowledge.embed_text vector_dimensions=#{vector.length}")
389
388
  vector
390
389
  else
391
- log.warn('Apollo Knowledge.embed_text returned no vector; using zero-vector fallback')
392
- Array.new(1024, 0.0)
390
+ log.warn('Apollo Knowledge.embed_text returned no usable vector; skipping embedding')
391
+ nil
393
392
  end
394
393
  rescue StandardError => e
395
- log.warn("Apollo Knowledge.embed_text failed: #{e.message}")
396
- Array.new(1024, 0.0)
394
+ handle_exception(e, level: :warn, operation: 'apollo.knowledge.embed_text')
395
+ nil
397
396
  end
398
397
 
399
398
  def normalize_text_input(value)
@@ -405,7 +404,7 @@ module Legion
405
404
 
406
405
  sanitize_for_postgres(result)
407
406
  rescue StandardError => e
408
- log.warn("Apollo Knowledge.normalize_text_input failed: #{e.message}")
407
+ handle_exception(e, level: :warn, operation: 'apollo.knowledge.normalize_text_input')
409
408
  ''
410
409
  end
411
410
 
@@ -429,10 +428,10 @@ module Legion
429
428
  def active_duplicate_for_hash(hash)
430
429
  return nil unless hash
431
430
 
432
- existing = Legion::Data::Model::ApolloEntry
433
- .where(content_hash: hash)
434
- .exclude(status: 'archived')
435
- .first
431
+ existing = Helpers::DataModels.apollo_entry
432
+ .where(content_hash: hash)
433
+ .exclude(status: 'archived')
434
+ .first
436
435
  existing&.update(confidence: [existing.confidence + Helpers::Confidence.retrieval_boost, 1.0].min)
437
436
  log.debug("Apollo Knowledge.active_duplicate_for_hash matched entry_id=#{existing.id}") if existing
438
437
  existing
@@ -452,21 +451,21 @@ module Legion
452
451
  end
453
452
 
454
453
  def create_candidate_entry(content:, content_type:, context:, metadata:, content_hash:, embedding:)
455
- new_entry = Legion::Data::Model::ApolloEntry.create(
454
+ new_entry = Helpers::DataModels.apollo_entry.create(
456
455
  content: content,
457
456
  content_type: content_type,
458
457
  confidence: Helpers::Confidence.initial_confidence,
459
458
  source_agent: metadata[:source_agent],
460
459
  source_provider: metadata[:source_provider],
461
460
  source_channel: metadata[:source_channel],
462
- source_context: ::JSON.dump(context.is_a?(Hash) ? context : {}),
461
+ source_context: json_dump(context.is_a?(Hash) ? context : {}),
463
462
  tags: Sequel.pg_array(metadata[:tags]),
464
463
  status: 'candidate',
465
464
  knowledge_domain: metadata[:domain],
466
465
  submitted_by: metadata[:submitted_by],
467
466
  submitted_from: metadata[:submitted_from],
468
467
  content_hash: content_hash,
469
- embedding: Sequel.lit("'[#{embedding.join(',')}]'::vector")
468
+ embedding: embedding ? Sequel.lit("'[#{embedding.join(',')}]'::vector") : nil
470
469
  )
471
470
  log.info("Apollo Knowledge.handle_ingest created entry_id=#{new_entry.id} status=candidate domain=#{metadata[:domain]} source_agent=#{metadata[:source_agent]}") # rubocop:disable Layout/LineLength
472
471
  new_entry.id
@@ -478,7 +477,7 @@ module Legion
478
477
 
479
478
  def list_entries_chronologically(query:, limit:, status:, status_defaulted:, tags:, domain:)
480
479
  log.debug("Apollo Knowledge.list_entries_chronologically limit=#{limit} statuses=#{Array(status).join(',')} status_defaulted=#{status_defaulted} tags=#{Array(tags).size} domain=#{domain || 'nil'}") # rubocop:disable Layout/LineLength
481
- dataset = Legion::Data::Model::ApolloEntry.exclude(status: 'archived')
480
+ dataset = Helpers::DataModels.apollo_entry.exclude(status: 'archived')
482
481
  requested = Array(status).map(&:to_s).reject(&:empty?)
483
482
  dataset = dataset.where(status: requested) unless status_defaulted || requested.empty?
484
483
  dataset = dataset.where(Sequel.lit('tags && ?', Sequel.pg_array(Array(tags)))) if tags && !Array(tags).empty?
@@ -501,12 +500,28 @@ module Legion
501
500
  knowledge_domain: entry[:knowledge_domain] }
502
501
  end
503
502
 
503
+ def record_query_access(entry_id:, agent_id:)
504
+ Helpers::DataModels.apollo_access_log.create(
505
+ entry_id: entry_id, agent_id: agent_id, action: 'query'
506
+ )
507
+ end
508
+
509
+ def boost_entry_after_query(entry_model, entry)
510
+ entry_model.where(id: entry[:id]).update(
511
+ access_count: Sequel.expr(:access_count) + 1,
512
+ confidence: Helpers::Confidence.apply_retrieval_boost(
513
+ confidence: entry[:confidence]
514
+ ),
515
+ updated_at: Time.now
516
+ )
517
+ end
518
+
519
+ def settings
520
+ Legion::Extensions::Apollo.settings
521
+ end
522
+
504
523
  def allowed_domains_for(target_domain)
505
- rules = if defined?(Legion::Settings) && Legion::Settings.dig(:apollo, :domain_isolation)
506
- Legion::Settings.dig(:apollo, :domain_isolation)
507
- else
508
- DOMAIN_ISOLATION
509
- end
524
+ rules = settings[:domain_isolation]
510
525
 
511
526
  allowed = rules[target_domain]
512
527
  return :all if allowed == :all || allowed.nil?
@@ -515,13 +530,13 @@ module Legion
515
530
  end
516
531
 
517
532
  def detect_contradictions(entry_id, embedding, content)
518
- return [] unless embedding && defined?(Legion::Data::Model::ApolloEntry)
533
+ return [] unless embedding && Helpers::DataModels.apollo_entry_available?
519
534
 
520
535
  sim_limit = Helpers::Confidence.apollo_setting(:contradiction, :similar_limit, default: 10)
521
536
  sim_threshold = Helpers::Confidence.apollo_setting(:contradiction, :similarity_threshold, default: 0.7)
522
537
  rel_weight = Helpers::Confidence.apollo_setting(:contradiction, :relation_weight, default: 0.8)
523
538
 
524
- db = Legion::Data::Model::ApolloEntry.db
539
+ db = Helpers::DataModels.apollo_entry.db
525
540
  log.debug("Apollo Knowledge.detect_contradictions entry_id=#{entry_id} similar_limit=#{sim_limit} threshold=#{sim_threshold}")
526
541
  similar = db.fetch(
527
542
  "SELECT id, content, embedding FROM apollo_entries WHERE id != :entry_id AND embedding IS NOT NULL ORDER BY embedding <=> :embedding LIMIT #{sim_limit}", # rubocop:disable Layout/LineLength
@@ -538,13 +553,13 @@ module Legion
538
553
  next unless sim > sim_threshold
539
554
  next unless llm_detects_conflict?(content, existing[:content])
540
555
 
541
- Legion::Data::Model::ApolloRelation.create(
556
+ Helpers::DataModels.apollo_relation.create(
542
557
  from_entry_id: entry_id, to_entry_id: existing[:id],
543
558
  relation_type: 'contradicts', source_agent: 'system:contradiction',
544
559
  weight: rel_weight
545
560
  )
546
561
 
547
- Legion::Data::Model::ApolloEntry.where(id: [entry_id, existing[:id]]).update(status: 'disputed')
562
+ Helpers::DataModels.apollo_entry.where(id: [entry_id, existing[:id]]).update(status: 'disputed')
548
563
  contradictions << existing[:id]
549
564
  end
550
565
  log.info("Apollo Knowledge.detect_contradictions entry_id=#{entry_id} contradictions=#{contradictions.size}") if contradictions.any?
@@ -569,17 +584,19 @@ module Legion
569
584
  )
570
585
  result[:data]&.dig(:contradicts) == true
571
586
  rescue StandardError => e
572
- log.warn("Apollo Knowledge.llm_detects_conflict? failed: #{e.message}")
587
+ handle_exception(e, level: :warn, operation: 'apollo.knowledge.llm_detects_conflict')
573
588
  false
574
589
  end
575
590
 
576
591
  def find_corroboration(embedding, content_type_sym, source_agent, source_channel = nil)
592
+ return [false, nil] unless embedding
593
+
577
594
  scan_limit = Helpers::Confidence.apollo_setting(:corroboration, :scan_limit, default: 50)
578
595
  log.debug("Apollo Knowledge.find_corroboration content_type=#{content_type_sym} source_agent=#{source_agent} source_channel=#{source_channel || 'nil'} scan_limit=#{scan_limit}") # rubocop:disable Layout/LineLength
579
- existing = Legion::Data::Model::ApolloEntry
580
- .where(content_type: content_type_sym)
581
- .exclude(embedding: nil)
582
- .limit(scan_limit)
596
+ existing = Helpers::DataModels.apollo_entry
597
+ .where(content_type: content_type_sym)
598
+ .exclude(embedding: nil)
599
+ .limit(scan_limit)
583
600
 
584
601
  existing.each do |entry|
585
602
  next unless entry.embedding
@@ -601,13 +618,6 @@ module Legion
601
618
  confidence: Helpers::Confidence.apply_corroboration_boost(confidence: entry.confidence, weight: weight),
602
619
  updated_at: Time.now
603
620
  )
604
- Legion::Data::Model::ApolloRelation.create(
605
- from_entry_id: entry.id,
606
- to_entry_id: entry.id,
607
- relation_type: 'similar_to',
608
- source_agent: source_agent,
609
- weight: sim
610
- )
611
621
  log.info("Apollo Knowledge.find_corroboration matched entry_id=#{entry.id} source_agent=#{source_agent} similarity=#{sim}")
612
622
  return [true, entry.id]
613
623
  end
@@ -632,12 +642,12 @@ module Legion
632
642
 
633
643
  def upsert_expertise(source_agent:, domain:)
634
644
  log.debug("Apollo Knowledge.upsert_expertise source_agent=#{source_agent} domain=#{domain}")
635
- expertise = Legion::Data::Model::ApolloExpertise
636
- .where(agent_id: source_agent, domain: domain).first
645
+ expertise = Helpers::DataModels.apollo_expertise
646
+ .where(agent_id: source_agent, domain: domain).first
637
647
  if expertise
638
648
  expertise.update(entry_count: expertise.entry_count + 1, last_active_at: Time.now)
639
649
  else
640
- Legion::Data::Model::ApolloExpertise.create(
650
+ Helpers::DataModels.apollo_expertise.create(
641
651
  agent_id: source_agent, domain: domain,
642
652
  proficiency: Helpers::Confidence.apollo_setting(:expertise, :initial_proficiency, default: 0.0),
643
653
  entry_count: 1, last_active_at: Time.now
@@ -646,6 +656,11 @@ module Legion
646
656
  end
647
657
 
648
658
  include Legion::Extensions::Helpers::Lex if defined?(Legion::Extensions::Helpers::Lex)
659
+ include Legion::JSON::Helper
660
+ include Legion::Settings::Helper
661
+ extend Legion::Extensions::Helpers::Lex if defined?(Legion::Extensions::Helpers::Lex)
662
+ extend Legion::JSON::Helper
663
+ extend Legion::Settings::Helper
649
664
  end
650
665
  end
651
666
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative '../helpers/confidence'
4
+ require_relative '../helpers/data_models'
4
5
  require_relative '../helpers/similarity'
5
6
 
6
7
  module Legion
@@ -42,7 +43,8 @@ module Legion
42
43
  )
43
44
 
44
45
  min_age_filter = Sequel.lit(
45
- "COALESCE(updated_at, created_at) < NOW() - INTERVAL '? hours'", min_age_hours
46
+ "COALESCE(updated_at, created_at) < NOW() - (? * INTERVAL '1 hour')",
47
+ min_age_hours
46
48
  )
47
49
 
48
50
  decayed = conn[:apollo_entries]
@@ -65,13 +67,13 @@ module Legion
65
67
  end
66
68
 
67
69
  def check_corroboration(**) # rubocop:disable Metrics/CyclomaticComplexity
68
- unless defined?(Legion::Data::Model::ApolloEntry)
70
+ unless Helpers::DataModels.apollo_entry_available?
69
71
  log.warn('Apollo Maintenance.check_corroboration skipped: apollo_data_not_available')
70
72
  return { success: false, error: 'apollo_data_not_available' }
71
73
  end
72
74
 
73
- candidates = Legion::Data::Model::ApolloEntry.where(status: 'candidate').exclude(embedding: nil).all
74
- confirmed = Legion::Data::Model::ApolloEntry.where(status: 'confirmed').exclude(embedding: nil).all
75
+ candidates = Helpers::DataModels.apollo_entry.where(status: 'candidate').exclude(embedding: nil).limit(100).all
76
+ confirmed = Helpers::DataModels.apollo_entry.where(status: 'confirmed').exclude(embedding: nil).limit(500).all
75
77
  log.debug("Apollo Maintenance.check_corroboration candidates=#{candidates.size} confirmed=#{confirmed.size}")
76
78
 
77
79
  promoted = 0
@@ -104,7 +106,7 @@ module Legion
104
106
  updated_at: Time.now
105
107
  )
106
108
 
107
- Legion::Data::Model::ApolloRelation.create(
109
+ Helpers::DataModels.apollo_relation.create(
108
110
  from_entry_id: candidate.id,
109
111
  to_entry_id: match.id,
110
112
  relation_type: 'similar_to',
@@ -1,10 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative '../helpers/data_models'
4
+
3
5
  module Legion
4
6
  module Extensions
5
7
  module Apollo
6
8
  module Runners
7
9
  module Request
10
+ include Legion::Logging::Helper
11
+ extend Legion::Logging::Helper
8
12
  extend self
9
13
 
10
14
  def self.data_required?
@@ -67,7 +71,7 @@ module Legion
67
71
  end
68
72
 
69
73
  def local_service_available?
70
- defined?(Legion::Data::Model::ApolloEntry) &&
74
+ Helpers::DataModels.apollo_entry_available? &&
71
75
  defined?(Knowledge)
72
76
  end
73
77
 
@@ -81,6 +85,7 @@ module Legion
81
85
  Transport::Messages::Query.new(payload).publish
82
86
  { success: true, dispatched: :transport, payload: payload }
83
87
  rescue StandardError => e
88
+ handle_exception(e, level: :warn, operation: 'apollo.request.publish_query')
84
89
  { success: false, error: e.message }
85
90
  end
86
91
 
@@ -88,6 +93,7 @@ module Legion
88
93
  Transport::Messages::Ingest.new(payload).publish
89
94
  { success: true, dispatched: :transport, payload: payload }
90
95
  rescue StandardError => e
96
+ handle_exception(e, level: :warn, operation: 'apollo.request.publish_ingest')
91
97
  { success: false, error: e.message }
92
98
  end
93
99
  end
@@ -3,7 +3,7 @@
3
3
  module Legion
4
4
  module Extensions
5
5
  module Apollo
6
- VERSION = '0.4.22'
6
+ VERSION = '0.4.24'
7
7
  end
8
8
  end
9
9
  end
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'legion/logging'
4
+ require 'legion/settings'
5
+ require 'legion/json'
3
6
  require 'legion/extensions/apollo/version'
4
7
  require 'legion/extensions/apollo/helpers/confidence'
5
8
  require 'legion/extensions/apollo/helpers/similarity'
@@ -7,6 +10,7 @@ require 'legion/extensions/apollo/helpers/graph_query'
7
10
  require 'legion/extensions/apollo/helpers/tag_normalizer'
8
11
  require 'legion/extensions/apollo/helpers/capability'
9
12
  require 'legion/extensions/apollo/helpers/writeback'
13
+ require 'legion/extensions/apollo/helpers/data_models'
10
14
  require 'legion/extensions/apollo/runners/knowledge'
11
15
  require 'legion/extensions/apollo/runners/expertise'
12
16
  require 'legion/extensions/apollo/runners/maintenance'
@@ -32,11 +36,103 @@ end
32
36
  module Legion
33
37
  module Extensions
34
38
  module Apollo
39
+ extend Legion::Logging::Helper
40
+ extend Legion::Settings::Helper
35
41
  extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core, false
36
42
 
37
43
  def self.remote_invocable?
38
44
  false
39
45
  end
46
+
47
+ def self.default_settings # rubocop:disable Metrics/MethodLength
48
+ {
49
+ confidence: {
50
+ initial: 0.5,
51
+ corroboration_boost: 0.3,
52
+ retrieval_boost: 0.02,
53
+ write_gate: 0.6,
54
+ novelty_gate: 0.3,
55
+ corroboration_similarity: 0.9
56
+ },
57
+ power_law_alpha: 0.05,
58
+ decay_threshold: 0.05,
59
+ stale_days: 90,
60
+ decay_min_age_hours: 168,
61
+ graph: {
62
+ spread_factor: 0.6,
63
+ default_depth: 2,
64
+ min_activation: 0.1
65
+ },
66
+ query: {
67
+ default_limit: 10,
68
+ default_min_confidence: 0.3,
69
+ retrieval_limit: 5,
70
+ redistribute_min_confidence: 0.5,
71
+ mesh_export_min_confidence: 0.5,
72
+ mesh_export_limit: 100
73
+ },
74
+ gas: {
75
+ relate_confidence_gate: 0.7,
76
+ synthesis_confidence_cap: 0.7,
77
+ max_anticipations: 3,
78
+ similar_entries_limit: 3,
79
+ fallback_confidence: 0.5
80
+ },
81
+ maintenance: {
82
+ force_decay_factor: 0.5
83
+ },
84
+ contradiction: {
85
+ similar_limit: 10,
86
+ similarity_threshold: 0.7,
87
+ relation_weight: 0.8
88
+ },
89
+ corroboration: {
90
+ relation_weight: 1.0,
91
+ scan_limit: 50,
92
+ same_provider_weight: 0.5
93
+ },
94
+ expertise: {
95
+ initial_proficiency: 0.0,
96
+ min_agents_at_risk: 2,
97
+ proficiency_cap: 1.0
98
+ },
99
+ entity_extractor: {
100
+ min_confidence: 0.7
101
+ },
102
+ entity_watchdog: {
103
+ concept_keywords: [],
104
+ types: %w[person service repository concept],
105
+ detect_confidence: 0.5,
106
+ exists_min_confidence: 0.1,
107
+ min_confidence: 0.7,
108
+ dedup_threshold: 0.92,
109
+ lookback_seconds: 300,
110
+ log_limit: 50
111
+ },
112
+ domain_isolation: {
113
+ 'claims_optimization' => ['claims_optimization'],
114
+ 'clinical_care' => %w[clinical_care general],
115
+ 'general' => :all
116
+ },
117
+ embedding: {
118
+ provider: nil,
119
+ model: nil
120
+ },
121
+ data: {
122
+ apollo_write: false
123
+ },
124
+ writeback: {
125
+ enabled: true,
126
+ min_content_length: 50
127
+ },
128
+ actors: {
129
+ decay_interval: 3600,
130
+ expertise_interval: 1800,
131
+ corroboration_interval: 900,
132
+ entity_watchdog_interval: 120
133
+ }
134
+ }
135
+ end
40
136
  end
41
137
  end
42
138
  end
@@ -53,7 +53,7 @@ RSpec.describe Legion::Extensions::Apollo::Actor::WritebackVectorize do
53
53
  { vector: Array.new(1024, 0.1), model: 'test', provider: :ollama, dimensions: 1024, tokens: 0 }
54
54
  end
55
55
  end
56
- stub_const('Legion::LLM::Embeddings', embeddings_mod)
56
+ stub_const('Legion::LLM::Call::Embeddings', embeddings_mod)
57
57
  end
58
58
 
59
59
  describe '#runner_function' do
@@ -66,7 +66,7 @@ RSpec.describe Legion::Extensions::Apollo::Actor::WritebackVectorize do
66
66
  let(:payload) { { content: 'test content', content_type: 'observation', tags: %w[test] } }
67
67
 
68
68
  before do
69
- allow(Legion::LLM::Embeddings).to receive(:generate)
69
+ allow(Legion::LLM::Call::Embeddings).to receive(:generate)
70
70
  .and_return({ vector: [0.1] * 1024, model: 'test', provider: :ollama, dimensions: 1024, tokens: 0 })
71
71
  allow(Legion::Extensions::Apollo::Helpers::Capability).to receive(:can_write?).and_return(false)
72
72
  end
@@ -92,7 +92,7 @@ RSpec.describe Legion::Extensions::Apollo::Actor::WritebackVectorize do
92
92
  end
93
93
 
94
94
  it 'returns error hash on failure' do
95
- allow(Legion::LLM::Embeddings).to receive(:generate).and_raise(RuntimeError, 'boom')
95
+ allow(Legion::LLM::Call::Embeddings).to receive(:generate).and_raise(RuntimeError, 'boom')
96
96
 
97
97
  result = actor.handle_vectorize(payload)
98
98
  expect(result[:success]).to be false