Module: Legion::Extensions::Apollo::Runners::Gas

Defined in:
lib/legion/extensions/apollo/runners/gas.rb

Constant Summary collapse

RELATION_TYPES =
%w[
  similar_to contradicts depends_on causes
  part_of supersedes supports_by extends
].freeze
RELATE_CONFIDENCE_GATE =
0.7
SYNTHESIS_CONFIDENCE_CAP =
0.7
MAX_ANTICIPATIONS =
3

Class Method Summary collapse

Class Method Details

.build_synthesis_entry(item, facts) ⇒ Object



256
257
258
259
260
261
262
263
264
265
266
267
268
269
# File 'lib/legion/extensions/apollo/runners/gas.rb', line 256

def build_synthesis_entry(item, facts)
  source_indices = item[:source_indices] || item['source_indices'] || []
  source_confs = source_indices.filter_map { |i| facts[i]&.dig(:confidence) }
  fb = fallback_confidence
  geo_mean = source_confs.empty? ? fb : geometric_mean(source_confs)

  {
    content:        item[:content] || item['content'],
    content_type:   (item[:content_type] || item['content_type'] || 'inference').to_sym,
    status:         :candidate,
    confidence:     [geo_mean, synthesis_confidence_cap].min,
    source_indices: source_indices
  }
end

.classify_relation(fact, entry) ⇒ Object



147
148
149
150
151
152
153
154
155
156
# File 'lib/legion/extensions/apollo/runners/gas.rb', line 147

def classify_relation(fact, entry)
  fb_conf = fallback_confidence
  if llm_available?
    llm_classify_relation(fact, entry)
  else
    { from_content: fact[:content], to_id: entry[:id], relation_type: 'similar_to', confidence: fb_conf }
  end
rescue StandardError
  { from_content: fact[:content], to_id: entry[:id], relation_type: 'similar_to', confidence: fb_conf }
end

.fallback_confidenceObject



23
# File 'lib/legion/extensions/apollo/runners/gas.rb', line 23

def fallback_confidence     = Helpers::Confidence.apollo_setting(:gas, :fallback_confidence, default: 0.5)

.fallback_relation(fact, entry) ⇒ Object



207
208
209
# File 'lib/legion/extensions/apollo/runners/gas.rb', line 207

def fallback_relation(fact, entry)
  { from_content: fact[:content], to_id: entry[:id], relation_type: 'similar_to', confidence: fallback_confidence }
end

.fetch_similar_entries(facts) ⇒ Object



134
135
136
137
138
139
140
141
142
143
144
145
# File 'lib/legion/extensions/apollo/runners/gas.rb', line 134

def fetch_similar_entries(facts)
  lim = similar_entries_limit
  min_conf = Helpers::GraphQuery.default_query_min_confidence
  entries = []
  facts.each do |fact|
    result = Runners::Knowledge.retrieve_relevant(query: fact[:content], limit: lim, min_confidence: min_conf)
    entries.concat(result[:entries]) if result[:success] && result[:entries]&.any?
  rescue StandardError
    next
  end
  entries.uniq { |e| e[:id] }
end

.geometric_mean(values) ⇒ Object



271
272
273
274
275
276
# File 'lib/legion/extensions/apollo/runners/gas.rb', line 271

def geometric_mean(values)
  return 0.0 if values.empty?

  product = values.reduce(1.0) { |acc, v| acc * v }
  product**(1.0 / values.length)
end

.llm_anticipate(facts) ⇒ Object



278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
# File 'lib/legion/extensions/apollo/runners/gas.rb', line 278

