lex-apollo 0.4.28 → 0.4.44

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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +107 -0
  3. data/README.md +42 -5
  4. data/lib/legion/extensions/apollo/actors/contradiction_scanner.rb +63 -0
  5. data/lib/legion/extensions/apollo/actors/entity_watchdog.rb +1 -1
  6. data/lib/legion/extensions/apollo/actors/gas_subscriber.rb +4 -6
  7. data/lib/legion/extensions/apollo/api.rb +19 -9
  8. data/lib/legion/extensions/apollo/helpers/capability.rb +8 -20
  9. data/lib/legion/extensions/apollo/helpers/confidence.rb +11 -18
  10. data/lib/legion/extensions/apollo/helpers/entity_watchdog.rb +1 -1
  11. data/lib/legion/extensions/apollo/helpers/graph_query.rb +37 -51
  12. data/lib/legion/extensions/apollo/helpers/writeback.rb +9 -13
  13. data/lib/legion/extensions/apollo/runners/entity_extractor.rb +1 -1
  14. data/lib/legion/extensions/apollo/runners/expertise.rb +3 -3
  15. data/lib/legion/extensions/apollo/runners/gas.rb +95 -25
  16. data/lib/legion/extensions/apollo/runners/knowledge.rb +177 -113
  17. data/lib/legion/extensions/apollo/runners/maintenance.rb +135 -36
  18. data/lib/legion/extensions/apollo/runners/request.rb +4 -5
  19. data/lib/legion/extensions/apollo/transport.rb +20 -0
  20. data/lib/legion/extensions/apollo/version.rb +1 -1
  21. data/lib/legion/extensions/apollo.rb +2 -0
  22. data/spec/legion/extensions/apollo/actors/contradiction_scanner_spec.rb +38 -0
  23. data/spec/legion/extensions/apollo/actors/entity_watchdog_spec.rb +1 -1
  24. data/spec/legion/extensions/apollo/actors/gas_subscriber_spec.rb +4 -14
  25. data/spec/legion/extensions/apollo/api_spec.rb +11 -21
  26. data/spec/legion/extensions/apollo/contradiction_spec.rb +0 -9
  27. data/spec/legion/extensions/apollo/helpers/capability_spec.rb +2 -2
  28. data/spec/legion/extensions/apollo/helpers/writeback_spec.rb +31 -3
  29. data/spec/legion/extensions/apollo/runners/corroboration_integration_spec.rb +191 -0
  30. data/spec/legion/extensions/apollo/runners/entity_extractor_spec.rb +2 -2
  31. data/spec/legion/extensions/apollo/runners/gas_deposit_integration_spec.rb +92 -0
  32. data/spec/legion/extensions/apollo/runners/gas_spec.rb +27 -33
  33. data/spec/legion/extensions/apollo/runners/ingest_transaction_integration_spec.rb +114 -0
  34. data/spec/legion/extensions/apollo/runners/knowledge_spec.rb +129 -57
  35. data/spec/legion/extensions/apollo/runners/lifecycle_integration_spec.rb +165 -0
  36. data/spec/legion/extensions/apollo/runners/maintenance_spec.rb +22 -108
  37. data/spec/legion/extensions/apollo/runners/request_spec.rb +43 -26
  38. metadata +35 -2
  39. data/spec/legion/extensions/apollo/helpers/graph_query_spec.rb +0 -100
@@ -31,16 +31,10 @@ module Legion
31
31
  content = response_content(response)
32
32
  return false if content.nil? || content.length < min_content_length
33
33
 
34
- tool_calls = extract_tool_calls(response, enrichments)
35
- research_calls = tool_calls.select { |tc| RESEARCH_TOOLS.include?(tc[:name] || tc['name']) }
36
-
37
- return false if research_calls.empty?
38
-
39
- apollo_results = enrichments['rag_context:apollo_results']
40
- return true if apollo_results.nil? || (apollo_results[:count] || 0).zero?
41
-
42
- # Apollo had results — only capture if LLM also did additional research
43
- research_calls.any?
34
+ extract_tool_calls(response, enrichments).any? do |call|
35
+ name = call.respond_to?(:name) ? call.name : call[:name] || call['name']
36
+ RESEARCH_TOOLS.include?(name)
37
+ end
44
38
  end
45
39
 
46
40
  def build_payload(request:, response:, source_channel: nil)
@@ -147,9 +141,11 @@ module Legion
147
141
 
148
142
  def extract_tool_calls(response, enrichments)
149
143
  calls = []
150
- calls += Array(response.tool_calls) if response.respond_to?(:tool_calls)
151
- calls += Array(enrichments['tool_calls']) if enrichments['tool_calls']
152
- calls.uniq { |tc| tc[:name] || tc['name'] }
144
+ calls.concat(Array(response.tools)) if response.respond_to?(:tools)
145
+ calls.concat(Array(response.tool_calls)) if response.respond_to?(:tool_calls)
146
+ calls.concat(Array(enrichments['tool_calls'])) if enrichments['tool_calls']
147
+ calls.uniq! { |call| call.respond_to?(:name) ? call.name : call[:name] || call['name'] }
148
+ calls
153
149
  end
