lex-apollo 0.4.4 → 0.4.5

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 98c5141f161198efe7dbdd37179e46bf36ec1c58404efa645897bc256975d3f8
4
- data.tar.gz: c2ac6728af5ada2ab45eb8fff0a0e80d6bceac5464305bca18bd836c81fc0ffb
3
+ metadata.gz: 692cd508a98259d83ceaf2c6b752c7c87ddeb219145291bd568fa76430e41577
4
+ data.tar.gz: b02962423b3dff2950af8b51b0cf5035a8db72f6c2ef80c4f1e54f9f0989e25c
5
5
  SHA512:
6
- metadata.gz: 45328d181484cabb6a17aceb9d6d1a816a623fbe1dc8f5c80972b9151a2250f244d88ba88551cf0cecac70229732546a35abe32189a2ff75a4026010279a810e
7
- data.tar.gz: 1eab2286abce601fe8a95b75c0da2edb0017bebb39e09abe05a1c7dbf72658e7955147b2ab1e1b6ed743788ed61a86c4871919e7ab53762ef3e2e40906eea00f
6
+ metadata.gz: 7c4989d252540ae09177a73f389a1f4d1a09c51aee09acdf88bdfbb9ffb3e5d7c6522e8cf0953117518011f199f97f1e76dacb6d88aa2f10a48ecfe1da064497
7
+ data.tar.gz: a806f1485d4f3ef587f2fbd015180c027934ac87b080c4e0264ccb12a478708d89d0aa313ac3c9b2f20ed3c64ec779e2468ec7b457967c7cb6c1ce780324a884
data/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.5] - 2026-03-25
4
+
5
+ ### Added
6
+ - `handle_traverse` method in Knowledge runner: executes recursive CTE graph traversal SQL, returns formatted entries with depth and activation scores (closes #3)
7
+ - `Runners::Request` client-tier runner with `data_required? false`: routes `query`, `retrieve`, `ingest`, `traverse` locally when DB is available or via transport when it isn't (closes #1)
8
+ - REST API (`Api` Sinatra app) with 8 endpoints: health, query, ingest, traverse, retrieve, deprecate, expertise, stats (closes #5)
9
+ - Conditional `require 'legion/extensions/apollo/api'` when Sinatra is available
10
+
11
+ ### Fixed
12
+ - Contradiction detection now orders by embedding distance (`<=>` pgvector operator) before LIMIT, scanning the most semantically similar entries instead of arbitrary rows (closes #4)
13
+ - Whitelisted `relation_types` in `handle_traverse` against `RELATION_TYPES` constant to prevent SQL injection
14
+
15
+ ### Changed
16
+ - GAIA phase wiring updated: `knowledge_retrieval` now targets `Request#retrieve` instead of `Knowledge#retrieve_relevant` (in legion-gaia)
17
+
3
18
  ## [0.4.4] - 2026-03-24
4
19
 
5
20
  ### Changed
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sinatra/base' unless defined?(Sinatra)
4
+ require 'json'
5
+
6
+ module Legion
7
+ module Extensions
8
+ module Apollo
9
+ class Api < Sinatra::Base
10
+ set :host_authorization, permitted: :any
11
+
12
+ before do
13
+ content_type :json
14
+ end
15
+
16
+ helpers do
17
+ def json_body
18
+ body = request.body.read
19
+ return {} if body.empty?
20
+
21
+ ::JSON.parse(body, symbolize_names: true)
22
+ rescue ::JSON::ParserError
23
+ halt 400, { error: 'invalid JSON' }.to_json
24
+ end
25
+
26
+ def runner
27
+ @runner ||= begin
28
+ obj = Object.new
29
+ obj.extend(Runners::Knowledge)
30
+ obj
31
+ end
32
+ end
33
+
34
+ def expertise_runner
35
+ @expertise_runner ||= begin
36
+ obj = Object.new
37
+ obj.extend(Runners::Expertise)
38
+ obj
39
+ end
40
+ end
41
+ end
42
+
43
+ # Health check
44
+ get '/api/apollo/health' do
45
+ available = defined?(Legion::Data::Model::ApolloEntry) ? true : false
46
+ { status: available ? 'ok' : 'degraded', data_available: available }.to_json
47
+ end
48
+
49
+ # Query knowledge (semantic search)
50
+ post '/api/apollo/query' do
51
+ req = json_body
52
+ halt 400, { error: 'query is required' }.to_json unless req[:query]
53
+
54
+ result = runner.handle_query(
55
+ query: req[:query],
56
+ limit: req[:limit] || 10,
57
+ min_confidence: req[:min_confidence] || 0.3,
58
+ status: req[:status] || [:confirmed],
59
+ tags: req[:tags],
60
+ domain: req[:domain],
61
+ agent_id: req[:agent_id] || 'api'
62
+ )
63
+ status result[:success] ? 200 : 500
64
+ result.to_json
65
+ end
66
+
67
+ # Ingest knowledge
68
+ post '/api/apollo/ingest' do
69
+ req = json_body
70
+ halt 400, { error: 'content is required' }.to_json unless req[:content]
71
+ halt 400, { error: 'content_type is required' }.to_json unless req[:content_type]
72
+
73
+ result = runner.handle_ingest(
74
+ content: req[:content],
75
+ content_type: req[:content_type],
76
+ tags: req[:tags] || [],
77
+ source_agent: req[:source_agent] || 'api',
78
+ source_provider: req[:source_provider],
79
+ source_channel: req[:source_channel],
80
+ knowledge_domain: req[:knowledge_domain],
81
+ context: req[:context] || {}
82
+ )
83
+ status result[:success] ? 201 : 500
84
+ result.to_json
85
+ end
86
+
87
+ # Graph traversal
88
+ post '/api/apollo/traverse' do
89
+ req = json_body
90
+ halt 400, { error: 'entry_id is required' }.to_json unless req[:entry_id]
91
+
92
+ result = runner.handle_traverse(
93
+ entry_id: req[:entry_id],
94
+ depth: req[:depth] || 2,
95
+ relation_types: req[:relation_types],
96
+ agent_id: req[:agent_id] || 'api'
97
+ )
98
+ status result[:success] ? 200 : 500
99
+ result.to_json
100
+ end
101
+
102
+ # Retrieve relevant (GAIA-compatible)
103
+ post '/api/apollo/retrieve' do
104
+ req = json_body
105
+ halt 400, { error: 'query is required' }.to_json unless req[:query]
106
+
107
+ result = runner.retrieve_relevant(
108
+ query: req[:query],
109
+ limit: req[:limit] || 5,
110
+ min_confidence: req[:min_confidence] || 0.3,
111
+ tags: req[:tags],
112
+ domain: req[:domain]
113
+ )
114
+ status result[:success] ? 200 : 500
115
+ result.to_json
116
+ end
117
+
118
+ # Deprecate entry
119
+ post '/api/apollo/entries/:id/deprecate' do
120
+ result = runner.deprecate_entry(
121
+ entry_id: params[:id],
122
+ reason: json_body[:reason] || 'deprecated via API'
123
+ )
124
+ result.to_json
125
+ end
126
+
127
+ # Domains at risk — must be declared before /:agent_id to avoid routing conflict
128
+ get '/api/apollo/expertise/at-risk' do
129
+ result = expertise_runner.domains_at_risk
130
+ result.to_json
131
+ end
132
+
133
+ # Expertise for an agent
134
+ get '/api/apollo/expertise/:agent_id' do
135
+ result = expertise_runner.agent_profile(agent_id: params[:agent_id])
136
+ result.to_json
137
+ end
138
+
139
+ # Statistics
140
+ get '/api/apollo/stats' do
141
+ stats = {}
142
+ if defined?(Legion::Data::Model::ApolloEntry)
143
+ stats[:total_entries] = Legion::Data::Model::ApolloEntry.count
144
+ stats[:by_status] = Legion::Data::Model::ApolloEntry.group_and_count(:status).all
145
+ .to_h { |r| [r[:status], r[:count]] }
146
+ stats[:by_content_type] = Legion::Data::Model::ApolloEntry.group_and_count(:content_type).all
147
+ .to_h { |r| [r[:content_type], r[:count]] }
148
+ stats[:total_relations] = Legion::Data::Model::ApolloRelation.count if defined?(Legion::Data::Model::ApolloRelation)
149
+ else
150
+ stats[:error] = 'apollo_data_not_available'
151
+ end
152
+ stats.to_json
153
+ end
154
+ end
155
+ end
156
+ end
157
+ end
@@ -140,6 +140,36 @@ module Legion
140
140
  { success: false, error: e.message }
141
141
  end
142
142
 
143
+ def handle_traverse(entry_id:, depth: Helpers::GraphQuery.default_depth, relation_types: nil, agent_id: 'unknown', **)
144
+ return { success: false, error: 'apollo_data_not_available' } unless defined?(Legion::Data::Model::ApolloEntry)
145
+
146
+ # Whitelist relation_types to prevent SQL injection (they are string-interpolated in build_traversal_sql)
147
+ if relation_types
148
+ allowed = Helpers::Confidence::RELATION_TYPES
149
+ relation_types = relation_types.select { |t| allowed.include?(t.to_s) }
150
+ end
151
+
152
+ sql = Helpers::GraphQuery.build_traversal_sql(depth: depth, relation_types: relation_types)
153
+ db = Legion::Data::Model::ApolloEntry.db
154
+ entries = db.fetch(sql, entry_id: entry_id).all
155
+
156
+ if entries.any? && agent_id != 'unknown'
157
+ Legion::Data::Model::ApolloAccessLog.create(
158
+ entry_id: entry_id, agent_id: agent_id, action: 'query'
159
+ )
160
+ end
161
+
162
+ formatted = entries.map do |entry|
163
+ { id: entry[:id], content: entry[:content], content_type: entry[:content_type],
164
+ confidence: entry[:confidence], tags: entry[:tags], source_agent: entry[:source_agent],
165
+ depth: entry[:depth], activation: entry[:activation] }
166
+ end
167
+
168
+ { success: true, entries: formatted, count: formatted.size }
169
+ rescue Sequel::Error => e
170
+ { success: false, error: e.message }
171
+ end
172
+
143
173
  def redistribute_knowledge(agent_id:, min_confidence: Helpers::Confidence.apollo_setting(:query, :redistribute_min_confidence, default: 0.5), **)
144
174
  return { success: false, error: 'apollo_data_not_available' } unless defined?(Legion::Data::Model::ApolloEntry)
145
175
 
@@ -281,25 +311,30 @@ module Legion
281
311
  sim_threshold = Helpers::Confidence.apollo_setting(:contradiction, :similarity_threshold, default: 0.7)
282
312
  rel_weight = Helpers::Confidence.apollo_setting(:contradiction, :relation_weight, default: 0.8)
283
313
 
284
- similar = Legion::Data::Model::ApolloEntry
285
- .exclude(id: entry_id)
286
- .exclude(embedding: nil)
287
- .limit(sim_limit).all
314
+ db = Legion::Data::Model::ApolloEntry.db
315
+ similar = db.fetch(
316
+ "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
317
+ entry_id: entry_id,
318
+ embedding: Sequel.lit("'[#{embedding.join(',')}]'::vector")
319
+ ).all
288
320
 
289
321
  contradictions = []
290
322
  similar.each do |existing|
291
- sim = Helpers::Similarity.cosine_similarity(vec_a: embedding, vec_b: existing.embedding)
323
+ existing_embedding = existing[:embedding]
324
+ next unless existing_embedding
325
+
326
+ sim = Helpers::Similarity.cosine_similarity(vec_a: embedding, vec_b: existing_embedding)
292
327
  next unless sim > sim_threshold
293
- next unless llm_detects_conflict?(content, existing.content)
328
+ next unless llm_detects_conflict?(content, existing[:content])
294
329
 
295
330
  Legion::Data::Model::ApolloRelation.create(
296
- from_entry_id: entry_id, to_entry_id: existing.id,
331
+ from_entry_id: entry_id, to_entry_id: existing[:id],
297
332
  relation_type: 'contradicts', source_agent: 'system:contradiction',
298
333
  weight: rel_weight
299
334
  )
300
335
 
301
- Legion::Data::Model::ApolloEntry.where(id: [entry_id, existing.id]).update(status: 'disputed')
302
- contradictions << existing.id
336
+ Legion::Data::Model::ApolloEntry.where(id: [entry_id, existing[:id]]).update(status: 'disputed')
337
+ contradictions << existing[:id]
303
338
  end
304
339
  contradictions
305
340
  rescue Sequel::Error
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Apollo
6
+ module Runners
7
+ module Request
8
+ extend self
9
+
10
+ def self.data_required?
11
+ false
12
+ end
13
+
14
+ def query(text:, limit: Helpers::GraphQuery.default_query_limit, min_confidence: Helpers::GraphQuery.default_query_min_confidence, tags: nil, # rubocop:disable Metrics/ParameterLists
15
+ domain: nil, agent_id: 'unknown', **)
16
+ if local_service_available?
17
+ knowledge_host.handle_query(query: text, limit: limit, min_confidence: min_confidence,
18
+ tags: tags, domain: domain, agent_id: agent_id)
19
+ elsif transport_available?
20
+ publish_query(action: :query, query: text, limit: limit, min_confidence: min_confidence,
21
+ tags: tags, domain: domain)
22
+ else
23
+ { success: false, error: :no_path_available }
24
+ end
25
+ end
26
+
27
+ def retrieve(text:, limit: 5, min_confidence: Helpers::GraphQuery.default_query_min_confidence, tags: nil, domain: nil, **)
28
+ if local_service_available?
29
+ knowledge_host.retrieve_relevant(query: text, limit: limit, min_confidence: min_confidence,
30
+ tags: tags, domain: domain)
31
+ elsif transport_available?
32
+ publish_query(action: :query, query: text, limit: limit, min_confidence: min_confidence,
33
+ tags: tags, domain: domain)
34
+ else
35
+ { success: false, error: :no_path_available }
36
+ end
37
+ end
38
+
39
+ def ingest(content:, content_type:, tags: [], source_agent: 'unknown', **)
40
+ if local_service_available?
41
+ knowledge_host.handle_ingest(content: content, content_type: content_type,
42
+ tags: tags, source_agent: source_agent, **)
43
+ elsif transport_available?
44
+ publish_ingest(content: content, content_type: content_type,
45
+ tags: tags, source_agent: source_agent, **)
46
+ else
47
+ { success: false, error: :no_path_available }
48
+ end
49
+ end
50
+
51
+ def traverse(entry_id:, depth: Helpers::GraphQuery.default_depth, relation_types: nil, agent_id: 'unknown', **)
52
+ if local_service_available?
53
+ knowledge_host.handle_traverse(entry_id: entry_id, depth: depth,
54
+ relation_types: relation_types, agent_id: agent_id)
55
+ elsif transport_available?
56
+ publish_query(action: :traverse, entry_id: entry_id, depth: depth,
57
+ relation_types: relation_types)
58
+ else
59
+ { success: false, error: :no_path_available }
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ def knowledge_host
66
+ @knowledge_host ||= Object.new.extend(Knowledge)
67
+ end
68
+
69
+ def local_service_available?
70
+ defined?(Legion::Data::Model::ApolloEntry) &&
71
+ defined?(Knowledge)
72
+ end
73
+
74
+ def transport_available?
75
+ defined?(Legion::Transport) &&
76
+ Legion::Transport.respond_to?(:connected?) &&
77
+ Legion::Transport.connected?
78
+ end
79
+
80
+ def publish_query(**payload)
81
+ Transport::Messages::Query.new(payload).publish
82
+ { success: true, dispatched: :transport, payload: payload }
83
+ rescue StandardError => e
84
+ { success: false, error: e.message }
85
+ end
86
+
87
+ def publish_ingest(**payload)
88
+ Transport::Messages::Ingest.new(payload).publish
89
+ { success: true, dispatched: :transport, payload: payload }
90
+ rescue StandardError => e
91
+ { success: false, error: e.message }
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -3,7 +3,7 @@
3
3
  module Legion
4
4
  module Extensions
5
5
  module Apollo
6
- VERSION = '0.4.4'
6
+ VERSION = '0.4.5'
7
7
  end
8
8
  end
9
9
  end
@@ -9,6 +9,9 @@ require 'legion/extensions/apollo/runners/expertise'
9
9
  require 'legion/extensions/apollo/runners/maintenance'
10
10
  require 'legion/extensions/apollo/runners/entity_extractor'
11
11
  require 'legion/extensions/apollo/runners/gas'
12
+ require 'legion/extensions/apollo/runners/request'
13
+
14
+ require 'legion/extensions/apollo/api' if defined?(Sinatra)
12
15
 
13
16
  if defined?(Legion::Transport)
14
17
  require 'legion/extensions/apollo/transport/exchanges/apollo'
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Stub Sinatra before requiring api.rb so the guard `unless defined?(Sinatra)` fires.
4
+ # This avoids a LoadError when sinatra is not in the bundle.
5
+ unless defined?(Sinatra)
6
+ module Sinatra
7
+ class Base
8
+ class << self
9
+ def set(*, **); end
10
+ def before(*, &); end
11
+ def helpers(*, &); end
12
+ def get(*, &); end
13
+ def post(*, &); end
14
+ def put(*, &); end
15
+ def delete(*, &); end
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+ require 'spec_helper'
22
+ require 'legion/extensions/apollo/api'
23
+
24
+ RSpec.describe Legion::Extensions::Apollo::Api do
25
+ it 'is defined as a Sinatra app' do
26
+ expect(described_class.superclass).to eq(Sinatra::Base)
27
+ end
28
+ end
@@ -15,5 +15,37 @@ RSpec.describe 'Apollo Contradiction Detection' do
15
15
  it 'returns empty when ApolloEntry model unavailable' do
16
16
  expect(knowledge.send(:detect_contradictions, 1, nil, 'test')).to eq([])
17
17
  end
18
+
19
+ it 'returns empty when embedding is nil' do
20
+ expect(knowledge.send(:detect_contradictions, 'uuid-1', nil, 'test')).to eq([])
21
+ end
22
+
23
+ context 'when entries exist' do
24
+ let(:mock_entry_class) { double('ApolloEntry') }
25
+ let(:mock_relation_class) { double('ApolloRelation') }
26
+ let(:mock_db) { double('db') }
27
+ let(:embedding) { Array.new(1536, 0.1) }
28
+
29
+ before do
30
+ stub_const('Legion::Data::Model::ApolloEntry', mock_entry_class)
31
+ stub_const('Legion::Data::Model::ApolloRelation', mock_relation_class)
32
+ allow(mock_entry_class).to receive(:db).and_return(mock_db)
33
+ end
34
+
35
+ it 'queries with ORDER BY embedding distance' do
36
+ allow(mock_db).to receive(:fetch).and_return(double(all: []))
37
+ knowledge.send(:detect_contradictions, 'uuid-1', embedding, 'test')
38
+ expect(mock_db).to have_received(:fetch).with(
39
+ a_string_including('ORDER BY embedding <=> $embedding'),
40
+ hash_including(:entry_id, :embedding)
41
+ )
42
+ end
43
+
44
+ it 'returns empty when no similar entries found' do
45
+ allow(mock_db).to receive(:fetch).and_return(double(all: []))
46
+ result = knowledge.send(:detect_contradictions, 'uuid-1', embedding, 'test')
47
+ expect(result).to eq([])
48
+ end
49
+ end
18
50
  end
19
51
  end
@@ -110,6 +110,8 @@ RSpec.describe Legion::Extensions::Apollo::Runners::Knowledge do
110
110
  let(:mock_entry) { double('entry', id: 'uuid-123', embedding: nil) }
111
111
  let(:empty_dataset) { double('dataset', each: nil) }
112
112
 
113
+ let(:mock_db) { double('db') }
114
+
113
115
  before do
114
116
  stub_const('Legion::Data::Model::ApolloEntry', mock_entry_class)
115
117
  stub_const('Legion::Data::Model::ApolloRelation', mock_relation_class)
@@ -119,8 +121,8 @@ RSpec.describe Legion::Extensions::Apollo::Runners::Knowledge do
119
121
  .and_return(Array.new(1536, 0.0))
120
122
 
121
123
  allow(mock_entry_class).to receive(:where).and_return(double(exclude: double(limit: empty_dataset)))
122
- allow(mock_entry_class).to receive(:exclude)
123
- .and_return(double(exclude: double(limit: double(all: []))))
124
+ allow(mock_entry_class).to receive(:db).and_return(mock_db)
125
+ allow(mock_db).to receive(:fetch).and_return(double(all: []))
124
126
  allow(mock_entry_class).to receive(:create).and_return(mock_entry)
125
127
  allow(mock_expertise_class).to receive(:where).and_return(double(first: nil))
126
128
  allow(mock_expertise_class).to receive(:create)
@@ -346,6 +348,83 @@ RSpec.describe Legion::Extensions::Apollo::Runners::Knowledge do
346
348
  end
347
349
  end
348
350
 
351
+ describe '#handle_traverse' do
352
+ let(:host) { Object.new.extend(described_class) }
353
+
354
+ context 'when Apollo data is not available' do
355
+ before { hide_const('Legion::Data::Model::ApolloEntry') if defined?(Legion::Data::Model::ApolloEntry) }
356
+
357
+ it 'returns a structured error' do
358
+ result = host.handle_traverse(entry_id: 'uuid-123')
359
+ expect(result[:success]).to be false
360
+ expect(result[:error]).to eq('apollo_data_not_available')
361
+ end
362
+ end
363
+
364
+ context 'when Apollo data is available' do
365
+ let(:mock_entry_class) { double('ApolloEntry') }
366
+ let(:mock_access_log_class) { double('ApolloAccessLog') }
367
+ let(:mock_db) { double('db') }
368
+ let(:traversal_results) do
369
+ [{ id: 'uuid-1', content: 'root', content_type: 'fact',
370
+ confidence: 0.8, tags: ['ruby'], source_agent: 'agent-1',
371
+ depth: 0, activation: 1.0 },
372
+ { id: 'uuid-2', content: 'related', content_type: 'concept',
373
+ confidence: 0.6, tags: ['ruby'], source_agent: 'agent-2',
374
+ depth: 1, activation: 0.48 }]
375
+ end
376
+
377
+ before do
378
+ stub_const('Legion::Data::Model::ApolloEntry', mock_entry_class)
379
+ stub_const('Legion::Data::Model::ApolloAccessLog', mock_access_log_class)
380
+ allow(mock_entry_class).to receive(:db).and_return(mock_db)
381
+ allow(mock_db).to receive(:fetch).and_return(double(all: traversal_results))
382
+ allow(mock_access_log_class).to receive(:create)
383
+ end
384
+
385
+ it 'executes traversal SQL and returns formatted entries' do
386
+ result = host.handle_traverse(entry_id: 'uuid-1', agent_id: 'agent-x')
387
+ expect(result[:success]).to be true
388
+ expect(result[:count]).to eq(2)
389
+ expect(result[:entries].first[:depth]).to eq(0)
390
+ expect(result[:entries].last[:activation]).to eq(0.48)
391
+ end
392
+
393
+ it 'logs access for known agents' do
394
+ expect(mock_access_log_class).to receive(:create).with(
395
+ hash_including(agent_id: 'agent-x', action: 'query')
396
+ )
397
+ host.handle_traverse(entry_id: 'uuid-1', agent_id: 'agent-x')
398
+ end
399
+
400
+ it 'skips access logging for unknown agents' do
401
+ expect(mock_access_log_class).not_to receive(:create)
402
+ host.handle_traverse(entry_id: 'uuid-1')
403
+ end
404
+
405
+ it 'filters invalid relation_types' do
406
+ expect(Legion::Extensions::Apollo::Helpers::GraphQuery).to receive(:build_traversal_sql).with(
407
+ hash_including(relation_types: ['causes'])
408
+ ).and_call_original
409
+ host.handle_traverse(entry_id: 'uuid-1', relation_types: %w[causes invalid_type])
410
+ end
411
+ end
412
+
413
+ context 'when Sequel raises an error' do
414
+ before do
415
+ stub_const('Legion::Data::Model::ApolloEntry', Class.new)
416
+ allow(Legion::Data::Model::ApolloEntry).to receive(:db)
417
+ .and_raise(Sequel::Error, 'connection lost')
418
+ end
419
+
420
+ it 'returns a structured error' do
421
+ result = host.handle_traverse(entry_id: 'uuid-1')
422
+ expect(result[:success]).to be false
423
+ expect(result[:error]).to eq('connection lost')
424
+ end
425
+ end
426
+ end
427
+
349
428
  describe '#redistribute_knowledge' do
350
429
  let(:host) { Object.new.extend(described_class) }
351
430
 
@@ -0,0 +1,186 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'legion/extensions/apollo/helpers/confidence'
5
+ require 'legion/extensions/apollo/helpers/similarity'
6
+ require 'legion/extensions/apollo/helpers/embedding'
7
+ require 'legion/extensions/apollo/helpers/graph_query'
8
+ require 'legion/extensions/apollo/runners/knowledge'
9
+ require 'legion/extensions/apollo/runners/request'
10
+
11
+ RSpec.describe Legion::Extensions::Apollo::Runners::Request do
12
+ before do
13
+ # Clear cached knowledge_host between examples
14
+ described_class.instance_variable_set(:@knowledge_host, nil)
15
+ end
16
+
17
+ describe '.data_required?' do
18
+ it 'returns false' do
19
+ expect(described_class.data_required?).to be false
20
+ end
21
+ end
22
+
23
+ describe '.query' do
24
+ context 'when local service is available' do
25
+ let(:mock_entry_class) { double('ApolloEntry') }
26
+ let(:mock_access_log_class) { double('ApolloAccessLog') }
27
+ let(:mock_db) { double('db') }
28
+ let(:sample_entries) do
29
+ [{ id: 'uuid-1', content: 'test', content_type: 'fact',
30
+ confidence: 0.8, distance: 0.15, tags: ['ruby'], source_agent: 'agent-1' }]
31
+ end
32
+
33
+ before do
34
+ stub_const('Legion::Data::Model::ApolloEntry', mock_entry_class)
35
+ stub_const('Legion::Data::Model::ApolloAccessLog', mock_access_log_class)
36
+ allow(Legion::Extensions::Apollo::Helpers::Embedding).to receive(:generate)
37
+ .and_return(Array.new(1536, 0.0))
38
+ allow(mock_entry_class).to receive(:db).and_return(mock_db)
39
+ allow(mock_db).to receive(:fetch).and_return(double(all: sample_entries))
40
+ allow(mock_entry_class).to receive(:where).and_return(double(update: true))
41
+ allow(mock_access_log_class).to receive(:create)
42
+ end
43
+
44
+ it 'delegates to Knowledge.handle_query' do
45
+ result = described_class.query(text: 'test query')
46
+ expect(result[:success]).to be true
47
+ expect(result[:entries]).to be_an(Array)
48
+ end
49
+ end
50
+
51
+ context 'when only transport is available' do
52
+ let(:mock_transport) do
53
+ Module.new do
54
+ def self.connected?
55
+ true
56
+ end
57
+
58
+ def self.respond_to?(method, *args)
59
+ method == :connected? || super
60
+ end
61
+ end
62
+ end
63
+ let(:mock_message) { double('message', publish: true) }
64
+
65
+ before do
66
+ hide_const('Legion::Data::Model::ApolloEntry') if defined?(Legion::Data::Model::ApolloEntry)
67
+ stub_const('Legion::Transport', mock_transport)
68
+ stub_const('Legion::Extensions::Apollo::Transport::Messages::Query',
69
+ double('QueryMsg', new: mock_message))
70
+ end
71
+
72
+ it 'publishes via transport' do
73
+ result = described_class.query(text: 'test query')
74
+ expect(result[:success]).to be true
75
+ expect(result[:dispatched]).to eq(:transport)
76
+ end
77
+ end
78
+
79
+ context 'when neither local nor transport is available' do
80
+ before do
81
+ hide_const('Legion::Data::Model::ApolloEntry') if defined?(Legion::Data::Model::ApolloEntry)
82
+ hide_const('Legion::Transport') if defined?(Legion::Transport)
83
+ end
84
+
85
+ it 'returns no_path_available error' do
86
+ result = described_class.query(text: 'test query')
87
+ expect(result[:success]).to be false
88
+ expect(result[:error]).to eq(:no_path_available)
89
+ end
90
+ end
91
+ end
92
+
93
+ describe '.retrieve' do
94
+ context 'when local service is available' do
95
+ let(:mock_entry_class) { double('ApolloEntry') }
96
+ let(:mock_db) { double('db') }
97
+
98
+ before do
99
+ stub_const('Legion::Data::Model::ApolloEntry', mock_entry_class)
100
+ allow(Legion::Extensions::Apollo::Helpers::Embedding).to receive(:generate)
101
+ .and_return(Array.new(1536, 0.0))
102
+ allow(mock_entry_class).to receive(:db).and_return(mock_db)
103
+ allow(mock_db).to receive(:fetch).and_return(double(all: []))
104
+ allow(mock_entry_class).to receive(:where).and_return(double(update: true))
105
+ end
106
+
107
+ it 'delegates to Knowledge.retrieve_relevant' do
108
+ result = described_class.retrieve(text: 'test query')
109
+ expect(result[:success]).to be true
110
+ expect(result[:entries]).to eq([])
111
+ end
112
+ end
113
+
114
+ context 'when neither path is available' do
115
+ before do
116
+ hide_const('Legion::Data::Model::ApolloEntry') if defined?(Legion::Data::Model::ApolloEntry)
117
+ hide_const('Legion::Transport') if defined?(Legion::Transport)
118
+ end
119
+
120
+ it 'returns no_path_available' do
121
+ result = described_class.retrieve(text: 'test')
122
+ expect(result[:success]).to be false
123
+ expect(result[:error]).to eq(:no_path_available)
124
+ end
125
+ end
126
+ end
127
+
128
+ describe '.ingest' do
129
+ context 'when local service is available' do
130
+ let(:mock_entry_class) { double('ApolloEntry') }
131
+ let(:mock_expertise_class) { double('ApolloExpertise') }
132
+ let(:mock_access_log_class) { double('ApolloAccessLog') }
133
+ let(:mock_entry) { double('entry', id: 'uuid-123', embedding: nil) }
134
+
135
+ before do
136
+ stub_const('Legion::Data::Model::ApolloEntry', mock_entry_class)
137
+ stub_const('Legion::Data::Model::ApolloExpertise', mock_expertise_class)
138
+ stub_const('Legion::Data::Model::ApolloAccessLog', mock_access_log_class)
139
+ allow(Legion::Extensions::Apollo::Helpers::Embedding).to receive(:generate)
140
+ .and_return(Array.new(1536, 0.0))
141
+ allow(mock_entry_class).to receive(:where).and_return(double(exclude: double(limit: double(each: nil))))
142
+ allow(mock_entry_class).to receive(:exclude)
143
+ .and_return(double(exclude: double(limit: double(all: []))))
144
+ allow(mock_entry_class).to receive(:db).and_return(double(fetch: double(all: [])))
145
+ allow(mock_entry_class).to receive(:create).and_return(mock_entry)
146
+ allow(mock_expertise_class).to receive(:where).and_return(double(first: nil))
147
+ allow(mock_expertise_class).to receive(:create)
148
+ allow(mock_access_log_class).to receive(:create)
149
+ end
150
+
151
+ it 'delegates to Knowledge.handle_ingest' do
152
+ result = described_class.ingest(content: 'test fact', content_type: 'fact', source_agent: 'agent-1')
153
+ expect(result[:success]).to be true
154
+ expect(result[:entry_id]).to eq('uuid-123')
155
+ end
156
+ end
157
+
158
+ context 'when neither path is available' do
159
+ before do
160
+ hide_const('Legion::Data::Model::ApolloEntry') if defined?(Legion::Data::Model::ApolloEntry)
161
+ hide_const('Legion::Transport') if defined?(Legion::Transport)
162
+ end
163
+
164
+ it 'returns no_path_available' do
165
+ result = described_class.ingest(content: 'test', content_type: 'fact')
166
+ expect(result[:success]).to be false
167
+ expect(result[:error]).to eq(:no_path_available)
168
+ end
169
+ end
170
+ end
171
+
172
+ describe '.traverse' do
173
+ context 'when neither path is available' do
174
+ before do
175
+ hide_const('Legion::Data::Model::ApolloEntry') if defined?(Legion::Data::Model::ApolloEntry)
176
+ hide_const('Legion::Transport') if defined?(Legion::Transport)
177
+ end
178
+
179
+ it 'returns no_path_available' do
180
+ result = described_class.traverse(entry_id: 'uuid-1')
181
+ expect(result[:success]).to be false
182
+ expect(result[:error]).to eq(:no_path_available)
183
+ end
184
+ end
185
+ end
186
+ 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.4.4
4
+ version: 0.4.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -153,6 +153,7 @@ files:
153
153
  - lib/legion/extensions/apollo/actors/gas_subscriber.rb
154
154
  - lib/legion/extensions/apollo/actors/ingest.rb
155
155
  - lib/legion/extensions/apollo/actors/query_responder.rb
156
+ - lib/legion/extensions/apollo/api.rb
156
157
  - lib/legion/extensions/apollo/client.rb
157
158
  - lib/legion/extensions/apollo/gaia_integration.rb
158
159
  - lib/legion/extensions/apollo/helpers/confidence.rb
@@ -165,6 +166,7 @@ files:
165
166
  - lib/legion/extensions/apollo/runners/gas.rb
166
167
  - lib/legion/extensions/apollo/runners/knowledge.rb
167
168
  - lib/legion/extensions/apollo/runners/maintenance.rb
169
+ - lib/legion/extensions/apollo/runners/request.rb
168
170
  - lib/legion/extensions/apollo/transport.rb
169
171
  - lib/legion/extensions/apollo/transport/exchanges/apollo.rb
170
172
  - lib/legion/extensions/apollo/transport/exchanges/llm_audit.rb
@@ -179,6 +181,7 @@ files:
179
181
  - spec/legion/extensions/apollo/actors/expertise_aggregator_spec.rb
180
182
  - spec/legion/extensions/apollo/actors/gas_subscriber_spec.rb
181
183
  - spec/legion/extensions/apollo/actors/ingest_spec.rb
184
+ - spec/legion/extensions/apollo/api_spec.rb
182
185
  - spec/legion/extensions/apollo/client_spec.rb
183
186
  - spec/legion/extensions/apollo/contradiction_spec.rb
184
187
  - spec/legion/extensions/apollo/gaia_integration_spec.rb
@@ -196,6 +199,7 @@ files:
196
199
  - spec/legion/extensions/apollo/runners/gas_synthesize_spec.rb
197
200
  - spec/legion/extensions/apollo/runners/knowledge_spec.rb
198
201
  - spec/legion/extensions/apollo/runners/maintenance_spec.rb
202
+ - spec/legion/extensions/apollo/runners/request_spec.rb
199
203
  - spec/legion/extensions/apollo/transport/messages/ingest_spec.rb
200
204
  - spec/legion/extensions/apollo/transport/messages/query_spec.rb
201
205
  - spec/spec_helper.rb