def llm_anticipate(facts)
  facts_text = facts.map { |f| "(#{f[:content_type]}) #{f[:content]}" }.join("\n")

  prompt = <<~PROMPT
    Given these knowledge entries, generate 1-3 likely follow-up questions a user might ask.

    Knowledge:
    #{facts_text}

    Return JSON with a "questions" array of question strings.
  PROMPT

  result = Legion::LLM::Pipeline::GaiaCaller.structured(
    message: prompt.strip,
    schema:  {
      type:       :object,
      properties: {
        questions: { type: :array, items: { type: :string } }
      },
      required:   ['questions']
    },
    phase:   'gas_anticipate'
  )

  content = result.respond_to?(:message) ? result.message[:content] : result.to_s
  parsed = Legion::JSON.load(content)
  questions = parsed.is_a?(Hash) ? (parsed[:questions] || parsed['questions'] || []) : []
  questions = questions.first(max_anticipations)

  questions.map do |q|
    promote_to_pattern_store(question: q, facts: facts)
    { question: q }
  end
rescue StandardError
  []
end

.llm_available?Boolean

Returns:

  • (Boolean)


327
328
329
330
331
# File 'lib/legion/extensions/apollo/runners/gas.rb', line 327

def llm_available?
  defined?(Legion::LLM::Pipeline::GaiaCaller)
rescue StandardError
  false
end

.llm_classify_relation(fact, entry) ⇒ Object



158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
# File 'lib/legion/extensions/apollo/runners/gas.rb', line 158

def llm_classify_relation(fact, entry)
  prompt = <<~PROMPT
    Classify the relationship between these two knowledge entries.
    Valid types: #{RELATION_TYPES.join(', ')}

    Entry A (new): #{fact[:content]}
    Entry B (existing): #{entry[:content]}

    Return JSON with relation_type and confidence (0.0-1.0).
  PROMPT

  result = Legion::LLM::Pipeline::GaiaCaller.structured(
    message: prompt.strip,
    schema:  {
      type:       :object,
      properties: {
        relations: {
          type:  :array,
          items: {
            type:       :object,
            properties: {
              relation_type: { type: :string },
              confidence:    { type: :number }
            },
            required:   %w[relation_type confidence]
          }
        }
      },
      required:   ['relations']
    },
    phase:   'gas_relate'
  )

  content = result.respond_to?(:message) ? result.message[:content] : result.to_s
  parsed = Legion::JSON.load(content)
  rels = parsed.is_a?(Hash) ? (parsed[:relations] || parsed['relations'] || []) : []
  best = rels.max_by { |r| r[:confidence] || r['confidence'] || 0 }

  return fallback_relation(fact, entry) unless best

  conf = best[:confidence] || best['confidence'] || 0
  rtype = best[:relation_type] || best['relation_type']
  return fallback_relation(fact, entry) if conf < relate_confidence_gate || !RELATION_TYPES.include?(rtype)

  { from_content: fact[:content], to_id: entry[:id], relation_type: rtype, confidence: conf }
rescue StandardError
  fallback_relation(fact, entry)
end

.llm_comprehend(messages, response) ⇒ Object



337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
# File 'lib/legion/extensions/apollo/runners/gas.rb', line 337

def llm_comprehend(messages, response)
  prompt = <<~PROMPT
    Extract distinct facts from this exchange. Return JSON array of {content:, content_type:} where content_type is one of: fact, concept, procedure, association.

    User: #{messages.last&.dig(:content)}
    Assistant: #{response}
  PROMPT

  result = Legion::LLM::Pipeline::GaiaCaller.structured(
    message: prompt.strip,
    schema:  {
      type:       :object,
      properties: {
        facts: {
          type:  :array,
          items: {
            type:       :object,
            properties: {
              content:      { type: :string },
              content_type: { type: :string }
            },
            required:   %w[content content_type]
          }
        }
      },
      required:   ['facts']
    },
    phase:   'gas_comprehend'
  )

  content = result.respond_to?(:message) ? result.message[:content] : result.to_s
  parsed = Legion::JSON.load(content)
  facts_array = parsed.is_a?(Hash) ? (parsed[:facts] || parsed['facts'] || []) : Array(parsed)
  facts_array.map do |f|
    {
      content:      f[:content] || f['content'],
      content_type: (f[:content_type] || f['content_type'] || 'fact').to_sym
    }
  end
rescue StandardError
  mechanical_comprehend(messages, response)
end

