lex-microsoft_teams 0.6.52 → 0.6.53
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/.rubocop.yml +4 -0
- data/CHANGELOG.md +4 -0
- data/lib/legion/extensions/microsoft_teams/absorbers/channel.rb +3 -3
- data/lib/legion/extensions/microsoft_teams/absorbers/chat.rb +3 -3
- data/lib/legion/extensions/microsoft_teams/absorbers/meeting.rb +2 -2
- data/lib/legion/extensions/microsoft_teams/actors/absorb_channel.rb +2 -2
- data/lib/legion/extensions/microsoft_teams/actors/absorb_chat.rb +2 -2
- data/lib/legion/extensions/microsoft_teams/actors/absorb_meeting.rb +2 -2
- data/lib/legion/extensions/microsoft_teams/actors/api_ingest.rb +2 -2
- data/lib/legion/extensions/microsoft_teams/actors/cache_bulk_ingest.rb +1 -1
- data/lib/legion/extensions/microsoft_teams/actors/channel_poller.rb +1 -1
- data/lib/legion/extensions/microsoft_teams/actors/direct_chat_poller.rb +1 -1
- data/lib/legion/extensions/microsoft_teams/actors/incremental_sync.rb +1 -1
- data/lib/legion/extensions/microsoft_teams/actors/meeting_ingest.rb +1 -1
- data/lib/legion/extensions/microsoft_teams/actors/observed_chat_poller.rb +1 -1
- data/lib/legion/extensions/microsoft_teams/actors/presence_poller.rb +1 -1
- data/lib/legion/extensions/microsoft_teams/actors/profile_ingest.rb +2 -2
- data/lib/legion/extensions/microsoft_teams/faraday/retry_after.rb +1 -1
- data/lib/legion/extensions/microsoft_teams/helpers/client.rb +4 -4
- data/lib/legion/extensions/microsoft_teams/helpers/graph_cache.rb +3 -3
- data/lib/legion/extensions/microsoft_teams/helpers/graph_client.rb +1 -1
- data/lib/legion/extensions/microsoft_teams/helpers/high_water_mark.rb +1 -1
- data/lib/legion/extensions/microsoft_teams/helpers/prompt_resolver.rb +1 -1
- data/lib/legion/extensions/microsoft_teams/helpers/session_manager.rb +1 -1
- data/lib/legion/extensions/microsoft_teams/helpers/trace_retriever.rb +1 -1
- data/lib/legion/extensions/microsoft_teams/local_cache/sstable_reader.rb +1 -1
- data/lib/legion/extensions/microsoft_teams/runners/api_ingest.rb +80 -19
- data/lib/legion/extensions/microsoft_teams/runners/bot.rb +2 -2
- data/lib/legion/extensions/microsoft_teams/runners/cache_ingest.rb +2 -2
- data/lib/legion/extensions/microsoft_teams/runners/profile_ingest.rb +39 -35
- data/lib/legion/extensions/microsoft_teams/version.rb +1 -1
- data/lib/legion/extensions/microsoft_teams.rb +2 -1
- 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: 1c452da3646f36302d562fefd816a7b00621e95d819017c6125cc7c0fcb1ea74
|
|
4
|
+
data.tar.gz: c11fa2558cb00fa66d884360242f98a574b68d4ecda61451ba7e1a5de3b8067c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: db1531411cd4b67f36b644652e3549aae7b9f3360a987187f04a8bc39c9d436bb8dea21873f61edcbf85964b6bcf5889e68bd64bd786f1967f0426f5aede2d51
|
|
7
|
+
data.tar.gz: 6798cc2a718ac3ba43db14b3476abaeab0279d9747c90c8d47f6dfd185f86271787f48042ab22d0dcdfce48af5d7cdf505bb4bf0766601d0d2bfdd8f37bfeb7a
|
data/.rubocop.yml
CHANGED
|
@@ -16,6 +16,10 @@ Legion/Extension/EveryActorRequiresTime:
|
|
|
16
16
|
# RunnerReturnHash fires on private helper methods inside runner modules (false positives)
|
|
17
17
|
Legion/Extension/RunnerReturnHash:
|
|
18
18
|
Enabled: false
|
|
19
|
+
|
|
20
|
+
# This extension uses trace types (episodic, semantic, procedural) not LLM taxonomy types
|
|
21
|
+
Legion/Llm/TaxonomyEnum:
|
|
22
|
+
Enabled: false
|
|
19
23
|
|
|
20
24
|
Metrics/CyclomaticComplexity:
|
|
21
25
|
Max: 25
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.6.53] - 2026-07-15
|
|
4
|
+
### Fixed
|
|
5
|
+
- Fix throttle propagation, invalid OData filter, member index priority, stop trace spam from API fetches
|
|
6
|
+
|
|
3
7
|
## [0.6.52] - 2026-05-29
|
|
4
8
|
### Fixed
|
|
5
9
|
- **Actors now defer their next scheduled run on a Graph throttle** instead of re-firing on their fixed timer interval — resolves the 0.6.51 "Known follow-up". New `Helpers::ThrottleAware` mixin records a "suppress until" instant of `now + retry_after` when a Graph call raises `Errors::Throttled`; subsequent ticks short-circuit until that window elapses. Wired into `PresencePoller`, `MeetingIngest`, and `ApiIngest`. Falls back to a bounded 60s deferral when the server gave no usable `Retry-After`, and clamps any single deferral to 600s. Previously a poller on a 30–300s interval would walk straight back into the still-open shared circuit every interval, emitting a `[throttle_circuit] hard circuit` ERROR (and a duplicate `Errors::Throttled` WARN/ERROR) each tick while ingesting nothing.
|
|
@@ -57,7 +57,7 @@ module Legion
|
|
|
57
57
|
def graph_token
|
|
58
58
|
Legion::Extensions::Identity::Entra::Helpers::TokenManager.load_token(:delegated)
|
|
59
59
|
rescue StandardError => e
|
|
60
|
-
handle_exception(e, level: :
|
|
60
|
+
handle_exception(e, level: :warn, operation: 'Channel#graph_token')
|
|
61
61
|
nil
|
|
62
62
|
end
|
|
63
63
|
|
|
@@ -85,7 +85,7 @@ module Legion
|
|
|
85
85
|
|
|
86
86
|
{ team_id: team_id, channel_id: channel_id, message_id: message_id }
|
|
87
87
|
rescue StandardError => e
|
|
88
|
-
handle_exception(e, level: :
|
|
88
|
+
handle_exception(e, level: :warn, operation: 'Channel#extract_ids', url: url)
|
|
89
89
|
nil
|
|
90
90
|
end
|
|
91
91
|
|
|
@@ -202,7 +202,7 @@ module Legion
|
|
|
202
202
|
" ↳ [#{timestamp}] #{sender}: #{text}"
|
|
203
203
|
end
|
|
204
204
|
rescue StandardError => e
|
|
205
|
-
handle_exception(e, level: :
|
|
205
|
+
handle_exception(e, level: :warn, operation: 'Channel#fetch_reply_lines',
|
|
206
206
|
team_id: team_id, channel_id: channel_id, message_id: message_id)
|
|
207
207
|
[]
|
|
208
208
|
end
|
|
@@ -46,7 +46,7 @@ module Legion
|
|
|
46
46
|
def graph_token
|
|
47
47
|
Legion::Extensions::Identity::Entra::Helpers::TokenManager.load_token(:delegated)
|
|
48
48
|
rescue StandardError => e
|
|
49
|
-
handle_exception(e, level: :
|
|
49
|
+
handle_exception(e, level: :warn, operation: 'Chat#graph_token')
|
|
50
50
|
nil
|
|
51
51
|
end
|
|
52
52
|
|
|
@@ -60,7 +60,7 @@ module Legion
|
|
|
60
60
|
|
|
61
61
|
URI.decode_uri_component(match[1])
|
|
62
62
|
rescue StandardError => e
|
|
63
|
-
handle_exception(e, level: :
|
|
63
|
+
handle_exception(e, level: :warn, operation: 'Chat#extract_chat_id', url: url)
|
|
64
64
|
nil
|
|
65
65
|
end
|
|
66
66
|
|
|
@@ -150,7 +150,7 @@ module Legion
|
|
|
150
150
|
" ↳ [#{timestamp}] #{sender}: #{text}"
|
|
151
151
|
end
|
|
152
152
|
rescue StandardError => e
|
|
153
|
-
handle_exception(e, level: :
|
|
153
|
+
handle_exception(e, level: :warn, operation: 'Chat#fetch_reply_lines',
|
|
154
154
|
chat_id: chat_id, message_id: message_id)
|
|
155
155
|
[]
|
|
156
156
|
end
|
|
@@ -51,7 +51,7 @@ module Legion
|
|
|
51
51
|
def graph_token
|
|
52
52
|
Legion::Extensions::Identity::Entra::Helpers::TokenManager.load_token(:delegated)
|
|
53
53
|
rescue StandardError => e
|
|
54
|
-
handle_exception(e, level: :
|
|
54
|
+
handle_exception(e, level: :warn, operation: 'Meeting#graph_token')
|
|
55
55
|
nil
|
|
56
56
|
end
|
|
57
57
|
|
|
@@ -75,7 +75,7 @@ module Legion
|
|
|
75
75
|
match = uri.path.match(%r{/l/chat/(19:meeting_[^/]+)})
|
|
76
76
|
match&.[](1)
|
|
77
77
|
rescue URI::InvalidURIError => e
|
|
78
|
-
handle_exception(e, level: :
|
|
78
|
+
handle_exception(e, level: :warn, operation: 'Meeting#extract_meeting_thread_id', url: url)
|
|
79
79
|
nil
|
|
80
80
|
end
|
|
81
81
|
|
|
@@ -14,7 +14,7 @@ module Legion
|
|
|
14
14
|
defined?(Legion::Extensions::Absorbers::Base) &&
|
|
15
15
|
defined?(Legion::Extensions::MicrosoftTeams::Absorbers::Channel)
|
|
16
16
|
rescue StandardError => e
|
|
17
|
-
handle_exception(e, level: :
|
|
17
|
+
handle_exception(e, level: :warn, operation: 'AbsorbChannel#enabled?')
|
|
18
18
|
false
|
|
19
19
|
end
|
|
20
20
|
|
|
@@ -53,7 +53,7 @@ module Legion
|
|
|
53
53
|
|
|
54
54
|
data.transform_keys(&:to_sym)
|
|
55
55
|
rescue StandardError => e
|
|
56
|
-
handle_exception(e, level: :
|
|
56
|
+
handle_exception(e, level: :warn, operation: 'AbsorbChannel#parse_payload')
|
|
57
57
|
{}
|
|
58
58
|
end
|
|
59
59
|
end
|
|
@@ -14,7 +14,7 @@ module Legion
|
|
|
14
14
|
defined?(Legion::Extensions::Absorbers::Base) &&
|
|
15
15
|
defined?(Legion::Extensions::MicrosoftTeams::Absorbers::Chat)
|
|
16
16
|
rescue StandardError => e
|
|
17
|
-
handle_exception(e, level: :
|
|
17
|
+
handle_exception(e, level: :warn, operation: 'AbsorbChat#enabled?')
|
|
18
18
|
false
|
|
19
19
|
end
|
|
20
20
|
|
|
@@ -53,7 +53,7 @@ module Legion
|
|
|
53
53
|
|
|
54
54
|
data.transform_keys(&:to_sym)
|
|
55
55
|
rescue StandardError => e
|
|
56
|
-
handle_exception(e, level: :
|
|
56
|
+
handle_exception(e, level: :warn, operation: 'AbsorbChat#parse_payload')
|
|
57
57
|
{}
|
|
58
58
|
end
|
|
59
59
|
end
|
|
@@ -14,7 +14,7 @@ module Legion
|
|
|
14
14
|
defined?(Legion::Extensions::Absorbers::Base) &&
|
|
15
15
|
defined?(Legion::Extensions::MicrosoftTeams::Absorbers::Meeting)
|
|
16
16
|
rescue StandardError => e
|
|
17
|
-
handle_exception(e, level: :
|
|
17
|
+
handle_exception(e, level: :warn, operation: 'AbsorbMeeting#enabled?')
|
|
18
18
|
false
|
|
19
19
|
end
|
|
20
20
|
|
|
@@ -53,7 +53,7 @@ module Legion
|
|
|
53
53
|
|
|
54
54
|
data.transform_keys(&:to_sym)
|
|
55
55
|
rescue StandardError => e
|
|
56
|
-
handle_exception(e, level: :
|
|
56
|
+
handle_exception(e, level: :warn, operation: 'AbsorbMeeting#parse_payload')
|
|
57
57
|
{}
|
|
58
58
|
end
|
|
59
59
|
end
|
|
@@ -24,7 +24,7 @@ module Legion
|
|
|
24
24
|
base_delay = auth_validator.respond_to?(:delay) ? auth_validator.delay.to_f : 9.0
|
|
25
25
|
[base_delay + 5.0, 14].max
|
|
26
26
|
rescue StandardError => e
|
|
27
|
-
handle_exception(e, level: :
|
|
27
|
+
handle_exception(e, level: :warn, operation: 'ApiIngest#delay')
|
|
28
28
|
14
|
|
29
29
|
end
|
|
30
30
|
|
|
@@ -93,7 +93,7 @@ module Legion
|
|
|
93
93
|
|
|
94
94
|
Legion::Extensions::Coldstart::Helpers::Bootstrap.new.imprint_active?
|
|
95
95
|
rescue StandardError => e
|
|
96
|
-
handle_exception(e, level: :
|
|
96
|
+
handle_exception(e, level: :warn, operation: 'ApiIngest#imprint_active?')
|
|
97
97
|
false
|
|
98
98
|
end
|
|
99
99
|
end
|
|
@@ -41,7 +41,7 @@ module Legion
|
|
|
41
41
|
|
|
42
42
|
Legion::Extensions::Coldstart::Helpers::Bootstrap.new.imprint_active?
|
|
43
43
|
rescue StandardError => e
|
|
44
|
-
handle_exception(e, level: :
|
|
44
|
+
handle_exception(e, level: :warn, operation: 'CacheBulkIngest#imprint_active?')
|
|
45
45
|
false
|
|
46
46
|
end
|
|
47
47
|
end
|
|
@@ -34,7 +34,7 @@ module Legion
|
|
|
34
34
|
def enabled?
|
|
35
35
|
settings.dig(:channel_poller, :enabled)
|
|
36
36
|
rescue StandardError => e
|
|
37
|
-
handle_exception(e, level: :
|
|
37
|
+
handle_exception(e, level: :warn, operation: 'ChannelPoller#enabled?')
|
|
38
38
|
false
|
|
39
39
|
end
|
|
40
40
|
|
|
@@ -29,7 +29,7 @@ module Legion
|
|
|
29
29
|
defined?(Legion::Extensions::MicrosoftTeams::Runners::Bot) &&
|
|
30
30
|
Legion.const_defined?(:Transport, false)
|
|
31
31
|
rescue StandardError => e
|
|
32
|
-
handle_exception(e, level: :
|
|
32
|
+
handle_exception(e, level: :warn, operation: 'DirectChatPoller#enabled?')
|
|
33
33
|
false
|
|
34
34
|
end
|
|
35
35
|
|
|
@@ -22,7 +22,7 @@ module Legion
|
|
|
22
22
|
defined?(Legion::Extensions::Agentic::Memory::Trace::Runners::Traces) &&
|
|
23
23
|
token_available?
|
|
24
24
|
rescue StandardError => e
|
|
25
|
-
handle_exception(e, level: :
|
|
25
|
+
handle_exception(e, level: :warn, operation: 'IncrementalSync#enabled?')
|
|
26
26
|
false
|
|
27
27
|
end
|
|
28
28
|
|
|
@@ -28,7 +28,7 @@ module Legion
|
|
|
28
28
|
settings.dig(:meeting_ingest, :enabled) &&
|
|
29
29
|
Legion::Extensions::Identity::Entra::Helpers::TokenManager.respond_to?(:load_token)
|
|
30
30
|
rescue StandardError => e
|
|
31
|
-
handle_exception(e, level: :
|
|
31
|
+
handle_exception(e, level: :warn, operation: 'MeetingIngest#enabled?')
|
|
32
32
|
false
|
|
33
33
|
end
|
|
34
34
|
|
|
@@ -28,7 +28,7 @@ module Legion
|
|
|
28
28
|
defined?(Legion::Extensions::MicrosoftTeams::Runners::Bot) &&
|
|
29
29
|
Legion.const_defined?(:Transport, false)
|
|
30
30
|
rescue StandardError => e
|
|
31
|
-
handle_exception(e, level: :
|
|
31
|
+
handle_exception(e, level: :warn, operation: 'ObservedChatPoller#enabled?')
|
|
32
32
|
false
|
|
33
33
|
end
|
|
34
34
|
|
|
@@ -23,7 +23,7 @@ module Legion
|
|
|
23
23
|
settings.dig(:presence_poller, :enabled) &&
|
|
24
24
|
Legion::Extensions::Identity::Entra::Helpers::TokenManager.respond_to?(:load_token)
|
|
25
25
|
rescue StandardError => e
|
|
26
|
-
handle_exception(e, level: :
|
|
26
|
+
handle_exception(e, level: :warn, operation: 'PresencePoller#enabled?')
|
|
27
27
|
false
|
|
28
28
|
end
|
|
29
29
|
|
|
@@ -16,7 +16,7 @@ module Legion
|
|
|
16
16
|
base_delay = auth_validator.respond_to?(:delay) ? auth_validator.delay.to_f : 9.0
|
|
17
17
|
base_delay + 5.0
|
|
18
18
|
rescue StandardError => e
|
|
19
|
-
handle_exception(e, level: :
|
|
19
|
+
handle_exception(e, level: :warn, operation: 'ProfileIngest#delay')
|
|
20
20
|
14.0
|
|
21
21
|
end
|
|
22
22
|
|
|
@@ -25,7 +25,7 @@ module Legion
|
|
|
25
25
|
defined?(Legion::Extensions::Agentic::Memory::Trace::Runners::Traces) &&
|
|
26
26
|
token_available?
|
|
27
27
|
rescue StandardError => e
|
|
28
|
-
handle_exception(e, level: :
|
|
28
|
+
handle_exception(e, level: :warn, operation: 'ProfileIngest#enabled?')
|
|
29
29
|
false
|
|
30
30
|
end
|
|
31
31
|
|
|
@@ -12,7 +12,7 @@ module Legion
|
|
|
12
12
|
include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers, false) &&
|
|
13
13
|
Legion::Extensions::Helpers.const_defined?(:Lex, false)
|
|
14
14
|
|
|
15
|
-
def graph_connection(token: nil, api_url: 'https://graph.microsoft.com/v1.0', **
|
|
15
|
+
def graph_connection(token: nil, api_url: 'https://graph.microsoft.com/v1.0', **)
|
|
16
16
|
token ||= entra_delegated_token
|
|
17
17
|
::Faraday.new(url: api_url) do |conn|
|
|
18
18
|
conn.request :json
|
|
@@ -24,7 +24,7 @@ module Legion
|
|
|
24
24
|
end
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
-
def bot_connection(token: nil, service_url: 'https://smba.trafficmanager.net/teams/', **
|
|
27
|
+
def bot_connection(token: nil, service_url: 'https://smba.trafficmanager.net/teams/', **)
|
|
28
28
|
::Faraday.new(url: service_url) do |conn|
|
|
29
29
|
conn.request :json
|
|
30
30
|
conn.use Legion::Extensions::MicrosoftTeams::Faraday::ThrottleCircuit, **throttle_circuit_options
|
|
@@ -39,7 +39,7 @@ module Legion
|
|
|
39
39
|
user_id == 'me' ? 'me' : "users/#{user_id}"
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
-
def oauth_connection(tenant_id: 'common', **
|
|
42
|
+
def oauth_connection(tenant_id: 'common', **)
|
|
43
43
|
::Faraday.new(url: "https://login.microsoftonline.com/#{tenant_id}") do |conn|
|
|
44
44
|
conn.request :url_encoded
|
|
45
45
|
conn.response :json, content_type: /\bjson$/
|
|
@@ -51,7 +51,7 @@ module Legion
|
|
|
51
51
|
def entra_delegated_token
|
|
52
52
|
Legion::Extensions::Identity::Entra::Helpers::TokenManager.load_token(:delegated)
|
|
53
53
|
rescue StandardError => e
|
|
54
|
-
handle_exception(e, level: :
|
|
54
|
+
handle_exception(e, level: :warn, operation: 'Client#entra_delegated_token')
|
|
55
55
|
nil
|
|
56
56
|
end
|
|
57
57
|
|
|
@@ -21,7 +21,7 @@ module Legion
|
|
|
21
21
|
resp.body
|
|
22
22
|
end
|
|
23
23
|
rescue StandardError => e
|
|
24
|
-
handle_exception(e, level: :
|
|
24
|
+
handle_exception(e, level: :warn, operation: 'GraphCache#cached_graph_get', path: path)
|
|
25
25
|
conn.get(path, params).body
|
|
26
26
|
end
|
|
27
27
|
|
|
@@ -35,7 +35,7 @@ module Legion
|
|
|
35
35
|
key = graph_cache_key(path: path, params: params, shared: shared)
|
|
36
36
|
cache_delete(key)
|
|
37
37
|
rescue StandardError => e
|
|
38
|
-
handle_exception(e, level: :
|
|
38
|
+
handle_exception(e, level: :warn, operation: 'GraphCache#invalidate_graph_cache')
|
|
39
39
|
end
|
|
40
40
|
|
|
41
41
|
private
|
|
@@ -55,7 +55,7 @@ module Legion
|
|
|
55
55
|
|
|
56
56
|
Legion::Settings[:microsoft_teams] || {}
|
|
57
57
|
rescue StandardError => e
|
|
58
|
-
handle_exception(e, level: :
|
|
58
|
+
handle_exception(e, level: :warn, operation: 'GraphCache#teams_extension_settings')
|
|
59
59
|
{}
|
|
60
60
|
end
|
|
61
61
|
end
|
|
@@ -59,7 +59,7 @@ module Legion
|
|
|
59
59
|
|
|
60
60
|
raw.is_a?(Hash) ? raw : ::JSON.parse(raw, symbolize_names: true)
|
|
61
61
|
rescue StandardError => e
|
|
62
|
-
handle_exception(e, level: :
|
|
62
|
+
handle_exception(e, level: :warn, operation: 'HighWaterMark#get_extended_hwm',
|
|
63
63
|
chat_id: chat_id)
|
|
64
64
|
nil
|
|
65
65
|
end
|
|
@@ -55,7 +55,7 @@ module Legion
|
|
|
55
55
|
Legion::Extensions::Mesh::Helpers::PreferenceProfile.preference_instructions(profile: profile)
|
|
56
56
|
rescue StandardError => e
|
|
57
57
|
if defined?(handle_exception)
|
|
58
|
-
handle_exception(e, level: :
|
|
58
|
+
handle_exception(e, level: :warn, operation: 'PromptResolver#preference_instructions_for',
|
|
59
59
|
owner_id: owner_id)
|
|
60
60
|
end
|
|
61
61
|
nil
|
|
@@ -153,7 +153,7 @@ module Legion
|
|
|
153
153
|
profile_traces.map { |t| { type: t[:trace_type], content: t[:content_payload].to_s[0, 200] } }
|
|
154
154
|
rescue StandardError => e
|
|
155
155
|
if defined?(handle_exception)
|
|
156
|
-
handle_exception(e, level: :
|
|
156
|
+
handle_exception(e, level: :warn, operation: 'SessionManager#trace_seed_for',
|
|
157
157
|
owner_id: owner_id)
|
|
158
158
|
end
|
|
159
159
|
nil
|
|
@@ -131,7 +131,7 @@ module Legion
|
|
|
131
131
|
return unless defined?(log)
|
|
132
132
|
|
|
133
133
|
if defined?(handle_exception)
|
|
134
|
-
handle_exception(error, level: :
|
|
134
|
+
handle_exception(error, level: :warn, operation: "TraceRetriever##{method}")
|
|
135
135
|
else
|
|
136
136
|
log.debug("TraceRetriever##{method} failed: #{error.message}")
|
|
137
137
|
end
|
|
@@ -22,16 +22,12 @@ module Legion
|
|
|
22
22
|
|
|
23
23
|
definition :ingest_api, mcp_exposed: false
|
|
24
24
|
|
|
25
|
-
# Fetch top contacts via /me/people, then pull recent messages from
|
|
26
|
-
# their 1:1 chats. Stores each message as an individual memory trace
|
|
27
|
-
# (same format as CacheIngest) with dedup by content hash.
|
|
28
|
-
#
|
|
29
|
-
# Requires a delegated token with Chat.Read and People.Read scopes.
|
|
30
25
|
def ingest_api(token:, top_people: 15, message_depth: 50, skip_bots: true, imprint_active: false, **) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
|
|
31
26
|
log.debug("ApiIngest#ingest_api top_people=#{top_people} message_depth=#{message_depth}")
|
|
32
27
|
return error_result('lex-memory not loaded') unless memory_available?
|
|
33
28
|
return error_result('no token provided') unless token && !token.empty?
|
|
34
29
|
|
|
30
|
+
@fetch_failures = 0
|
|
35
31
|
restore_hwm_from_traces
|
|
36
32
|
|
|
37
33
|
people = fetch_top_people(token: token, top: top_people)
|
|
@@ -104,6 +100,7 @@ module Legion
|
|
|
104
100
|
|
|
105
101
|
{ result: { stored: stored, skipped: skipped, people_ingested: people_ingested,
|
|
106
102
|
people_found: people.length, chats_found: chats.length,
|
|
103
|
+
fetch_failures: @fetch_failures,
|
|
107
104
|
apollo: apollo_results } }
|
|
108
105
|
# rubocop:disable Legion/RescueLogging/NoCapture
|
|
109
106
|
# Re-raise unlogged: surface the typed throttle to the caller (the
|
|
@@ -117,13 +114,15 @@ module Legion
|
|
|
117
114
|
# rubocop:enable Legion/RescueLogging/NoCapture
|
|
118
115
|
rescue StandardError => e
|
|
119
116
|
handle_exception(e, level: :error, operation: 'ApiIngest#ingest_api')
|
|
120
|
-
{ result: { stored: stored || 0, skipped: skipped || 0,
|
|
117
|
+
{ result: { stored: stored || 0, skipped: skipped || 0,
|
|
118
|
+
fetch_failures: @fetch_failures || 0, error: e.message } }
|
|
121
119
|
end
|
|
122
120
|
|
|
123
121
|
include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers, false) &&
|
|
124
122
|
Legion::Extensions::Helpers.const_defined?(:Lex, false)
|
|
125
123
|
|
|
126
124
|
MAX_CHAT_PAGES = 10
|
|
125
|
+
CHAT_TYPE_PRIORITY = { 'oneOnOne' => 0, 'group' => 1, 'meeting' => 2 }.freeze
|
|
127
126
|
|
|
128
127
|
private
|
|
129
128
|
|
|
@@ -135,15 +134,24 @@ module Legion
|
|
|
135
134
|
resp = conn.get('me/people', { '$top' => top })
|
|
136
135
|
|
|
137
136
|
log.debug("ApiIngest: fetch_top_people status=#{resp.status} count=#{(resp.body || {}).fetch('value', []).size}")
|
|
138
|
-
|
|
139
|
-
|
|
137
|
+
unless (200..299).cover?(resp.status)
|
|
138
|
+
error_code = resp.body&.dig('error', 'code')
|
|
139
|
+
log.warn('[microsoft_teams][api_ingest] fetch_top_people non-2xx: ' \
|
|
140
|
+
"status=#{resp.status} error_code=#{error_code}")
|
|
141
|
+
record_denial('/me/people', resp.body&.dig('error', 'message') || 'Forbidden') if resp.status == 403
|
|
142
|
+
@fetch_failures = (@fetch_failures || 0) + 1
|
|
140
143
|
return []
|
|
141
144
|
end
|
|
142
145
|
|
|
143
146
|
people = (resp.body || {}).fetch('value', [])
|
|
144
147
|
people.sort_by { |p| -(p.dig('scoredEmailAddresses', 0, 'relevanceScore') || 0) }
|
|
148
|
+
# rubocop:disable Legion/RescueLogging/NoCapture
|
|
149
|
+
rescue Errors::Throttled
|
|
150
|
+
raise
|
|
151
|
+
# rubocop:enable Legion/RescueLogging/NoCapture
|
|
145
152
|
rescue StandardError => e
|
|
146
153
|
handle_exception(e, level: :warn, operation: 'ApiIngest#fetch_top_people')
|
|
154
|
+
@fetch_failures = (@fetch_failures || 0) + 1
|
|
147
155
|
[]
|
|
148
156
|
end
|
|
149
157
|
|
|
@@ -154,8 +162,17 @@ module Legion
|
|
|
154
162
|
params = { '$top' => 50 }
|
|
155
163
|
pages = 0
|
|
156
164
|
|
|
157
|
-
|
|
165
|
+
MAX_CHAT_PAGES.times do
|
|
158
166
|
resp = conn.get(url, params)
|
|
167
|
+
|
|
168
|
+
unless (200..299).cover?(resp.status)
|
|
169
|
+
error_code = resp.body&.dig('error', 'code')
|
|
170
|
+
log.warn('[microsoft_teams][api_ingest] fetch_one_on_one_chats non-2xx: ' \
|
|
171
|
+
"status=#{resp.status} error_code=#{error_code}")
|
|
172
|
+
@fetch_failures = (@fetch_failures || 0) + 1
|
|
173
|
+
break
|
|
174
|
+
end
|
|
175
|
+
|
|
159
176
|
body = resp.body || {}
|
|
160
177
|
chats = body.fetch('value', [])
|
|
161
178
|
all_chats.concat(chats)
|
|
@@ -163,7 +180,6 @@ module Legion
|
|
|
163
180
|
|
|
164
181
|
next_link = body['@odata.nextLink']
|
|
165
182
|
break unless next_link
|
|
166
|
-
break if pages >= MAX_CHAT_PAGES
|
|
167
183
|
|
|
168
184
|
url = next_link
|
|
169
185
|
params = {}
|
|
@@ -173,8 +189,13 @@ module Legion
|
|
|
173
189
|
filtered = all_chats.select { |c| allowed_types.include?(c['chatType']) }
|
|
174
190
|
log.info("ApiIngest: fetched #{all_chats.size} chats (#{pages} pages), #{filtered.size} eligible (1:1/group/meeting)")
|
|
175
191
|
filtered
|
|
192
|
+
# rubocop:disable Legion/RescueLogging/NoCapture
|
|
193
|
+
rescue Errors::Throttled
|
|
194
|
+
raise
|
|
195
|
+
# rubocop:enable Legion/RescueLogging/NoCapture
|
|
176
196
|
rescue StandardError => e
|
|
177
197
|
handle_exception(e, level: :warn, operation: 'ApiIngest#fetch_one_on_one_chats')
|
|
198
|
+
@fetch_failures = (@fetch_failures || 0) + 1
|
|
178
199
|
[]
|
|
179
200
|
end
|
|
180
201
|
|
|
@@ -183,9 +204,10 @@ module Legion
|
|
|
183
204
|
by_user_id = {}
|
|
184
205
|
by_name = {}
|
|
185
206
|
|
|
186
|
-
chats.
|
|
207
|
+
sorted = chats.sort_by { |c| CHAT_TYPE_PRIORITY[c['chatType']] || 99 }
|
|
208
|
+
sorted.each do |chat|
|
|
187
209
|
members = cached_graph_get(conn: conn, path: "chats/#{chat['id']}/members",
|
|
188
|
-
shared: true)
|
|
210
|
+
shared: true, ttl: members_cache_ttl)
|
|
189
211
|
.then { |body| (body || {}).fetch('value', []) }
|
|
190
212
|
members.each do |m|
|
|
191
213
|
email = m['email']&.downcase
|
|
@@ -201,8 +223,13 @@ module Legion
|
|
|
201
223
|
end
|
|
202
224
|
|
|
203
225
|
{ email: by_email, user_id: by_user_id, name: by_name }
|
|
226
|
+
# rubocop:disable Legion/RescueLogging/NoCapture
|
|
227
|
+
rescue Errors::Throttled
|
|
228
|
+
raise
|
|
229
|
+
# rubocop:enable Legion/RescueLogging/NoCapture
|
|
204
230
|
rescue StandardError => e
|
|
205
231
|
handle_exception(e, level: :warn, operation: 'ApiIngest#build_chat_member_index')
|
|
232
|
+
@fetch_failures = (@fetch_failures || 0) + 1
|
|
206
233
|
{ email: {}, user_id: {}, name: {} }
|
|
207
234
|
end
|
|
208
235
|
|
|
@@ -216,17 +243,41 @@ module Legion
|
|
|
216
243
|
chat_index[:name][display_name]
|
|
217
244
|
end
|
|
218
245
|
|
|
246
|
+
def hwm_client_side_cut(messages:, hwm:)
|
|
247
|
+
return messages unless hwm&.dig(:last_message_at)
|
|
248
|
+
|
|
249
|
+
cutoff = hwm[:last_message_at]
|
|
250
|
+
seen = Set.new
|
|
251
|
+
messages.take_while { |m| m['createdDateTime'] && m['createdDateTime'] > cutoff }
|
|
252
|
+
.select { |m| seen.add?(m['id'] || Digest::SHA256.hexdigest(m.dig('body', 'content').to_s)[0, 16]) }
|
|
253
|
+
end
|
|
254
|
+
|
|
219
255
|
def fetch_chat_messages(conn:, chat_id:, depth: 50)
|
|
220
256
|
hwm = get_extended_hwm(chat_id: chat_id)
|
|
221
257
|
params = { '$top' => depth, '$orderby' => 'createdDateTime desc' }
|
|
222
|
-
params['$filter'] = "createdDateTime gt #{hwm[:last_message_at]}" if hwm&.dig(:last_message_at)
|
|
223
258
|
|
|
224
259
|
resp = conn.get("chats/#{chat_id}/messages", params)
|
|
225
|
-
|
|
226
|
-
(
|
|
260
|
+
|
|
261
|
+
unless (200..299).cover?(resp.status)
|
|
262
|
+
error_code = resp.body&.dig('error', 'code')
|
|
263
|
+
log.warn('[microsoft_teams][api_ingest] fetch_chat_messages non-2xx: ' \
|
|
264
|
+
"chat_id=#{chat_id} status=#{resp.status} error_code=#{error_code}")
|
|
265
|
+
@fetch_failures = (@fetch_failures || 0) + 1
|
|
266
|
+
return []
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
messages = (resp.body || {}).fetch('value', [])
|
|
270
|
+
messages = hwm_client_side_cut(messages: messages, hwm: hwm)
|
|
271
|
+
log.debug("ApiIngest: fetch_messages chat=#{chat_id} count=#{messages.size}")
|
|
272
|
+
messages
|
|
273
|
+
# rubocop:disable Legion/RescueLogging/NoCapture
|
|
274
|
+
rescue Errors::Throttled
|
|
275
|
+
raise
|
|
276
|
+
# rubocop:enable Legion/RescueLogging/NoCapture
|
|
227
277
|
rescue StandardError => e
|
|
228
278
|
handle_exception(e, level: :warn, operation: 'ApiIngest#fetch_chat_messages',
|
|
229
279
|
chat_id: chat_id)
|
|
280
|
+
@fetch_failures = (@fetch_failures || 0) + 1
|
|
230
281
|
[]
|
|
231
282
|
end
|
|
232
283
|
|
|
@@ -295,7 +346,7 @@ module Legion
|
|
|
295
346
|
end
|
|
296
347
|
hashes
|
|
297
348
|
rescue StandardError => e
|
|
298
|
-
handle_exception(e, level: :
|
|
349
|
+
handle_exception(e, level: :warn, operation: 'ApiIngest#load_existing_hashes')
|
|
299
350
|
Set.new
|
|
300
351
|
end
|
|
301
352
|
|
|
@@ -324,12 +375,12 @@ module Legion
|
|
|
324
375
|
trace_ids.each_cons(2) do |id_a, id_b|
|
|
325
376
|
store.record_coactivation(id_a, id_b)
|
|
326
377
|
rescue StandardError => e
|
|
327
|
-
handle_exception(e, level: :
|
|
378
|
+
handle_exception(e, level: :warn, operation: 'ApiIngest#coactivate_thread_traces',
|
|
328
379
|
id_a: id_a, id_b: id_b)
|
|
329
380
|
end
|
|
330
381
|
end
|
|
331
382
|
rescue StandardError => e
|
|
332
|
-
handle_exception(e, level: :
|
|
383
|
+
handle_exception(e, level: :warn, operation: 'ApiIngest#coactivate_thread_traces')
|
|
333
384
|
end
|
|
334
385
|
|
|
335
386
|
def publish_to_apollo(person_texts)
|
|
@@ -388,7 +439,7 @@ module Legion
|
|
|
388
439
|
|
|
389
440
|
{ success: true, count: result[:entities].length }
|
|
390
441
|
rescue StandardError => e
|
|
391
|
-
handle_exception(e, level: :
|
|
442
|
+
handle_exception(e, level: :warn, operation: 'ApiIngest#extract_and_ingest_entities',
|
|
392
443
|
person_name: person_name)
|
|
393
444
|
{ success: false, count: 0 }
|
|
394
445
|
end
|
|
@@ -407,6 +458,16 @@ module Legion
|
|
|
407
458
|
@apollo_knowledge_runner ||= Object.new.extend(Legion::Extensions::Apollo::Runners::Knowledge)
|
|
408
459
|
end
|
|
409
460
|
|
|
461
|
+
def members_cache_ttl
|
|
462
|
+
return @members_cache_ttl if defined?(@members_cache_ttl)
|
|
463
|
+
|
|
464
|
+
@members_cache_ttl = if respond_to?(:settings, true) && settings.respond_to?(:dig)
|
|
465
|
+
settings.dig(:cache, :members_ttl) || 86_400
|
|
466
|
+
else
|
|
467
|
+
86_400
|
|
468
|
+
end
|
|
469
|
+
end
|
|
470
|
+
|
|
410
471
|
def error_result(message)
|
|
411
472
|
{ result: { stored: 0, skipped: 0, error: message } }
|
|
412
473
|
end
|
|
@@ -200,7 +200,7 @@ module Legion
|
|
|
200
200
|
|
|
201
201
|
retrieve_context(message: message, owner_id: owner_id, chat_id: chat_id)
|
|
202
202
|
rescue StandardError => e
|
|
203
|
-
handle_exception(e, level: :
|
|
203
|
+
handle_exception(e, level: :warn, operation: 'Bot#retrieve_trace_context') if defined?(handle_exception)
|
|
204
204
|
nil
|
|
205
205
|
end
|
|
206
206
|
|
|
@@ -231,7 +231,7 @@ module Legion
|
|
|
231
231
|
Legion::Settings.dig(:microsoft_teams, :bot, :observe, :enabled) == true
|
|
232
232
|
end
|
|
233
233
|
|
|
234
|
-
def notify_enabled?(**
|
|
234
|
+
def notify_enabled?(**)
|
|
235
235
|
return false unless defined?(Legion::Settings)
|
|
236
236
|
|
|
237
237
|
Legion::Settings.dig(:microsoft_teams, :bot, :observe, :notify) == true
|
|
@@ -123,12 +123,12 @@ module Legion
|
|
|
123
123
|
trace_ids.each_cons(2) do |id_a, id_b|
|
|
124
124
|
store.record_coactivation(id_a, id_b)
|
|
125
125
|
rescue StandardError => e
|
|
126
|
-
handle_exception(e, level: :
|
|
126
|
+
handle_exception(e, level: :warn, operation: 'CacheIngest#coactivate_thread_traces',
|
|
127
127
|
id_a: id_a, id_b: id_b)
|
|
128
128
|
end
|
|
129
129
|
end
|
|
130
130
|
rescue StandardError => e
|
|
131
|
-
handle_exception(e, level: :
|
|
131
|
+
handle_exception(e, level: :warn, operation: 'CacheIngest#coactivate_thread_traces')
|
|
132
132
|
end
|
|
133
133
|
end
|
|
134
134
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'json'
|
|
4
|
+
require 'digest'
|
|
4
5
|
require 'legion/extensions/microsoft_teams/errors'
|
|
5
6
|
require 'legion/extensions/microsoft_teams/helpers/client'
|
|
6
7
|
require 'legion/extensions/microsoft_teams/helpers/graph_cache'
|
|
@@ -52,29 +53,12 @@ module Legion
|
|
|
52
53
|
conn = graph_connection(token: token)
|
|
53
54
|
profile = conn.get('me').body
|
|
54
55
|
|
|
55
|
-
memory_runner.store_trace(
|
|
56
|
-
type: :identity,
|
|
57
|
-
content_payload: ::JSON.dump(profile),
|
|
58
|
-
domain_tags: %w[teams self owner],
|
|
59
|
-
confidence: 1.0,
|
|
60
|
-
origin: :direct_experience
|
|
61
|
-
)
|
|
62
|
-
|
|
63
56
|
presence = begin
|
|
64
57
|
conn.get('me/presence').body
|
|
65
58
|
rescue StandardError => e
|
|
66
|
-
handle_exception(e, level: :
|
|
59
|
+
handle_exception(e, level: :warn, operation: 'ProfileIngest#ingest_self presence') if defined?(handle_exception)
|
|
67
60
|
{}
|
|
68
61
|
end
|
|
69
|
-
unless presence.empty?
|
|
70
|
-
memory_runner.store_trace(
|
|
71
|
-
type: :sensory,
|
|
72
|
-
content_payload: ::JSON.dump(presence),
|
|
73
|
-
domain_tags: %w[teams presence self],
|
|
74
|
-
confidence: 0.8,
|
|
75
|
-
origin: :direct_experience
|
|
76
|
-
)
|
|
77
|
-
end
|
|
78
62
|
|
|
79
63
|
{ profile: profile, presence: presence }
|
|
80
64
|
# rubocop:disable Legion/RescueLogging/NoCapture
|
|
@@ -103,18 +87,6 @@ module Legion
|
|
|
103
87
|
people = (resp.body || {}).fetch('value', [])
|
|
104
88
|
people.sort_by! { |p| -(p.dig('scoredEmailAddresses', 0, 'relevanceScore') || 0) }
|
|
105
89
|
|
|
106
|
-
people.each do |person|
|
|
107
|
-
name = person['displayName'] || 'Unknown'
|
|
108
|
-
memory_runner.store_trace(
|
|
109
|
-
type: :semantic,
|
|
110
|
-
content_payload: ::JSON.dump(person.slice('displayName', 'jobTitle', 'department',
|
|
111
|
-
'officeLocation', 'scoredEmailAddresses')),
|
|
112
|
-
domain_tags: ['teams', 'peer', "peer:#{name}"],
|
|
113
|
-
confidence: 0.7,
|
|
114
|
-
origin: :direct_experience
|
|
115
|
-
)
|
|
116
|
-
end
|
|
117
|
-
|
|
118
90
|
{ people: people, count: people.length }
|
|
119
91
|
# rubocop:disable Legion/RescueLogging/NoCapture
|
|
120
92
|
# Re-raise unlogged: full_ingest logs the throttle once and aborts
|
|
@@ -248,13 +220,16 @@ module Legion
|
|
|
248
220
|
top_people: top_people, message_depth: message_depth)
|
|
249
221
|
end
|
|
250
222
|
|
|
223
|
+
CHAT_TYPE_PRIORITY = { 'oneOnOne' => 0, 'group' => 1, 'meeting' => 2 }.freeze
|
|
224
|
+
|
|
251
225
|
private
|
|
252
226
|
|
|
253
227
|
def build_chat_member_index(conn:, chats:)
|
|
254
228
|
index = {}
|
|
255
|
-
chats.
|
|
229
|
+
sorted = chats.sort_by { |c| CHAT_TYPE_PRIORITY[c['chatType']] || 99 }
|
|
230
|
+
sorted.each do |chat|
|
|
256
231
|
members = cached_graph_get(conn: conn, path: "chats/#{chat['id']}/members",
|
|
257
|
-
shared: true)
|
|
232
|
+
shared: true, ttl: members_cache_ttl)
|
|
258
233
|
.then { |body| (body || {}).fetch('value', []) }
|
|
259
234
|
members.each do |m|
|
|
260
235
|
email = m['email']&.downcase
|
|
@@ -273,13 +248,32 @@ module Legion
|
|
|
273
248
|
{}
|
|
274
249
|
end
|
|
275
250
|
|
|
251
|
+
def members_cache_ttl
|
|
252
|
+
return @members_cache_ttl if defined?(@members_cache_ttl)
|
|
253
|
+
|
|
254
|
+
@members_cache_ttl = if respond_to?(:settings, true) && settings.respond_to?(:dig)
|
|
255
|
+
settings.dig(:cache, :members_ttl) || 86_400
|
|
256
|
+
else
|
|
257
|
+
86_400
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
276
261
|
def fetch_new_messages(conn:, chat_id:, depth: 50)
|
|
277
262
|
hwm = get_extended_hwm(chat_id: chat_id)
|
|
278
263
|
params = { '$top' => depth, '$orderby' => 'createdDateTime desc' }
|
|
279
|
-
params['$filter'] = "createdDateTime gt #{hwm[:last_message_at]}" if hwm&.dig(:last_message_at)
|
|
280
264
|
|
|
281
265
|
resp = conn.get("chats/#{chat_id}/messages", params)
|
|
282
|
-
|
|
266
|
+
|
|
267
|
+
unless (200..299).cover?(resp.status)
|
|
268
|
+
error_code = resp.body&.dig('error', 'code')
|
|
269
|
+
log.warn('[microsoft_teams][profile_ingest] fetch_new_messages non-2xx: ' \
|
|
270
|
+
"chat_id=#{chat_id} status=#{resp.status} error_code=#{error_code}")
|
|
271
|
+
@fetch_failures = (@fetch_failures || 0) + 1
|
|
272
|
+
return []
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
messages = (resp.body || {}).fetch('value', [])
|
|
276
|
+
hwm_client_side_cut(messages: messages, hwm: hwm)
|
|
283
277
|
# rubocop:disable Legion/RescueLogging/NoCapture
|
|
284
278
|
# Re-raise unlogged: propagate to full_ingest so the per-chat
|
|
285
279
|
# fan-out stops on the first throttle; full_ingest logs it once.
|
|
@@ -289,9 +283,19 @@ module Legion
|
|
|
289
283
|
rescue StandardError => e
|
|
290
284
|
handle_exception(e, level: :warn, operation: 'ProfileIngest#fetch_new_messages',
|
|
291
285
|
chat_id: chat_id)
|
|
286
|
+
@fetch_failures = (@fetch_failures || 0) + 1
|
|
292
287
|
[]
|
|
293
288
|
end
|
|
294
289
|
|
|
290
|
+
def hwm_client_side_cut(messages:, hwm:)
|
|
291
|
+
return messages unless hwm&.dig(:last_message_at)
|
|
292
|
+
|
|
293
|
+
cutoff = hwm[:last_message_at]
|
|
294
|
+
seen = Set.new
|
|
295
|
+
messages.take_while { |m| m['createdDateTime'] && m['createdDateTime'] > cutoff }
|
|
296
|
+
.select { |m| seen.add?(m['id'] || Digest::SHA256.hexdigest(m.dig('body', 'content').to_s)[0, 16]) }
|
|
297
|
+
end
|
|
298
|
+
|
|
295
299
|
def extract_conversation(messages:, peer_name:)
|
|
296
300
|
return nil if messages.empty?
|
|
297
301
|
|
|
@@ -315,7 +319,7 @@ module Legion
|
|
|
315
319
|
llm_ask(message: "#{definition[:prompt]}\n\nConversation with #{peer_name}:\n#{text}")
|
|
316
320
|
end
|
|
317
321
|
rescue StandardError => e
|
|
318
|
-
handle_exception(e, level: :
|
|
322
|
+
handle_exception(e, level: :warn, operation: 'ProfileIngest#extract_conversation')
|
|
319
323
|
nil
|
|
320
324
|
end
|
|
321
325
|
|