lex-apollo 0.4.4 → 0.4.6
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 +4 -4
- data/CHANGELOG.md +26 -0
- data/lib/legion/extensions/apollo/api.rb +157 -0
- data/lib/legion/extensions/apollo/helpers/embedding.rb +59 -3
- data/lib/legion/extensions/apollo/helpers/graph_query.rb +3 -3
- data/lib/legion/extensions/apollo/helpers/similarity.rb +13 -0
- data/lib/legion/extensions/apollo/runners/knowledge.rb +49 -11
- data/lib/legion/extensions/apollo/runners/request.rb +97 -0
- data/lib/legion/extensions/apollo/version.rb +1 -1
- data/lib/legion/extensions/apollo.rb +3 -0
- data/spec/legion/extensions/apollo/api_spec.rb +28 -0
- data/spec/legion/extensions/apollo/contradiction_spec.rb +32 -0
- data/spec/legion/extensions/apollo/helpers/embedding_spec.rb +6 -6
- data/spec/legion/extensions/apollo/helpers/graph_query_spec.rb +2 -2
- data/spec/legion/extensions/apollo/runners/knowledge_spec.rb +81 -2
- data/spec/legion/extensions/apollo/runners/request_spec.rb +186 -0
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9a1c9042626e5d9e09a783b8c703b11f261573fe1658498a4fa48ad023306baf
|
|
4
|
+
data.tar.gz: 2f3e561b6851825d9d5c67325007802eafea63c10c93a00e37a40170202a0bdc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d2dae8784e591deeb1608671dbabbd313813a6f94214b77de0539d2316d6ef3a34e21d5002eb28a262e98c4c4d57773793be4ba006d185e0fd3afcf6816902c5
|
|
7
|
+
data.tar.gz: 41e11ae62a001fab8e79b514d30c3c28374d38d1d4379926e00752df74e35ccfa79c61b3ac75e7e60341de0420287635413b265eca5fa0cd7fe4e7f02a5fad0f
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.4.6] - 2026-03-25
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Apollo-specific embedding provider/model settings: `apollo.embedding.provider` and `apollo.embedding.model` override LLM defaults
|
|
7
|
+
- `embedding_opts` helper reads Apollo settings and passes `provider:`/`model:` to `Legion::LLM.embed`
|
|
8
|
+
- Local-first embedding: `detect_local_model` checks Ollama for pulled 1024-dim models (`mxbai-embed-large`, `bge-large`, `snowflake-arctic-embed`) before falling back to cloud provider
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- `DEFAULT_DIMENSION` changed from 1536 to 1024 for cross-provider compatibility (Bedrock Titan v2, OpenAI with dimensions:, Ollama models)
|
|
12
|
+
- `Helpers::Embedding.generate` now passes provider/model from Apollo settings, falling back to LLM defaults when not configured
|
|
13
|
+
|
|
14
|
+
## [0.4.5] - 2026-03-25
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- `handle_traverse` method in Knowledge runner: executes recursive CTE graph traversal SQL, returns formatted entries with depth and activation scores (closes #3)
|
|
18
|
+
- `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)
|
|
19
|
+
- REST API (`Api` Sinatra app) with 8 endpoints: health, query, ingest, traverse, retrieve, deprecate, expertise, stats (closes #5)
|
|
20
|
+
- Conditional `require 'legion/extensions/apollo/api'` when Sinatra is available
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
- Contradiction detection now orders by embedding distance (`<=>` pgvector operator) before LIMIT, scanning the most semantically similar entries instead of arbitrary rows (closes #4)
|
|
24
|
+
- Whitelisted `relation_types` in `handle_traverse` against `RELATION_TYPES` constant to prevent SQL injection
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
- GAIA phase wiring updated: `knowledge_retrieval` now targets `Request#retrieve` instead of `Knowledge#retrieve_relevant` (in legion-gaia)
|
|
28
|
+
|
|
3
29
|
## [0.4.4] - 2026-03-24
|
|
4
30
|
|
|
5
31
|
### 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
|
|
@@ -5,7 +5,9 @@ module Legion
|
|
|
5
5
|
module Apollo
|
|
6
6
|
module Helpers
|
|
7
7
|
module Embedding
|
|
8
|
-
DEFAULT_DIMENSION =
|
|
8
|
+
DEFAULT_DIMENSION = 1024
|
|
9
|
+
|
|
10
|
+
LOCAL_EMBEDDING_MODELS = %w[mxbai-embed-large bge-large snowflake-arctic-embed].freeze
|
|
9
11
|
|
|
10
12
|
module_function
|
|
11
13
|
|
|
@@ -15,8 +17,15 @@ module Legion
|
|
|
15
17
|
return zero_vector
|
|
16
18
|
end
|
|
17
19
|
|
|
18
|
-
|
|
19
|
-
vector =
|
|
20
|
+
local_model = detect_local_model
|
|
21
|
+
vector = if local_model
|
|
22
|
+
ollama_embed(text, local_model)
|
|
23
|
+
else
|
|
24
|
+
opts = cloud_embedding_opts
|
|
25
|
+
result = Legion::LLM.embed(text, **opts)
|
|
26
|
+
result.is_a?(Hash) ? result[:vector] : result
|
|
27
|
+
end
|
|
28
|
+
|
|
20
29
|
if vector.is_a?(Array) && vector.any?
|
|
21
30
|
@dimension = vector.size
|
|
22
31
|
vector
|
|
@@ -38,6 +47,53 @@ module Legion
|
|
|
38
47
|
DEFAULT_DIMENSION
|
|
39
48
|
end
|
|
40
49
|
|
|
50
|
+
def ollama_embed(text, model)
|
|
51
|
+
require 'faraday'
|
|
52
|
+
base_url = ollama_base_url
|
|
53
|
+
Legion::Logging.debug("[apollo] embedding via local Ollama: #{model}") if defined?(Legion::Logging)
|
|
54
|
+
conn = Faraday.new(url: base_url) { |f| f.options.timeout = 10 }
|
|
55
|
+
response = conn.post('/api/embed', { model: model, input: text }.to_json,
|
|
56
|
+
'Content-Type' => 'application/json')
|
|
57
|
+
return nil unless response.success?
|
|
58
|
+
|
|
59
|
+
parsed = ::JSON.parse(response.body)
|
|
60
|
+
parsed['embeddings']&.first
|
|
61
|
+
rescue StandardError => e
|
|
62
|
+
Legion::Logging.warn("[apollo] local Ollama embed failed: #{e.message}") if defined?(Legion::Logging)
|
|
63
|
+
nil
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def ollama_base_url
|
|
67
|
+
return 'http://localhost:11434' unless defined?(Legion::Settings)
|
|
68
|
+
|
|
69
|
+
Legion::Settings[:llm].dig(:providers, :ollama, :base_url) || 'http://localhost:11434'
|
|
70
|
+
rescue StandardError
|
|
71
|
+
'http://localhost:11434'
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def cloud_embedding_opts
|
|
75
|
+
return {} unless defined?(Legion::Settings) && !Legion::Settings[:apollo].nil?
|
|
76
|
+
|
|
77
|
+
embedding = Legion::Settings[:apollo][:embedding] || {}
|
|
78
|
+
opts = {}
|
|
79
|
+
opts[:provider] = embedding[:provider].to_sym if embedding[:provider]
|
|
80
|
+
opts[:model] = embedding[:model] if embedding[:model]
|
|
81
|
+
opts
|
|
82
|
+
rescue StandardError
|
|
83
|
+
{}
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def detect_local_model
|
|
87
|
+
return nil unless defined?(Legion::LLM::Discovery::Ollama)
|
|
88
|
+
|
|
89
|
+
LOCAL_EMBEDDING_MODELS.find do |m|
|
|
90
|
+
Legion::LLM::Discovery::Ollama.model_available?(m) ||
|
|
91
|
+
Legion::LLM::Discovery::Ollama.model_available?("#{m}:latest")
|
|
92
|
+
end
|
|
93
|
+
rescue StandardError
|
|
94
|
+
nil
|
|
95
|
+
end
|
|
96
|
+
|
|
41
97
|
def zero_vector
|
|
42
98
|
Array.new(dimension, 0.0)
|
|
43
99
|
end
|
|
@@ -32,7 +32,7 @@ module Legion
|
|
|
32
32
|
SELECT e.id, e.content, e.content_type, e.confidence, e.tags, e.source_agent,
|
|
33
33
|
0 AS depth, 1.0::float AS activation
|
|
34
34
|
FROM apollo_entries e
|
|
35
|
-
WHERE e.id =
|
|
35
|
+
WHERE e.id = :entry_id
|
|
36
36
|
|
|
37
37
|
UNION ALL
|
|
38
38
|
|
|
@@ -72,11 +72,11 @@ module Legion
|
|
|
72
72
|
<<~SQL
|
|
73
73
|
SELECT e.id, e.content, e.content_type, e.confidence, e.tags, e.source_agent,
|
|
74
74
|
e.access_count, e.created_at, e.knowledge_domain,
|
|
75
|
-
(e.embedding <=>
|
|
75
|
+
(e.embedding <=> :embedding) AS distance
|
|
76
76
|
FROM apollo_entries e
|
|
77
77
|
WHERE #{where_clause}
|
|
78
78
|
AND e.embedding IS NOT NULL
|
|
79
|
-
ORDER BY e.embedding <=>
|
|
79
|
+
ORDER BY e.embedding <=> :embedding
|
|
80
80
|
LIMIT #{limit}
|
|
81
81
|
SQL
|
|
82
82
|
end
|
|
@@ -10,6 +10,10 @@ module Legion
|
|
|
10
10
|
module_function
|
|
11
11
|
|
|
12
12
|
def cosine_similarity(vec_a:, vec_b:, **)
|
|
13
|
+
vec_a = parse_vector(vec_a)
|
|
14
|
+
vec_b = parse_vector(vec_b)
|
|
15
|
+
return 0.0 unless vec_a.is_a?(Array) && vec_b.is_a?(Array)
|
|
16
|
+
|
|
13
17
|
dot = vec_a.zip(vec_b).sum { |x, y| x * y }
|
|
14
18
|
mag_a = Math.sqrt(vec_a.sum { |x| x**2 })
|
|
15
19
|
mag_b = Math.sqrt(vec_b.sum { |x| x**2 })
|
|
@@ -18,6 +22,15 @@ module Legion
|
|
|
18
22
|
dot / (mag_a * mag_b)
|
|
19
23
|
end
|
|
20
24
|
|
|
25
|
+
def parse_vector(vec)
|
|
26
|
+
return vec if vec.is_a?(Array)
|
|
27
|
+
return nil unless vec.is_a?(String)
|
|
28
|
+
|
|
29
|
+
::JSON.parse(vec)
|
|
30
|
+
rescue StandardError
|
|
31
|
+
nil
|
|
32
|
+
end
|
|
33
|
+
|
|
21
34
|
def above_corroboration_threshold?(similarity:, **)
|
|
22
35
|
similarity >= Confidence::CORROBORATION_SIMILARITY_THRESHOLD
|
|
23
36
|
end
|
|
@@ -112,6 +112,8 @@ module Legion
|
|
|
112
112
|
db = Legion::Data::Model::ApolloEntry.db
|
|
113
113
|
entries = db.fetch(sql, embedding: Sequel.lit("'[#{embedding.join(',')}]'::vector")).all
|
|
114
114
|
|
|
115
|
+
entries = entries.reject { |e| e[:distance].respond_to?(:nan?) && e[:distance].nan? }
|
|
116
|
+
|
|
115
117
|
entries.each do |entry|
|
|
116
118
|
Legion::Data::Model::ApolloEntry.where(id: entry[:id]).update(
|
|
117
119
|
access_count: Sequel.expr(:access_count) + 1,
|
|
@@ -130,7 +132,7 @@ module Legion
|
|
|
130
132
|
|
|
131
133
|
formatted = entries.map do |entry|
|
|
132
134
|
{ id: entry[:id], content: entry[:content], content_type: entry[:content_type],
|
|
133
|
-
confidence: entry[:confidence], distance: entry[:distance],
|
|
135
|
+
confidence: entry[:confidence], distance: entry[:distance]&.to_f,
|
|
134
136
|
tags: entry[:tags], source_agent: entry[:source_agent],
|
|
135
137
|
knowledge_domain: entry[:knowledge_domain] }
|
|
136
138
|
end
|
|
@@ -140,6 +142,36 @@ module Legion
|
|
|
140
142
|
{ success: false, error: e.message }
|
|
141
143
|
end
|
|
142
144
|
|
|
145
|
+
def handle_traverse(entry_id:, depth: Helpers::GraphQuery.default_depth, relation_types: nil, agent_id: 'unknown', **)
|
|
146
|
+
return { success: false, error: 'apollo_data_not_available' } unless defined?(Legion::Data::Model::ApolloEntry)
|
|
147
|
+
|
|
148
|
+
# Whitelist relation_types to prevent SQL injection (they are string-interpolated in build_traversal_sql)
|
|
149
|
+
if relation_types
|
|
150
|
+
allowed = Helpers::Confidence::RELATION_TYPES
|
|
151
|
+
relation_types = relation_types.select { |t| allowed.include?(t.to_s) }
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
sql = Helpers::GraphQuery.build_traversal_sql(depth: depth, relation_types: relation_types)
|
|
155
|
+
db = Legion::Data::Model::ApolloEntry.db
|
|
156
|
+
entries = db.fetch(sql, entry_id: entry_id).all
|
|
157
|
+
|
|
158
|
+
if entries.any? && agent_id != 'unknown'
|
|
159
|
+
Legion::Data::Model::ApolloAccessLog.create(
|
|
160
|
+
entry_id: entry_id, agent_id: agent_id, action: 'query'
|
|
161
|
+
)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
formatted = entries.map do |entry|
|
|
165
|
+
{ id: entry[:id], content: entry[:content], content_type: entry[:content_type],
|
|
166
|
+
confidence: entry[:confidence], tags: entry[:tags], source_agent: entry[:source_agent],
|
|
167
|
+
depth: entry[:depth], activation: entry[:activation] }
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
{ success: true, entries: formatted, count: formatted.size }
|
|
171
|
+
rescue Sequel::Error => e
|
|
172
|
+
{ success: false, error: e.message }
|
|
173
|
+
end
|
|
174
|
+
|
|
143
175
|
def redistribute_knowledge(agent_id:, min_confidence: Helpers::Confidence.apollo_setting(:query, :redistribute_min_confidence, default: 0.5), **)
|
|
144
176
|
return { success: false, error: 'apollo_data_not_available' } unless defined?(Legion::Data::Model::ApolloEntry)
|
|
145
177
|
|
|
@@ -188,6 +220,7 @@ module Legion
|
|
|
188
220
|
|
|
189
221
|
db = Legion::Data::Model::ApolloEntry.db
|
|
190
222
|
entries = db.fetch(sql, embedding: Sequel.lit("'[#{embedding.join(',')}]'::vector")).all
|
|
223
|
+
entries = entries.reject { |e| e[:distance].respond_to?(:nan?) && e[:distance].nan? }
|
|
191
224
|
|
|
192
225
|
entries.each do |entry|
|
|
193
226
|
Legion::Data::Model::ApolloEntry.where(id: entry[:id]).update(
|
|
@@ -198,7 +231,7 @@ module Legion
|
|
|
198
231
|
|
|
199
232
|
formatted = entries.map do |entry|
|
|
200
233
|
{ id: entry[:id], content: entry[:content], content_type: entry[:content_type],
|
|
201
|
-
confidence: entry[:confidence], distance: entry[:distance],
|
|
234
|
+
confidence: entry[:confidence], distance: entry[:distance]&.to_f,
|
|
202
235
|
tags: entry[:tags], source_agent: entry[:source_agent],
|
|
203
236
|
knowledge_domain: entry[:knowledge_domain] }
|
|
204
237
|
end
|
|
@@ -281,25 +314,30 @@ module Legion
|
|
|
281
314
|
sim_threshold = Helpers::Confidence.apollo_setting(:contradiction, :similarity_threshold, default: 0.7)
|
|
282
315
|
rel_weight = Helpers::Confidence.apollo_setting(:contradiction, :relation_weight, default: 0.8)
|
|
283
316
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
317
|
+
db = Legion::Data::Model::ApolloEntry.db
|
|
318
|
+
similar = db.fetch(
|
|
319
|
+
"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
|
|
320
|
+
entry_id: entry_id,
|
|
321
|
+
embedding: Sequel.lit("'[#{embedding.join(',')}]'::vector")
|
|
322
|
+
).all
|
|
288
323
|
|
|
289
324
|
contradictions = []
|
|
290
325
|
similar.each do |existing|
|
|
291
|
-
|
|
326
|
+
existing_embedding = existing[:embedding]
|
|
327
|
+
next unless existing_embedding
|
|
328
|
+
|
|
329
|
+
sim = Helpers::Similarity.cosine_similarity(vec_a: embedding, vec_b: existing_embedding)
|
|
292
330
|
next unless sim > sim_threshold
|
|
293
|
-
next unless llm_detects_conflict?(content, existing
|
|
331
|
+
next unless llm_detects_conflict?(content, existing[:content])
|
|
294
332
|
|
|
295
333
|
Legion::Data::Model::ApolloRelation.create(
|
|
296
|
-
from_entry_id: entry_id, to_entry_id: existing
|
|
334
|
+
from_entry_id: entry_id, to_entry_id: existing[:id],
|
|
297
335
|
relation_type: 'contradicts', source_agent: 'system:contradiction',
|
|
298
336
|
weight: rel_weight
|
|
299
337
|
)
|
|
300
338
|
|
|
301
|
-
Legion::Data::Model::ApolloEntry.where(id: [entry_id, existing
|
|
302
|
-
contradictions << existing
|
|
339
|
+
Legion::Data::Model::ApolloEntry.where(id: [entry_id, existing[:id]]).update(status: 'disputed')
|
|
340
|
+
contradictions << existing[:id]
|
|
303
341
|
end
|
|
304
342
|
contradictions
|
|
305
343
|
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
|
|
@@ -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
|
|
@@ -12,8 +12,8 @@ RSpec.describe Legion::Extensions::Apollo::Helpers::Embedding do
|
|
|
12
12
|
|
|
13
13
|
it 'returns a zero vector of the correct dimension' do
|
|
14
14
|
result = described_class.generate(text: 'hello world')
|
|
15
|
-
expect(result).to eq(Array.new(
|
|
16
|
-
expect(result.size).to eq(
|
|
15
|
+
expect(result).to eq(Array.new(1024, 0.0))
|
|
16
|
+
expect(result.size).to eq(1024)
|
|
17
17
|
end
|
|
18
18
|
end
|
|
19
19
|
|
|
@@ -24,12 +24,12 @@ RSpec.describe Legion::Extensions::Apollo::Helpers::Embedding do
|
|
|
24
24
|
|
|
25
25
|
it 'returns a zero vector' do
|
|
26
26
|
result = described_class.generate(text: 'hello world')
|
|
27
|
-
expect(result).to eq(Array.new(
|
|
27
|
+
expect(result).to eq(Array.new(1024, 0.0))
|
|
28
28
|
end
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
context 'when Legion::LLM is available and started' do
|
|
32
|
-
let(:mock_vector) { Array.new(
|
|
32
|
+
let(:mock_vector) { Array.new(1024) { rand(-1.0..1.0) } }
|
|
33
33
|
|
|
34
34
|
before do
|
|
35
35
|
llm = Module.new do
|
|
@@ -44,7 +44,7 @@ RSpec.describe Legion::Extensions::Apollo::Helpers::Embedding do
|
|
|
44
44
|
it 'returns the vector from the LLM response hash' do
|
|
45
45
|
result = described_class.generate(text: 'hello world')
|
|
46
46
|
expect(result).to eq(mock_vector)
|
|
47
|
-
expect(Legion::LLM).to have_received(:embed).with('hello world')
|
|
47
|
+
expect(Legion::LLM).to have_received(:embed).with('hello world', **{})
|
|
48
48
|
end
|
|
49
49
|
end
|
|
50
50
|
|
|
@@ -120,7 +120,7 @@ RSpec.describe Legion::Extensions::Apollo::Helpers::Embedding do
|
|
|
120
120
|
end
|
|
121
121
|
|
|
122
122
|
it 'returns the default dimension' do
|
|
123
|
-
expect(described_class.configured_dimension).to eq(
|
|
123
|
+
expect(described_class.configured_dimension).to eq(1024)
|
|
124
124
|
end
|
|
125
125
|
end
|
|
126
126
|
end
|
|
@@ -24,7 +24,7 @@ RSpec.describe Legion::Extensions::Apollo::Helpers::GraphQuery do
|
|
|
24
24
|
expect(sql).to include('apollo_entries')
|
|
25
25
|
expect(sql).to include('apollo_relations')
|
|
26
26
|
expect(sql).to include('WITH RECURSIVE')
|
|
27
|
-
expect(sql).to include('
|
|
27
|
+
expect(sql).to include(':entry_id')
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
it 'includes relation type filter when specified' do
|
|
@@ -49,7 +49,7 @@ RSpec.describe Legion::Extensions::Apollo::Helpers::GraphQuery do
|
|
|
49
49
|
it 'returns SQL with vector placeholder' do
|
|
50
50
|
sql = described_class.build_semantic_search_sql(limit: 5, min_confidence: 0.3)
|
|
51
51
|
expect(sql).to include('apollo_entries')
|
|
52
|
-
expect(sql).to include('
|
|
52
|
+
expect(sql).to include(':embedding')
|
|
53
53
|
expect(sql).to include('<=>')
|
|
54
54
|
expect(sql).to include('LIMIT 5')
|
|
55
55
|
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(:
|
|
123
|
-
|
|
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
|
+
version: 0.4.6
|
|
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
|