.llm_synthesize(facts) ⇒ Object



211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
# File 'lib/legion/extensions/apollo/runners/gas.rb', line 211

def llm_synthesize(facts)
  facts_text = facts.each_with_index.map { |f, i| "[#{i}] (#{f[:content_type]}) #{f[:content]}" }.join("\n")

  prompt = <<~PROMPT
    Given these knowledge entries, generate derivative insights (inferences, implications, or connections).
    Each synthesis should combine information from multiple sources.

    Entries:
    #{facts_text}

    Return JSON with a "synthesis" array where each item has: content (string), content_type (inference/implication/connection), source_indices (array of entry indices used).
  PROMPT

  result = Legion::LLM::Pipeline::GaiaCaller.structured(
    message: prompt.strip,
    schema:  {
      type:       :object,
      properties: {
        synthesis: {
          type:  :array,
          items: {
            type:       :object,
            properties: {
              content:        { type: :string },
              content_type:   { type: :string },
              source_indices: { type: :array, items: { type: :integer } }
            },
            required:   %w[content content_type source_indices]
          }
        }
      },
      required:   ['synthesis']
    },
    phase:   'gas_synthesize'
  )

  content = result.respond_to?(:message) ? result.message[:content] : result.to_s
  parsed = Legion::JSON.load(content)
  items = parsed.is_a?(Hash) ? (parsed[:synthesis] || parsed['synthesis'] || []) : []

  items.map { |item| build_synthesis_entry(item, facts) }
rescue StandardError
  []
end

.max_anticipationsObject



21
# File 'lib/legion/extensions/apollo/runners/gas.rb', line 21

def max_anticipations       = Helpers::Confidence.apollo_setting(:gas, :max_anticipations, default: MAX_ANTICIPATIONS)

.mechanical_comprehend(_messages, response) ⇒ Object



333
334
335
# File 'lib/legion/extensions/apollo/runners/gas.rb', line 333

def mechanical_comprehend(_messages, response)
  [{ content: response, content_type: :observation }]
end

.phase_anticipate(facts, _synthesis) ⇒ Object

Phase 6: Anticipate - pre-cache likely follow-up questions



125
126
127
128
129
130
131
132
# File 'lib/legion/extensions/apollo/runners/gas.rb', line 125

def phase_anticipate(facts, _synthesis)
  return [] if facts.empty?
  return [] unless llm_available?

  llm_anticipate(facts)
rescue StandardError
  []
end

.phase_comprehend(audit_event) ⇒ Object

Phase 1: Comprehend - extract typed facts from the exchange



54
55
56
57
58
59
60
61
62
63
# File 'lib/legion/extensions/apollo/runners/gas.rb', line 54

def phase_comprehend(audit_event)
  messages = audit_event[:messages]
  response = audit_event[:response_content]

  if llm_available?
    llm_comprehend(messages, response)
  else
    mechanical_comprehend(messages, response)
  end
end

.phase_deposit(facts, _entities, _relations, _synthesis, audit_event) ⇒ Object

Phase 5: Deposit - atomic write to Apollo



103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# File 'lib/legion/extensions/apollo/runners/gas.rb', line 103

def phase_deposit(facts, _entities, _relations, _synthesis, audit_event)
  return { deposited: 0 } unless defined?(Runners::Knowledge)

  deposited = 0
  facts.each do |fact|
    Runners::Knowledge.handle_ingest(
      content:          fact[:content],
      content_type:     fact[:content_type].to_s,
      tags:             %w[gas auto_extracted],
      source_agent:     'gas_pipeline',
      source_provider:  audit_event.dig(:routing, :provider)&.to_s,
      knowledge_domain: 'general',
      context:          { source_request_id: audit_event[:request_id] }
    )
    deposited += 1
  rescue StandardError => e
    Legion::Logging.warn("GAS deposit error: #{e.message}") if defined?(Legion::Logging)
  end
  { deposited: deposited }
end

.phase_extract(audit_event, _facts) ⇒ Object

Phase 2: Extract - entity extraction (delegates to existing EntityExtractor)



