lex-mesh 0.3.4 → 0.4.0
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/lib/legion/extensions/mesh/helpers/preference_profile.rb +95 -0
- data/lib/legion/extensions/mesh/runners/preferences.rb +37 -6
- data/lib/legion/extensions/mesh/version.rb +1 -1
- data/spec/legion/extensions/mesh/helpers/preference_profile_spec.rb +82 -0
- data/spec/legion/extensions/mesh/runners/preferences_spec.rb +118 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fbc0df66608354b5104c675cc838ceede54f6efde1e7877a38d1dda1f53a96bd
|
|
4
|
+
data.tar.gz: efc6aaffe3bb7bbc45df398c0b303dcc8945a513d1c949c545e0707ebfdfd368
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a1da77aeb0c9c9f9eea9a9b506457e55c19a5cb68605a8ed2069c37bf35ecc6337d7ff28b781dd50afa96805d08b9288d261d1072fa7dd4078c8aca2d0069fc3
|
|
7
|
+
data.tar.gz: 1700abac8d24b5b1362d4112128fe02fbcc991bb5e4779ecf427196fa8f0fb919419b20d85778d07d7d11621023c1c8e7523b125958ca3fea5ba5c614dabbbbe
|
|
@@ -121,6 +121,84 @@ module Legion
|
|
|
121
121
|
lines.join(' ')
|
|
122
122
|
end
|
|
123
123
|
|
|
124
|
+
MESH_CACHE_TTL = 3600 # 1 hour default
|
|
125
|
+
|
|
126
|
+
def store_mesh_profile(agent_id:, profile:, source_agent_id:)
|
|
127
|
+
@mesh_cache ||= {}
|
|
128
|
+
@mesh_cache[agent_id.to_s] = {
|
|
129
|
+
profile: profile,
|
|
130
|
+
source_agent_id: source_agent_id,
|
|
131
|
+
origin: :mesh_transfer,
|
|
132
|
+
cached_at: Time.now
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if memory_available?
|
|
136
|
+
store_preference(
|
|
137
|
+
owner_id: agent_id,
|
|
138
|
+
domain: 'mesh_profile',
|
|
139
|
+
value: profile.to_s,
|
|
140
|
+
source: 'mesh_transfer'
|
|
141
|
+
)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
{ stored: true, agent_id: agent_id, origin: :mesh_transfer }
|
|
145
|
+
rescue StandardError => e
|
|
146
|
+
{ stored: false, error: e.message }
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def cached_mesh_profile(agent_id:, ttl: MESH_CACHE_TTL)
|
|
150
|
+
@mesh_cache ||= {}
|
|
151
|
+
entry = @mesh_cache[agent_id.to_s]
|
|
152
|
+
return nil unless entry
|
|
153
|
+
|
|
154
|
+
if Time.now - entry[:cached_at] > ttl
|
|
155
|
+
@mesh_cache.delete(agent_id.to_s)
|
|
156
|
+
return nil
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
entry[:profile]
|
|
160
|
+
rescue StandardError
|
|
161
|
+
nil
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def clear_mesh_cache(agent_id: nil)
|
|
165
|
+
@mesh_cache ||= {}
|
|
166
|
+
if agent_id
|
|
167
|
+
@mesh_cache.delete(agent_id.to_s)
|
|
168
|
+
else
|
|
169
|
+
@mesh_cache.clear
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def for_agent(agent_id:, ttl: MESH_CACHE_TTL)
|
|
174
|
+
cached = cached_mesh_profile(agent_id: agent_id, ttl: ttl)
|
|
175
|
+
if cached
|
|
176
|
+
return {
|
|
177
|
+
source: :mesh_cache,
|
|
178
|
+
profile: cached,
|
|
179
|
+
agent_id: agent_id,
|
|
180
|
+
compatibility: compute_compatibility(cached)
|
|
181
|
+
}
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
profile = resolve(owner_id: agent_id)
|
|
185
|
+
|
|
186
|
+
{
|
|
187
|
+
source: :local,
|
|
188
|
+
profile: profile,
|
|
189
|
+
agent_id: agent_id,
|
|
190
|
+
compatibility: compute_compatibility(profile)
|
|
191
|
+
}
|
|
192
|
+
rescue StandardError => e
|
|
193
|
+
{
|
|
194
|
+
source: :local,
|
|
195
|
+
profile: DEFAULTS.dup,
|
|
196
|
+
agent_id: agent_id,
|
|
197
|
+
compatibility: nil,
|
|
198
|
+
error: e.message
|
|
199
|
+
}
|
|
200
|
+
end
|
|
201
|
+
|
|
124
202
|
def memory_available?
|
|
125
203
|
defined?(Legion::Extensions::Memory::Runners::Traces)
|
|
126
204
|
end
|
|
@@ -151,6 +229,23 @@ module Legion
|
|
|
151
229
|
rescue StandardError
|
|
152
230
|
nil
|
|
153
231
|
end
|
|
232
|
+
|
|
233
|
+
def compute_compatibility(profile)
|
|
234
|
+
return nil unless profile.is_a?(Hash) && profile[:personality]
|
|
235
|
+
return nil unless personality_available?
|
|
236
|
+
|
|
237
|
+
personality_runner = Object.new.extend(
|
|
238
|
+
Legion::Extensions::Agentic::Self::Personality::Runners::Personality
|
|
239
|
+
)
|
|
240
|
+
result = personality_runner.personality_compatibility(other_profile: profile[:personality])
|
|
241
|
+
{ score: result[:compatibility], interpretation: result[:interpretation] }
|
|
242
|
+
rescue StandardError
|
|
243
|
+
nil
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def personality_available?
|
|
247
|
+
defined?(Legion::Extensions::Agentic::Self::Personality::Runners::Personality)
|
|
248
|
+
end
|
|
154
249
|
end
|
|
155
250
|
end
|
|
156
251
|
end
|
|
@@ -30,7 +30,12 @@ module Legion
|
|
|
30
30
|
{ success: true, source: :local_default, profile: default_profile, error: e.message }
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
-
def handle_preference_query(**)
|
|
33
|
+
def handle_preference_query(requesting_agent_id: nil, **)
|
|
34
|
+
if trust_available? && requesting_agent_id
|
|
35
|
+
trust_result = check_requester_trust(requesting_agent_id)
|
|
36
|
+
return { success: false, reason: :insufficient_trust, responding_agent_id: local_agent_id } if trust_result == :denied
|
|
37
|
+
end
|
|
38
|
+
|
|
34
39
|
owner_id = local_agent_id
|
|
35
40
|
profile = Helpers::PreferenceProfile.resolve(owner_id: owner_id)
|
|
36
41
|
|
|
@@ -39,7 +44,15 @@ module Legion
|
|
|
39
44
|
{ success: false, error: e.message }
|
|
40
45
|
end
|
|
41
46
|
|
|
42
|
-
def handle_preference_response(correlation_id:, profile:, **)
|
|
47
|
+
def handle_preference_response(correlation_id:, profile:, responding_agent_id: nil, **)
|
|
48
|
+
if responding_agent_id && profile.is_a?(Hash)
|
|
49
|
+
Helpers::PreferenceProfile.store_mesh_profile(
|
|
50
|
+
agent_id: responding_agent_id,
|
|
51
|
+
profile: profile,
|
|
52
|
+
source_agent_id: responding_agent_id
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
|
|
43
56
|
resolved = pending_requests.resolve(correlation_id: correlation_id, result: profile)
|
|
44
57
|
{ resolved: resolved }
|
|
45
58
|
end
|
|
@@ -57,8 +70,9 @@ module Legion
|
|
|
57
70
|
profile
|
|
58
71
|
when 'preference_response'
|
|
59
72
|
handle_preference_response(
|
|
60
|
-
correlation_id:
|
|
61
|
-
profile:
|
|
73
|
+
correlation_id: msg[:correlation_id],
|
|
74
|
+
profile: msg[:profile] || {},
|
|
75
|
+
responding_agent_id: msg[:responding_agent_id]
|
|
62
76
|
)
|
|
63
77
|
else
|
|
64
78
|
{ success: false, error: "unknown preference message type: #{type}" }
|
|
@@ -110,11 +124,28 @@ module Legion
|
|
|
110
124
|
end
|
|
111
125
|
|
|
112
126
|
def default_preference_callback(target_agent_id:)
|
|
113
|
-
lambda do |
|
|
114
|
-
log_debug("[mesh] received preferences for #{target_agent_id}
|
|
127
|
+
lambda do |_profile|
|
|
128
|
+
log_debug("[mesh] received and cached preferences for #{target_agent_id}")
|
|
115
129
|
end
|
|
116
130
|
end
|
|
117
131
|
|
|
132
|
+
def trust_available?
|
|
133
|
+
defined?(Legion::Extensions::Agentic::Social::Trust::Runners::Trust)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def check_requester_trust(agent_id)
|
|
137
|
+
trust_mod = Legion::Extensions::Agentic::Social::Trust::Runners::Trust
|
|
138
|
+
evaluator = Object.new.extend(trust_mod)
|
|
139
|
+
result = evaluator.get_trust(agent_id: agent_id, domain: :general)
|
|
140
|
+
|
|
141
|
+
return :allowed unless result[:found]
|
|
142
|
+
|
|
143
|
+
composite = result.dig(:trust, :composite) || 0.0
|
|
144
|
+
composite >= Helpers::Topology::TRUST_CONSIDER_THRESHOLD ? :allowed : :denied
|
|
145
|
+
rescue StandardError
|
|
146
|
+
:allowed
|
|
147
|
+
end
|
|
148
|
+
|
|
118
149
|
def log_debug(msg)
|
|
119
150
|
log.debug(msg)
|
|
120
151
|
end
|
|
@@ -105,6 +105,88 @@ RSpec.describe Legion::Extensions::Mesh::Helpers::PreferenceProfile do
|
|
|
105
105
|
end
|
|
106
106
|
end
|
|
107
107
|
|
|
108
|
+
describe '.store_mesh_profile' do
|
|
109
|
+
it 'stores a profile with mesh_transfer origin' do
|
|
110
|
+
result = described_class.store_mesh_profile(
|
|
111
|
+
agent_id: 'agent-42',
|
|
112
|
+
profile: { verbosity: :concise, tone: :casual },
|
|
113
|
+
source_agent_id: 'agent-42'
|
|
114
|
+
)
|
|
115
|
+
expect(result[:stored]).to be true
|
|
116
|
+
expect(result[:origin]).to eq(:mesh_transfer)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
describe '.cached_mesh_profile' do
|
|
121
|
+
it 'returns nil when no cached profile exists' do
|
|
122
|
+
result = described_class.cached_mesh_profile(agent_id: 'agent-unknown')
|
|
123
|
+
expect(result).to be_nil
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
it 'returns cached profile after store' do
|
|
127
|
+
described_class.store_mesh_profile(
|
|
128
|
+
agent_id: 'agent-42',
|
|
129
|
+
profile: { verbosity: :concise, tone: :casual },
|
|
130
|
+
source_agent_id: 'agent-42'
|
|
131
|
+
)
|
|
132
|
+
result = described_class.cached_mesh_profile(agent_id: 'agent-42')
|
|
133
|
+
expect(result).to be_a(Hash)
|
|
134
|
+
expect(result[:verbosity]).to eq(:concise)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
it 'returns nil for expired cache entries' do
|
|
138
|
+
described_class.store_mesh_profile(
|
|
139
|
+
agent_id: 'agent-42',
|
|
140
|
+
profile: { verbosity: :concise },
|
|
141
|
+
source_agent_id: 'agent-42'
|
|
142
|
+
)
|
|
143
|
+
# Expire the entry by manipulating the cache timestamp
|
|
144
|
+
cache = described_class.instance_variable_get(:@mesh_cache)
|
|
145
|
+
cache['agent-42'][:cached_at] = Time.now - 7200 if cache&.dig('agent-42')
|
|
146
|
+
result = described_class.cached_mesh_profile(agent_id: 'agent-42', ttl: 3600)
|
|
147
|
+
expect(result).to be_nil
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
describe '.for_agent' do
|
|
152
|
+
before { described_class.clear_mesh_cache }
|
|
153
|
+
|
|
154
|
+
it 'returns local profile when no mesh cache exists' do
|
|
155
|
+
result = described_class.for_agent(agent_id: 'agent-99')
|
|
156
|
+
expect(result[:source]).to eq(:local)
|
|
157
|
+
expect(result[:profile]).to be_a(Hash)
|
|
158
|
+
expect(result[:profile][:verbosity]).to eq(:normal)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
it 'returns cached mesh profile when available' do
|
|
162
|
+
described_class.store_mesh_profile(
|
|
163
|
+
agent_id: 'agent-42',
|
|
164
|
+
profile: { verbosity: :concise, tone: :formal },
|
|
165
|
+
source_agent_id: 'agent-42'
|
|
166
|
+
)
|
|
167
|
+
result = described_class.for_agent(agent_id: 'agent-42')
|
|
168
|
+
expect(result[:source]).to eq(:mesh_cache)
|
|
169
|
+
expect(result[:profile][:verbosity]).to eq(:concise)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
it 'falls back to local when mesh cache is expired' do
|
|
173
|
+
described_class.store_mesh_profile(
|
|
174
|
+
agent_id: 'agent-42',
|
|
175
|
+
profile: { verbosity: :concise },
|
|
176
|
+
source_agent_id: 'agent-42'
|
|
177
|
+
)
|
|
178
|
+
cache = described_class.instance_variable_get(:@mesh_cache)
|
|
179
|
+
cache['agent-42'][:cached_at] = Time.now - 7200
|
|
180
|
+
result = described_class.for_agent(agent_id: 'agent-42')
|
|
181
|
+
expect(result[:source]).to eq(:local)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
it 'includes compatibility when available' do
|
|
185
|
+
result = described_class.for_agent(agent_id: 'agent-99')
|
|
186
|
+
expect(result).to have_key(:compatibility)
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
108
190
|
describe '.preference_instructions' do
|
|
109
191
|
it 'generates natural language prompt instructions from profile' do
|
|
110
192
|
profile = {
|
|
@@ -65,6 +65,41 @@ RSpec.describe Legion::Extensions::Mesh::Runners::Preferences do
|
|
|
65
65
|
result = runner.handle_preference_query(requesting_agent_id: 'agent-1')
|
|
66
66
|
expect(result[:profile]).to have_key(:verbosity)
|
|
67
67
|
end
|
|
68
|
+
|
|
69
|
+
context 'with trust module available' do
|
|
70
|
+
let(:trust_runner) do
|
|
71
|
+
Module.new do
|
|
72
|
+
def get_trust(agent_id:, domain: :general, **)
|
|
73
|
+
case agent_id
|
|
74
|
+
when 'trusted-agent' then { found: true, trust: { composite: 0.8 } }
|
|
75
|
+
when 'untrusted-agent' then { found: true, trust: { composite: 0.1 } }
|
|
76
|
+
else { found: false, agent_id: agent_id, domain: domain }
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
before do
|
|
83
|
+
stub_const('Legion::Extensions::Agentic::Social::Trust::Runners::Trust', trust_runner)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it 'returns profile for trusted agents' do
|
|
87
|
+
result = runner.handle_preference_query(requesting_agent_id: 'trusted-agent')
|
|
88
|
+
expect(result[:success]).to be true
|
|
89
|
+
expect(result[:profile]).to be_a(Hash)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it 'refuses preferences for untrusted agents' do
|
|
93
|
+
result = runner.handle_preference_query(requesting_agent_id: 'untrusted-agent')
|
|
94
|
+
expect(result[:success]).to be false
|
|
95
|
+
expect(result[:reason]).to eq(:insufficient_trust)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
it 'returns profile for unknown agents (trust not found defaults to open)' do
|
|
99
|
+
result = runner.handle_preference_query(requesting_agent_id: 'unknown-agent')
|
|
100
|
+
expect(result[:success]).to be true
|
|
101
|
+
end
|
|
102
|
+
end
|
|
68
103
|
end
|
|
69
104
|
|
|
70
105
|
describe '#handle_preference_response' do
|
|
@@ -125,6 +160,22 @@ RSpec.describe Legion::Extensions::Mesh::Runners::Preferences do
|
|
|
125
160
|
)
|
|
126
161
|
expect(result[:resolved]).to be false
|
|
127
162
|
end
|
|
163
|
+
|
|
164
|
+
it 'passes responding_agent_id for mesh caching' do
|
|
165
|
+
pending_req = runner.send(:pending_requests)
|
|
166
|
+
pending_req.register(correlation_id: 'corr-dispatch', callback: ->(_p) {})
|
|
167
|
+
runner.dispatch_preference_message(
|
|
168
|
+
type: 'preference_response',
|
|
169
|
+
correlation_id: 'corr-dispatch',
|
|
170
|
+
profile: { verbosity: :terse },
|
|
171
|
+
responding_agent_id: 'agent-77'
|
|
172
|
+
)
|
|
173
|
+
cached = Legion::Extensions::Mesh::Helpers::PreferenceProfile.cached_mesh_profile(
|
|
174
|
+
agent_id: 'agent-77'
|
|
175
|
+
)
|
|
176
|
+
expect(cached).to be_a(Hash)
|
|
177
|
+
expect(cached[:verbosity]).to eq(:terse)
|
|
178
|
+
end
|
|
128
179
|
end
|
|
129
180
|
|
|
130
181
|
context 'with unknown type' do
|
|
@@ -143,6 +194,73 @@ RSpec.describe Legion::Extensions::Mesh::Runners::Preferences do
|
|
|
143
194
|
end
|
|
144
195
|
end
|
|
145
196
|
|
|
197
|
+
describe '#handle_preference_response with mesh caching' do
|
|
198
|
+
it 'caches the received profile' do
|
|
199
|
+
pending_req = runner.send(:pending_requests)
|
|
200
|
+
pending_req.register(
|
|
201
|
+
correlation_id: 'corr-cache',
|
|
202
|
+
callback: ->(profile) {}
|
|
203
|
+
)
|
|
204
|
+
runner.handle_preference_response(
|
|
205
|
+
correlation_id: 'corr-cache',
|
|
206
|
+
profile: { verbosity: :concise, tone: :casual },
|
|
207
|
+
responding_agent_id: 'agent-42'
|
|
208
|
+
)
|
|
209
|
+
cached = Legion::Extensions::Mesh::Helpers::PreferenceProfile.cached_mesh_profile(
|
|
210
|
+
agent_id: 'agent-42'
|
|
211
|
+
)
|
|
212
|
+
expect(cached).to be_a(Hash)
|
|
213
|
+
expect(cached[:verbosity]).to eq(:concise)
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
describe 'personality compatibility' do
|
|
218
|
+
context 'without personality module' do
|
|
219
|
+
it 'returns nil compatibility in for_agent' do
|
|
220
|
+
result = Legion::Extensions::Mesh::Helpers::PreferenceProfile.for_agent(agent_id: 'agent-99')
|
|
221
|
+
expect(result[:compatibility]).to be_nil
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
context 'with personality module available' do
|
|
226
|
+
let(:personality_runner) do
|
|
227
|
+
Module.new do
|
|
228
|
+
def personality_compatibility(**)
|
|
229
|
+
{ compatibility: 0.82, interpretation: :compatible }
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
private
|
|
233
|
+
|
|
234
|
+
def personality_store
|
|
235
|
+
@personality_store ||= Struct.new(:model).new(nil)
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
before do
|
|
241
|
+
stub_const(
|
|
242
|
+
'Legion::Extensions::Agentic::Self::Personality::Runners::Personality',
|
|
243
|
+
personality_runner
|
|
244
|
+
)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
it 'returns compatibility score when personality data exists' do
|
|
248
|
+
Legion::Extensions::Mesh::Helpers::PreferenceProfile.store_mesh_profile(
|
|
249
|
+
agent_id: 'agent-42',
|
|
250
|
+
profile: {
|
|
251
|
+
verbosity: :concise,
|
|
252
|
+
personality: { openness: 0.8, conscientiousness: 0.6 }
|
|
253
|
+
},
|
|
254
|
+
source_agent_id: 'agent-42'
|
|
255
|
+
)
|
|
256
|
+
result = Legion::Extensions::Mesh::Helpers::PreferenceProfile.for_agent(agent_id: 'agent-42')
|
|
257
|
+
expect(result[:compatibility]).to be_a(Hash)
|
|
258
|
+
expect(result[:compatibility][:score]).to eq(0.82)
|
|
259
|
+
expect(result[:compatibility][:interpretation]).to eq(:compatible)
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
146
264
|
describe '#expire_pending_requests' do
|
|
147
265
|
it 'cleans up expired entries and returns count' do
|
|
148
266
|
pending_req = runner.send(:pending_requests)
|