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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4d967d6f545da4301ebb7175d8e5ede497549f476f9a590b04b769cf61fa09e7
4
- data.tar.gz: 6d56d3fe6e686cb04e99bc5fd82962303e3a79b0c87447bc253193d917aad601
3
+ metadata.gz: 77260f7b11e2e6274661a661d12c0c09b97a854faf0366b58ac49fcd31485a63
4
+ data.tar.gz: b2164cc013bc8265a421dba4f2a35ae66808d871725b73db68c58344d22b3068
5
5
  SHA512:
6
- metadata.gz: 5d5472ede458093d44ddcb48effafdfdfacd60b75b3eda88500e849eb18d13b5ab2cff54b086321c088e511ee9f53bce4a40f4f8b5ca03d3dec0dfd75f8c91e0
7
- data.tar.gz: ae7bbda82bc87982a24032475919d21351c67613414fb397ed4e839a191ccc99a3d1446b168c0fc92603836ef43fde6793642f283eecb6f7ef902cc221cff1b3
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.9'
56
- spec.add_dependency 'legion-crypt', '>= 1.4.8'
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.12'
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)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Legion
4
- VERSION = '1.4.126'
4
+ VERSION = '1.4.129'
5
5
  end
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.126
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.9
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.9
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.8
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.8
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.12
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.12
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