66
67
68
69
70
71
72
73
# File 'lib/legion/extensions/apollo/runners/gas.rb', line 66

def phase_extract(audit_event, _facts)
  return [] unless defined?(Runners::EntityExtractor)

  result = Runners::EntityExtractor.extract_entities(text: audit_event[:response_content])
  result[:success] ? (result[:entities] || []) : []
rescue StandardError
  []
end

.phase_relate(facts, _entities) ⇒ Object

Phase 3: Relate - classify relationships between new and existing entries



76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/legion/extensions/apollo/runners/gas.rb', line 76

def phase_relate(facts, _entities)
  return [] unless defined?(Runners::Knowledge)

  existing = fetch_similar_entries(facts)
  return [] if existing.empty?

  relations = []
  facts.each do |fact|
    existing.each do |entry|
      relation = classify_relation(fact, entry)
      relations << relation if relation
    end
  end
  relations
end

.phase_synthesize(facts, _relations) ⇒ Object

Phase 4: Synthesize - generate derivative knowledge



93
94
95
96
97
98
99
100
# File 'lib/legion/extensions/apollo/runners/gas.rb', line 93

def phase_synthesize(facts, _relations)
  return [] if facts.length < 2
  return [] unless llm_available?

  llm_synthesize(facts)
rescue StandardError
  []
end

.process(audit_event) ⇒ Object



25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# File 'lib/legion/extensions/apollo/runners/gas.rb', line 25

def process(audit_event)
  return { phases_completed: 0, reason: 'no content' } unless processable?(audit_event)

  facts = phase_comprehend(audit_event)
  entities = phase_extract(audit_event, facts)
  relations = phase_relate(facts, entities)
  synthesis = phase_synthesize(facts, relations)
  deposit_result = phase_deposit(facts, entities, relations, synthesis, audit_event)
  anticipations = phase_anticipate(facts, synthesis)

  {
    phases_completed: 6,
    facts:            facts.length,
    entities:         entities.length,
    relations:        relations.length,
    synthesis:        synthesis.length,
    deposited:        deposit_result,
    anticipations:    anticipations.length
  }
rescue StandardError => e
  Legion::Logging.warn("GAS pipeline error: #{e.message}") if defined?(Legion::Logging)
  { phases_completed: 0, error: e.message }
end

.processable?(event) ⇒ Boolean

Returns:

  • (Boolean)


49
50
51
# File 'lib/legion/extensions/apollo/runners/gas.rb', line 49

def processable?(event)
  event[:messages]&.any? == true && !event[:response_content].nil?
end

.promote_to_pattern_store(question:, facts:) ⇒ Object



315
316
317
318
319
320
321
322
323
324
325
# File 'lib/legion/extensions/apollo/runners/gas.rb', line 315

def promote_to_pattern_store(question:, facts:)
  return unless defined?(Legion::Extensions::Agentic::TBI::PatternStore)

  Legion::Extensions::Agentic::TBI::PatternStore.promote_candidate(
    intent:     question,
    resolution: { source: 'gas_anticipate', facts: facts.map { |f| f[:content] } },
    confidence: fallback_confidence
  )
rescue StandardError
  nil
end

.relate_confidence_gateObject



19
# File 'lib/legion/extensions/apollo/runners/gas.rb', line 19

def relate_confidence_gate = Helpers::Confidence.apollo_setting(:gas, :relate_confidence_gate, default: RELATE_CONFIDENCE_GATE)

.similar_entries_limitObject



22
# File 'lib/legion/extensions/apollo/runners/gas.rb', line 22

def similar_entries_limit   = Helpers::Confidence.apollo_setting(:gas, :similar_entries_limit, default: 3)

.synthesis_confidence_capObject



20
# File 'lib/legion/extensions/apollo/runners/gas.rb', line 20

def synthesis_confidence_cap = Helpers::Confidence.apollo_setting(:gas, :synthesis_confidence_cap, default: SYNTHESIS_CONFIDENCE_CAP)