154
150
 
155
151
  def derive_tags(query)
@@ -8,7 +8,7 @@ module Legion
8
8
  DEFAULT_ENTITY_TYPES = %w[person service repository concept].freeze
9
9
  DEFAULT_MIN_CONFIDENCE = 0.7
10
10
 
11
- def extract_entities(text:, entity_types: nil, min_confidence: Helpers::Confidence.apollo_setting(:entity_extractor, :min_confidence, default: DEFAULT_MIN_CONFIDENCE), **) # rubocop:disable Layout/LineLength
11
+ def extract_entities(text:, entity_types: nil, min_confidence: Legion::Extensions::Apollo.settings[:entity_extractor][:min_confidence], **)
12
12
  if text.to_s.strip.empty?
13
13
  log.debug('Apollo EntityExtractor.extract_entities skipped reason=empty_text')
14
14
  return { success: true, entities: [], source: :empty }
@@ -7,11 +7,11 @@ module Legion
7
7
  module Apollo
8
8
  module Runners
9
9
  module Expertise
10
- def get_expertise(domain:, min_proficiency: Helpers::Confidence.apollo_setting(:expertise, :initial_proficiency, default: 0.0), **)
10
+ def get_expertise(domain:, min_proficiency: Legion::Extensions::Apollo.settings[:expertise][:initial_proficiency], **)
11
11
  { action: :expertise_query, domain: domain, min_proficiency: min_proficiency }
12
12
  end
13
13
 
14
- def domains_at_risk(min_agents: Helpers::Confidence.apollo_setting(:expertise, :min_agents_at_risk, default: 2), **)
14
+ def domains_at_risk(min_agents: Legion::Extensions::Apollo.settings[:expertise][:min_agents_at_risk], **)
15
15
  { action: :domains_at_risk, min_agents: min_agents }
16
16
  end
17
17
 
@@ -75,7 +75,7 @@ module Legion
75
75
 
76
76
  def expertise_proficiency(confidences)
77
77
  avg = confidences.sum / confidences.size
78
- cap = Helpers::Confidence.apollo_setting(:expertise, :proficiency_cap, default: 1.0)
78
+ cap = Legion::Extensions::Apollo.settings[:expertise][:proficiency_cap]
79
79
  [avg * Math.log2(confidences.size + 1), cap].min
80
80
  end
81
81
 
@@ -27,11 +27,11 @@ module Legion
27
27
  json_parse(str)
28
28
  end
29
29
 
30
- def relate_confidence_gate = Helpers::Confidence.apollo_setting(:gas, :relate_confidence_gate, default: RELATE_CONFIDENCE_GATE)
31
- def synthesis_confidence_cap = Helpers::Confidence.apollo_setting(:gas, :synthesis_confidence_cap, default: SYNTHESIS_CONFIDENCE_CAP)
32
- def max_anticipations = Helpers::Confidence.apollo_setting(:gas, :max_anticipations, default: MAX_ANTICIPATIONS)
33
- def similar_entries_limit = Helpers::Confidence.apollo_setting(:gas, :similar_entries_limit, default: 3)
34
- def fallback_confidence = Helpers::Confidence.apollo_setting(:gas, :fallback_confidence, default: 0.5)
30
+ def relate_confidence_gate = Legion::Extensions::Apollo.settings[:gas][:relate_confidence_gate]
31
+ def synthesis_confidence_cap = Legion::Extensions::Apollo.settings[:gas][:synthesis_confidence_cap]
32
+ def max_anticipations = Legion::Extensions::Apollo.settings[:gas][:max_anticipations]
33
+ def similar_entries_limit = Legion::Extensions::Apollo.settings[:gas][:similar_entries_limit]
34
+ def fallback_confidence = Legion::Extensions::Apollo.settings[:gas][:fallback_confidence]
35
35
 
36
36
  def process(audit_event)
37
37
  unless processable?(audit_event)
@@ -139,31 +139,101 @@ module Legion
139
139
  []
140
140
  end
141
141
 
142
- # Phase 5: Deposit - atomic write to Apollo
143
- def phase_deposit(facts, _entities, _relations, _synthesis, audit_event)
144
- unless defined?(Runners::Knowledge)
145
- log.debug('GAS phase_deposit skipped reason=knowledge_runner_unavailable')
146
- return { deposited: 0 }
142
+ # Phase 5: Deposit entries, then link successfully stored facts.
143
+ def phase_deposit(facts, _entities, relations, synthesis, audit_event)
144
+ counts = { deposited: 0, failed: 0, facts: 0, synthesis: 0 }
145
+ unless defined?(Legion::Extensions::Apollo::Runners::Knowledge)
146
+ return counts.merge(failed: facts.size + synthesis.size, relations: 0,
147
+ relations_failed: relations.size, relations_skipped: 0)
147
148
  end
148
149
 
