legionio 1.4.126 → 1.4.129
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 +19 -0
- data/legionio.gemspec +3 -3
- data/lib/legion/cli/chat/tool_registry.rb +3 -1
- data/lib/legion/cli/chat/tools/search_traces.rb +203 -0
- data/lib/legion/extensions/helpers/cache.rb +2 -12
- data/lib/legion/extensions/helpers/core.rb +3 -8
- data/lib/legion/version.rb +1 -1
- metadata +8 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 77260f7b11e2e6274661a661d12c0c09b97a854faf0366b58ac49fcd31485a63
|
|
4
|
+
data.tar.gz: b2164cc013bc8265a421dba4f2a35ae66808d871725b73db68c58344d22b3068
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6a04b0dbe4ccf7b78a5c2ea2423675ab29c41d1804d0c12527a878276c6d1b2febe511a15ee8c137ba114b3f318f9ed74b91ebeb11041983043e0d6f0f61f5ea
|
|
7
|
+
data.tar.gz: b4a04e1b34802584b2241dc31b105fae654043c881daa1bc264001548d9f0358a6131bac8dd4679654fac959c126bf24ac4bfe55b569679d05218b031cce2742
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Legion Changelog
|
|
2
2
|
|
|
3
|
+
## [1.4.129] - 2026-03-22
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- SearchTraces chat tool for querying cognitive memory traces (Teams messages, conversations, meetings, people)
|
|
7
|
+
- Keyword-ranked search with person, domain, and trace type filtering
|
|
8
|
+
- Structured output formatting with age, strength, and domain tag metadata
|
|
9
|
+
|
|
10
|
+
## [1.4.128] - 2026-03-22
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- `Extensions::Helpers::Cache` now delegates to `Legion::Cache::Helper` from legion-cache gem
|
|
14
|
+
- Require legion-cache >= 1.3.11 and legion-crypt >= 1.4.9
|
|
15
|
+
|
|
16
|
+
## [1.4.127] - 2026-03-22
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- `Extensions::Helpers::Core` now delegates `settings` to `Legion::Settings::Helper` from legion-settings gem
|
|
20
|
+
- Require legion-settings >= 1.3.14 for the new Helper module
|
|
21
|
+
|
|
3
22
|
## [1.4.126] - 2026-03-22
|
|
4
23
|
|
|
5
24
|
### Changed
|
data/legionio.gemspec
CHANGED
|
@@ -52,11 +52,11 @@ Gem::Specification.new do |spec|
|
|
|
52
52
|
spec.add_dependency 'thor', '>= 1.3'
|
|
53
53
|
spec.add_dependency 'tty-spinner', '~> 0.9'
|
|
54
54
|
|
|
55
|
-
spec.add_dependency 'legion-cache', '>= 1.3.
|
|
56
|
-
spec.add_dependency 'legion-crypt', '>= 1.4.
|
|
55
|
+
spec.add_dependency 'legion-cache', '>= 1.3.11'
|
|
56
|
+
spec.add_dependency 'legion-crypt', '>= 1.4.9'
|
|
57
57
|
spec.add_dependency 'legion-json', '>= 1.2.0'
|
|
58
58
|
spec.add_dependency 'legion-logging', '>= 1.3.2'
|
|
59
|
-
spec.add_dependency 'legion-settings', '>= 1.3.
|
|
59
|
+
spec.add_dependency 'legion-settings', '>= 1.3.14'
|
|
60
60
|
spec.add_dependency 'legion-transport', '>= 1.3.6'
|
|
61
61
|
|
|
62
62
|
spec.add_dependency 'legion-tty', '>= 0.4.30'
|
|
@@ -15,6 +15,7 @@ begin
|
|
|
15
15
|
require 'legion/cli/chat/tools/search_memory'
|
|
16
16
|
require 'legion/cli/chat/tools/web_search'
|
|
17
17
|
require 'legion/cli/chat/tools/spawn_agent'
|
|
18
|
+
require 'legion/cli/chat/tools/search_traces'
|
|
18
19
|
rescue LoadError => e
|
|
19
20
|
Legion::Logging.debug("ToolRegistry ruby_llm not available, chat tools will not be registered: #{e.message}") if defined?(Legion::Logging)
|
|
20
21
|
end
|
|
@@ -36,7 +37,8 @@ module Legion
|
|
|
36
37
|
Tools::SaveMemory,
|
|
37
38
|
Tools::SearchMemory,
|
|
38
39
|
Tools::WebSearch,
|
|
39
|
-
Tools::SpawnAgent
|
|
40
|
+
Tools::SpawnAgent,
|
|
41
|
+
Tools::SearchTraces
|
|
40
42
|
].freeze
|
|
41
43
|
else
|
|
42
44
|
[].freeze
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'ruby_llm'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'legion/cli/chat_command'
|
|
6
|
+
|
|
7
|
+
module Legion
|
|
8
|
+
module CLI
|
|
9
|
+
class Chat
|
|
10
|
+
module Tools
|
|
11
|
+
class SearchTraces < RubyLLM::Tool
|
|
12
|
+
description 'Search cognitive memory traces for information from Teams messages, conversations, ' \
|
|
13
|
+
'meetings, people, and other ingested data. Use this when the user asks about what ' \
|
|
14
|
+
'someone said, conversation topics, meeting details, or any previously observed context.'
|
|
15
|
+
param :query, type: 'string', desc: 'Natural language search query (e.g., "what did Bob say about deployment")'
|
|
16
|
+
param :person, type: 'string', desc: 'Filter by person name (matches peer:Name domain tags)', required: false
|
|
17
|
+
param :domain, type: 'string', desc: 'Filter by domain tag (e.g., "teams", "meeting", "conversation")', required: false
|
|
18
|
+
param :trace_type, type: 'string', desc: 'Filter by trace type: episodic, semantic, sensory, identity', required: false
|
|
19
|
+
param :limit, type: 'integer', desc: 'Max results to return (default: 20)', required: false
|
|
20
|
+
|
|
21
|
+
STRUCTURED_FIELDS = [
|
|
22
|
+
['Person', 'displayName', :displayName, 'peer', :peer],
|
|
23
|
+
['Summary', 'summary', :summary],
|
|
24
|
+
['Subject', 'subject', :subject],
|
|
25
|
+
['Team', 'team', :team],
|
|
26
|
+
['Job', 'jobTitle', :jobTitle]
|
|
27
|
+
].freeze
|
|
28
|
+
|
|
29
|
+
def execute(query:, person: nil, domain: nil, trace_type: nil, limit: nil)
|
|
30
|
+
return 'Memory trace system not available (lex-agentic-memory not loaded).' unless trace_store_available?
|
|
31
|
+
|
|
32
|
+
limit = (limit || 20).clamp(1, 50)
|
|
33
|
+
traces = collect_traces(person: person, domain: domain, trace_type: trace_type, limit: limit * 3)
|
|
34
|
+
return 'No memory traces found matching those filters.' if traces.empty?
|
|
35
|
+
|
|
36
|
+
ranked = rank_by_query(traces: traces, query: query)
|
|
37
|
+
results = ranked.first(limit)
|
|
38
|
+
return 'No traces matched your query.' if results.empty?
|
|
39
|
+
|
|
40
|
+
format_results(results)
|
|
41
|
+
rescue StandardError => e
|
|
42
|
+
Legion::Logging.warn("SearchTraces#execute failed: #{e.message}") if defined?(Legion::Logging)
|
|
43
|
+
"Error searching traces: #{e.message}"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def trace_store_available?
|
|
49
|
+
defined?(Legion::Extensions::Agentic::Memory::Trace) &&
|
|
50
|
+
Legion::Extensions::Agentic::Memory::Trace.respond_to?(:shared_store)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def store
|
|
54
|
+
Legion::Extensions::Agentic::Memory::Trace.shared_store
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def collect_traces(person:, domain:, trace_type:, limit:)
|
|
58
|
+
if person
|
|
59
|
+
tag = "peer:#{person}"
|
|
60
|
+
candidates = store.retrieve_by_domain(tag, min_strength: 0.01, limit: limit)
|
|
61
|
+
candidates += store.retrieve_by_domain('teams', min_strength: 0.01, limit: limit) if candidates.size < 5
|
|
62
|
+
return candidates.uniq { |t| t[:trace_id] }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
return store.retrieve_by_domain(domain, min_strength: 0.01, limit: limit) if domain
|
|
66
|
+
|
|
67
|
+
if trace_type
|
|
68
|
+
sym = trace_type.to_sym
|
|
69
|
+
return store.retrieve_by_type(sym, min_strength: 0.01, limit: limit)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
store.all_traces(min_strength: 0.01).sort_by { |t| -t[:strength] }.first(limit)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def rank_by_query(traces:, query:)
|
|
76
|
+
keywords = query.downcase.split(/\s+/).reject { |w| w.length < 3 }
|
|
77
|
+
return traces if keywords.empty?
|
|
78
|
+
|
|
79
|
+
scored = traces.filter_map do |trace|
|
|
80
|
+
text = extract_searchable_text(trace)
|
|
81
|
+
next nil if text.empty?
|
|
82
|
+
|
|
83
|
+
score = compute_score(text: text, keywords: keywords, trace: trace)
|
|
84
|
+
next nil if score.zero?
|
|
85
|
+
|
|
86
|
+
{ trace: trace, score: score }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
scored.sort_by { |s| -s[:score] }.map { |s| s[:trace] }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def extract_searchable_text(trace)
|
|
93
|
+
payload = trace[:content_payload] || trace[:content]
|
|
94
|
+
text = case payload
|
|
95
|
+
when String
|
|
96
|
+
begin
|
|
97
|
+
parsed = ::JSON.parse(payload)
|
|
98
|
+
flatten_to_text(parsed)
|
|
99
|
+
rescue ::JSON::ParserError
|
|
100
|
+
payload
|
|
101
|
+
end
|
|
102
|
+
when Hash
|
|
103
|
+
flatten_to_text(payload)
|
|
104
|
+
else
|
|
105
|
+
payload.to_s
|
|
106
|
+
end
|
|
107
|
+
text.downcase
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def flatten_to_text(obj)
|
|
111
|
+
case obj
|
|
112
|
+
when Hash
|
|
113
|
+
obj.values.map { |v| flatten_to_text(v) }.join(' ')
|
|
114
|
+
when Array
|
|
115
|
+
obj.map { |v| flatten_to_text(v) }.join(' ')
|
|
116
|
+
else
|
|
117
|
+
obj.to_s
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def compute_score(text:, keywords:, trace:)
|
|
122
|
+
keyword_hits = keywords.count { |kw| text.include?(kw) }
|
|
123
|
+
return 0.0 if keyword_hits.zero?
|
|
124
|
+
|
|
125
|
+
keyword_ratio = keyword_hits.to_f / keywords.size
|
|
126
|
+
strength_bonus = trace[:strength] || 0.0
|
|
127
|
+
recency_bonus = recency_score(trace[:created_at])
|
|
128
|
+
|
|
129
|
+
(keyword_ratio * 10.0) + (strength_bonus * 2.0) + (recency_bonus * 3.0)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def recency_score(created_at)
|
|
133
|
+
return 0.0 unless created_at.is_a?(Time)
|
|
134
|
+
|
|
135
|
+
age_hours = (Time.now.utc - created_at) / 3600.0
|
|
136
|
+
1.0 / (1.0 + (age_hours / 24.0))
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def format_results(traces)
|
|
140
|
+
parts = traces.map.with_index(1) do |trace, idx|
|
|
141
|
+
payload = trace[:content_payload] || trace[:content]
|
|
142
|
+
content = format_payload(payload)
|
|
143
|
+
tags = (trace[:domain_tags] || []).join(', ')
|
|
144
|
+
age = format_age(trace[:created_at])
|
|
145
|
+
|
|
146
|
+
"#{idx}. [#{trace[:trace_type]}] #{content}\n tags: #{tags} | strength: #{(trace[:strength] || 0).round(2)} | #{age}"
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
"Found #{traces.size} matching traces:\n\n#{parts.join("\n\n")}"
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def format_payload(payload)
|
|
153
|
+
data = parse_payload(payload)
|
|
154
|
+
return truncate(data, 300) if data.is_a?(String)
|
|
155
|
+
|
|
156
|
+
format_structured(data)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def parse_payload(payload)
|
|
160
|
+
case payload
|
|
161
|
+
when String
|
|
162
|
+
::JSON.parse(payload)
|
|
163
|
+
when Hash
|
|
164
|
+
payload
|
|
165
|
+
else
|
|
166
|
+
payload.to_s
|
|
167
|
+
end
|
|
168
|
+
rescue ::JSON::ParserError
|
|
169
|
+
payload
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def format_structured(data)
|
|
173
|
+
parts = STRUCTURED_FIELDS.filter_map do |label, *keys|
|
|
174
|
+
val = keys.lazy.filter_map { |k| data[k] }.first
|
|
175
|
+
"#{label}: #{val}" if val
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
return parts.join(' | ') unless parts.empty?
|
|
179
|
+
|
|
180
|
+
truncate(flatten_to_text(data), 300)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def truncate(text, max)
|
|
184
|
+
text.length > max ? "#{text[0..(max - 3)]}..." : text
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def format_age(created_at)
|
|
188
|
+
return 'age unknown' unless created_at.is_a?(Time)
|
|
189
|
+
|
|
190
|
+
seconds = Time.now.utc - created_at
|
|
191
|
+
if seconds < 3600
|
|
192
|
+
"#{(seconds / 60).to_i}m ago"
|
|
193
|
+
elsif seconds < 86_400
|
|
194
|
+
"#{(seconds / 3600).to_i}h ago"
|
|
195
|
+
else
|
|
196
|
+
"#{(seconds / 86_400).to_i}d ago"
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
@@ -1,24 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'legion/extensions/helpers/base'
|
|
4
|
+
require 'legion/cache/helper'
|
|
4
5
|
|
|
5
6
|
module Legion
|
|
6
7
|
module Extensions
|
|
7
8
|
module Helpers
|
|
8
9
|
module Cache
|
|
9
10
|
include Legion::Extensions::Helpers::Base
|
|
10
|
-
|
|
11
|
-
def cache_namespace
|
|
12
|
-
@cache_namespace ||= lex_name
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def cache_set(key, value, ttl: 60, **)
|
|
16
|
-
Legion::Cache.set(cache_namespace + key, value, ttl: ttl)
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def cache_get(key)
|
|
20
|
-
Legion::Cache.get(cache_namespace + key)
|
|
21
|
-
end
|
|
11
|
+
include Legion::Cache::Helper
|
|
22
12
|
end
|
|
23
13
|
end
|
|
24
14
|
end
|
|
@@ -1,19 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative 'base'
|
|
4
|
+
require 'legion/settings/helper'
|
|
5
|
+
|
|
4
6
|
module Legion
|
|
5
7
|
module Extensions
|
|
6
8
|
module Helpers
|
|
7
9
|
module Core
|
|
8
10
|
include Legion::Extensions::Helpers::Base
|
|
9
|
-
|
|
10
|
-
def settings
|
|
11
|
-
if Legion::Settings[:extensions].key?(lex_filename.to_sym)
|
|
12
|
-
Legion::Settings[:extensions][lex_filename.to_sym]
|
|
13
|
-
else
|
|
14
|
-
{ logger: { level: 'info', extended: false, internal: false } }
|
|
15
|
-
end
|
|
16
|
-
end
|
|
11
|
+
include Legion::Settings::Helper
|
|
17
12
|
|
|
18
13
|
# looks local, then in crypt, then settings, then cache, then env
|
|
19
14
|
def find_setting(name, **opts)
|
data/lib/legion/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: legionio
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.4.
|
|
4
|
+
version: 1.4.129
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -225,28 +225,28 @@ dependencies:
|
|
|
225
225
|
requirements:
|
|
226
226
|
- - ">="
|
|
227
227
|
- !ruby/object:Gem::Version
|
|
228
|
-
version: 1.3.
|
|
228
|
+
version: 1.3.11
|
|
229
229
|
type: :runtime
|
|
230
230
|
prerelease: false
|
|
231
231
|
version_requirements: !ruby/object:Gem::Requirement
|
|
232
232
|
requirements:
|
|
233
233
|
- - ">="
|
|
234
234
|
- !ruby/object:Gem::Version
|
|
235
|
-
version: 1.3.
|
|
235
|
+
version: 1.3.11
|
|
236
236
|
- !ruby/object:Gem::Dependency
|
|
237
237
|
name: legion-crypt
|
|
238
238
|
requirement: !ruby/object:Gem::Requirement
|
|
239
239
|
requirements:
|
|
240
240
|
- - ">="
|
|
241
241
|
- !ruby/object:Gem::Version
|
|
242
|
-
version: 1.4.
|
|
242
|
+
version: 1.4.9
|
|
243
243
|
type: :runtime
|
|
244
244
|
prerelease: false
|
|
245
245
|
version_requirements: !ruby/object:Gem::Requirement
|
|
246
246
|
requirements:
|
|
247
247
|
- - ">="
|
|
248
248
|
- !ruby/object:Gem::Version
|
|
249
|
-
version: 1.4.
|
|
249
|
+
version: 1.4.9
|
|
250
250
|
- !ruby/object:Gem::Dependency
|
|
251
251
|
name: legion-json
|
|
252
252
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -281,14 +281,14 @@ dependencies:
|
|
|
281
281
|
requirements:
|
|
282
282
|
- - ">="
|
|
283
283
|
- !ruby/object:Gem::Version
|
|
284
|
-
version: 1.3.
|
|
284
|
+
version: 1.3.14
|
|
285
285
|
type: :runtime
|
|
286
286
|
prerelease: false
|
|
287
287
|
version_requirements: !ruby/object:Gem::Requirement
|
|
288
288
|
requirements:
|
|
289
289
|
- - ">="
|
|
290
290
|
- !ruby/object:Gem::Version
|
|
291
|
-
version: 1.3.
|
|
291
|
+
version: 1.3.14
|
|
292
292
|
- !ruby/object:Gem::Dependency
|
|
293
293
|
name: legion-transport
|
|
294
294
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -474,6 +474,7 @@ files:
|
|
|
474
474
|
- lib/legion/cli/chat/tools/search_content.rb
|
|
475
475
|
- lib/legion/cli/chat/tools/search_files.rb
|
|
476
476
|
- lib/legion/cli/chat/tools/search_memory.rb
|
|
477
|
+
- lib/legion/cli/chat/tools/search_traces.rb
|
|
477
478
|
- lib/legion/cli/chat/tools/spawn_agent.rb
|
|
478
479
|
- lib/legion/cli/chat/tools/web_search.rb
|
|
479
480
|
- lib/legion/cli/chat/tools/write_file.rb
|