149
- deposited = 0
150
- facts.each do |fact|
151
- Runners::Knowledge.handle_ingest(
152
- content: fact[:content],
153
- content_type: fact[:content_type].to_s,
154
- tags: %w[gas auto_extracted],
155
- source_agent: 'gas_pipeline',
156
- source_provider: audit_event.dig(:routing, :provider)&.to_s,
157
- knowledge_domain: 'general',
158
- context: { source_request_id: audit_event[:request_id] }
159
- )
160
- deposited += 1
150
+ fact_ids = Array.new(facts.size)
151
+ source_ids = {}
152
+ { facts: facts, synthesis: synthesis }.each do |kind, entries|
153
+ entries.each_with_index do |item, index|
154
+ context = { source_request_id: audit_event[:request_id] }
155
+ if kind == :synthesis
156
+ context[:source_indices] = Array(item[:source_indices])
157
+ context[:source_entry_ids] = context[:source_indices].filter_map do |source_index|
158
+ fact_ids[source_index] if source_index.is_a?(Integer) && source_index.between?(0, facts.size - 1)
159
+ end.uniq
160
+ end
161
+ result = Legion::Extensions::Apollo::Runners::Knowledge.handle_ingest(
162
+ content: item[:content], content_type: item[:content_type].to_s,
163
+ tags: %w[gas auto_extracted], source_agent: 'gas_pipeline',
164
+ source_provider: audit_event.dig(:routing, :provider),
165
+ knowledge_domain: 'general', context: context
166
+ )
167
+ unless result.is_a?(Hash) && result[:success] == true && result[:entry_id]
168
+ counts[:failed] += 1
169
+ next
170
+ end
171
+
172
+ counts[:deposited] += 1
173
+ counts[kind] += 1
174
+ next unless kind == :facts
175
+
176
+ fact_ids[index] = result[:entry_id]
177
+ ids = (source_ids[item[:content]] ||= [])
178
+ ids << result[:entry_id] unless ids.include?(result[:entry_id])
179
+ rescue StandardError => e
180
+ counts[:failed] += 1
181
+ handle_exception(e, level: :warn, operation: 'apollo.gas.phase_deposit_entry')
182
+ end
183
+ end
184
+ counts.merge!(gas_deposit_relations(relations: relations, source_ids: source_ids))
185
+ log.info("GAS phase_deposit deposited=#{counts[:deposited]} failed=#{counts[:failed]} relations=#{counts[:relations]} relations_failed=#{counts[:relations_failed]}") # rubocop:disable Layout/LineLength
186
+ counts
187
+ end
188
+
189
+ def gas_deposit_relations(relations:, source_ids:)
190
+ counts = { relations: 0, relations_failed: 0, relations_skipped: 0 }
191
+ return counts if relations.empty?
192
+ unless Legion::Extensions::Apollo::Helpers::DataModels.apollo_entry_available? &&
193
+ Legion::Extensions::Apollo::Helpers::DataModels.apollo_relation_available?
194
+ return counts.merge(relations_failed: relations.size)
195
+ end
196
+
197
+ entry_model = Legion::Extensions::Apollo::Helpers::DataModels.apollo_entry
198
+ relation_model = Legion::Extensions::Apollo::Helpers::DataModels.apollo_relation
199
+ seen = {}
200
+ relations.each do |relation|
201
+ type = relation[:relation_type].to_s
202
+ weight = relation[:confidence]
203
+ unless gas_relation_accepted?(type: type, confidence: weight)
204
+ counts[:relations_skipped] += 1
205
+ next
206
+ end
207
+ ids = source_ids[relation[:from_content]]
208
+ source = entry_model[ids.first] if ids&.one?
209
+ target = entry_model[relation[:to_id]] if relation[:to_id]
210
+ unless source && target
211
+ counts[:relations_failed] += 1
212
+ next
213
+ end
214
+
215
+ key = [source.id, target.id, type]
216
+ if source.id == target.id || source[:access_scope] != target[:access_scope] || seen[key]
217
+ counts[:relations_skipped] += 1
218
+ next
219
+ end
220
+ seen[key] = true
221
+ relation_model.create(from_entry_id: source.id, to_entry_id: target.id, relation_type: type,
222
+ weight: weight, source_agent: 'gas_pipeline')
223
+ counts[:relations] += 1
161
224
  rescue StandardError => e
162
- handle_exception(e, level: :warn, operation: 'apollo.gas.phase_deposit_fact')
225
+ counts[:relations_failed] += 1
226
+ handle_exception(e, level: :warn, operation: 'apollo.gas.phase_deposit_relation')
163
227
  end
164
- log.info("GAS phase_deposit deposited=#{deposited} facts=#{facts.size}")
165
- { deposited: deposited }
228
+ counts
229
+ end
230
+
231
+ def gas_relation_accepted?(type:, confidence:)
232
+ Legion::Extensions::Apollo::Helpers::Confidence::RELATION_TYPES.include?(type) &&
233
+ confidence.is_a?(Numeric) && confidence.real? && confidence.finite? &&
234
+ confidence.between?(relate_confidence_gate, 1.0)
166
235
  end
236
+ private_class_method :gas_deposit_relations, :gas_relation_accepted?
167
237
 
168
238
  # Phase 6: Anticipate - pre-cache likely follow-up questions
169
239
  def phase_anticipate(facts, _synthesis)