lex-microsoft_teams 0.6.51 → 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 +10 -1
- 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 +16 -6
- 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 +14 -6
- data/lib/legion/extensions/microsoft_teams/actors/observed_chat_poller.rb +1 -1
- data/lib/legion/extensions/microsoft_teams/actors/presence_poller.rb +14 -5
- 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/throttle_aware.rb +185 -0
- 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 +91 -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 +89 -35
- data/lib/legion/extensions/microsoft_teams/version.rb +1 -1
- data/lib/legion/extensions/microsoft_teams.rb +3 -1
- metadata +2 -1
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'json'
|
|
4
|
+
require 'digest'
|
|
5
|
+
require 'legion/extensions/microsoft_teams/errors'
|
|
4
6
|
require 'legion/extensions/microsoft_teams/helpers/client'
|
|
5
7
|
require 'legion/extensions/microsoft_teams/helpers/graph_cache'
|
|
6
8
|
require 'legion/extensions/microsoft_teams/helpers/permission_guard'
|
|
@@ -36,37 +38,36 @@ module Legion
|
|
|
36
38
|
teams_result = ingest_teams_and_meetings(token: token)
|
|
37
39
|
|
|
38
40
|
{ self: self_result, people: people_result, conversations: conv_result, teams: teams_result }
|
|
41
|
+
rescue Errors::Throttled => e
|
|
42
|
+
# Once Graph throttles us, the shared circuit is open and every
|
|
43
|
+
# remaining stage would just raise Throttled(attempts: 0)
|
|
44
|
+
# instantly — a burst of identical errors that ingests nothing.
|
|
45
|
+
# Stop the fan-out at the first throttled stage and report it.
|
|
46
|
+
log.warn('[microsoft_teams][profile_ingest] throttled mid-ingest; ' \
|
|
47
|
+
'aborting remaining stages (retry_after=' \
|
|
48
|
+
"#{e.retry_after_known? ? format('%.1f', e.retry_after) : 'unknown'} path=#{e.request})")
|
|
49
|
+
{ throttled: true, retry_after: e.retry_after, request: e.request }
|
|
39
50
|
end
|
|
40
51
|
|
|
41
52
|
def ingest_self(token:, **)
|
|
42
53
|
conn = graph_connection(token: token)
|
|
43
54
|
profile = conn.get('me').body
|
|
44
55
|
|
|
45
|
-
memory_runner.store_trace(
|
|
46
|
-
type: :identity,
|
|
47
|
-
content_payload: ::JSON.dump(profile),
|
|
48
|
-
domain_tags: %w[teams self owner],
|
|
49
|
-
confidence: 1.0,
|
|
50
|
-
origin: :direct_experience
|
|
51
|
-
)
|
|
52
|
-
|
|
53
56
|
presence = begin
|
|
54
57
|
conn.get('me/presence').body
|
|
55
58
|
rescue StandardError => e
|
|
56
|
-
handle_exception(e, level: :
|
|
59
|
+
handle_exception(e, level: :warn, operation: 'ProfileIngest#ingest_self presence') if defined?(handle_exception)
|
|
57
60
|
{}
|
|
58
61
|
end
|
|
59
|
-
unless presence.empty?
|
|
60
|
-
memory_runner.store_trace(
|
|
61
|
-
type: :sensory,
|
|
62
|
-
content_payload: ::JSON.dump(presence),
|
|
63
|
-
domain_tags: %w[teams presence self],
|
|
64
|
-
confidence: 0.8,
|
|
65
|
-
origin: :direct_experience
|
|
66
|
-
)
|
|
67
|
-
end
|
|
68
62
|
|
|
69
63
|
{ profile: profile, presence: presence }
|
|
64
|
+
# rubocop:disable Legion/RescueLogging/NoCapture
|
|
65
|
+
# Re-raise unlogged: full_ingest logs the throttle once and aborts
|
|
66
|
+
# the fan-out. A throttle is a fleet signal, not a per-stage error;
|
|
67
|
+
# logging it here too would just duplicate the line per stage.
|
|
68
|
+
rescue Errors::Throttled
|
|
69
|
+
raise
|
|
70
|
+
# rubocop:enable Legion/RescueLogging/NoCapture
|
|
70
71
|
rescue StandardError => e
|
|
71
72
|
handle_exception(e, level: :error, operation: 'ProfileIngest#ingest_self')
|
|
72
73
|
{ error: e.message }
|
|
@@ -86,19 +87,14 @@ module Legion
|
|
|
86
87
|
people = (resp.body || {}).fetch('value', [])
|
|
87
88
|
people.sort_by! { |p| -(p.dig('scoredEmailAddresses', 0, 'relevanceScore') || 0) }
|
|
88
89
|
|
|
89
|
-
people.each do |person|
|
|
90
|
-
name = person['displayName'] || 'Unknown'
|
|
91
|
-
memory_runner.store_trace(
|
|
92
|
-
type: :semantic,
|
|
93
|
-
content_payload: ::JSON.dump(person.slice('displayName', 'jobTitle', 'department',
|
|
94
|
-
'officeLocation', 'scoredEmailAddresses')),
|
|
95
|
-
domain_tags: ['teams', 'peer', "peer:#{name}"],
|
|
96
|
-
confidence: 0.7,
|
|
97
|
-
origin: :direct_experience
|
|
98
|
-
)
|
|
99
|
-
end
|
|
100
|
-
|
|
101
90
|
{ people: people, count: people.length }
|
|
91
|
+
# rubocop:disable Legion/RescueLogging/NoCapture
|
|
92
|
+
# Re-raise unlogged: full_ingest logs the throttle once and aborts
|
|
93
|
+
# the fan-out. A throttle is a fleet signal, not a per-stage error;
|
|
94
|
+
# logging it here too would just duplicate the line per stage.
|
|
95
|
+
rescue Errors::Throttled
|
|
96
|
+
raise
|
|
97
|
+
# rubocop:enable Legion/RescueLogging/NoCapture
|
|
102
98
|
rescue StandardError => e
|
|
103
99
|
handle_exception(e, level: :error, operation: 'ProfileIngest#ingest_people')
|
|
104
100
|
{ error: e.message, skipped: false }
|
|
@@ -148,6 +144,13 @@ module Legion
|
|
|
148
144
|
end
|
|
149
145
|
|
|
150
146
|
{ ingested: ingested }
|
|
147
|
+
# rubocop:disable Legion/RescueLogging/NoCapture
|
|
148
|
+
# Re-raise unlogged: full_ingest logs the throttle once and aborts
|
|
149
|
+
# the fan-out. A throttle is a fleet signal, not a per-stage error;
|
|
150
|
+
# logging it here too would just duplicate the line per stage.
|
|
151
|
+
rescue Errors::Throttled
|
|
152
|
+
raise
|
|
153
|
+
# rubocop:enable Legion/RescueLogging/NoCapture
|
|
151
154
|
rescue StandardError => e
|
|
152
155
|
handle_exception(e, level: :error, operation: 'ProfileIngest#ingest_conversations')
|
|
153
156
|
{ error: e.message, ingested: ingested || 0 }
|
|
@@ -194,6 +197,13 @@ module Legion
|
|
|
194
197
|
end
|
|
195
198
|
|
|
196
199
|
{ teams: teams_count, meetings: meetings_count }
|
|
200
|
+
# rubocop:disable Legion/RescueLogging/NoCapture
|
|
201
|
+
# Re-raise unlogged: full_ingest logs the throttle once and aborts
|
|
202
|
+
# the fan-out. A throttle is a fleet signal, not a per-stage error;
|
|
203
|
+
# logging it here too would just duplicate the line per stage.
|
|
204
|
+
rescue Errors::Throttled
|
|
205
|
+
raise
|
|
206
|
+
# rubocop:enable Legion/RescueLogging/NoCapture
|
|
197
207
|
rescue StandardError => e
|
|
198
208
|
handle_exception(e, level: :error, operation: 'ProfileIngest#ingest_teams_and_meetings')
|
|
199
209
|
{ error: e.message }
|
|
@@ -210,13 +220,16 @@ module Legion
|
|
|
210
220
|
top_people: top_people, message_depth: message_depth)
|
|
211
221
|
end
|
|
212
222
|
|
|
223
|
+
CHAT_TYPE_PRIORITY = { 'oneOnOne' => 0, 'group' => 1, 'meeting' => 2 }.freeze
|
|
224
|
+
|
|
213
225
|
private
|
|
214
226
|
|
|
215
227
|
def build_chat_member_index(conn:, chats:)
|
|
216
228
|
index = {}
|
|
217
|
-
chats.
|
|
229
|
+
sorted = chats.sort_by { |c| CHAT_TYPE_PRIORITY[c['chatType']] || 99 }
|
|
230
|
+
sorted.each do |chat|
|
|
218
231
|
members = cached_graph_get(conn: conn, path: "chats/#{chat['id']}/members",
|
|
219
|
-
shared: true)
|
|
232
|
+
shared: true, ttl: members_cache_ttl)
|
|
220
233
|
.then { |body| (body || {}).fetch('value', []) }
|
|
221
234
|
members.each do |m|
|
|
222
235
|
email = m['email']&.downcase
|
|
@@ -224,24 +237,65 @@ module Legion
|
|
|
224
237
|
end
|
|
225
238
|
end
|
|
226
239
|
index
|
|
240
|
+
# rubocop:disable Legion/RescueLogging/NoCapture
|
|
241
|
+
# Re-raise unlogged: propagate to full_ingest so the per-chat
|
|
242
|
+
# fan-out stops on the first throttle; full_ingest logs it once.
|
|
243
|
+
rescue Errors::Throttled
|
|
244
|
+
raise
|
|
245
|
+
# rubocop:enable Legion/RescueLogging/NoCapture
|
|
227
246
|
rescue StandardError => e
|
|
228
247
|
handle_exception(e, level: :warn, operation: 'ProfileIngest#build_chat_member_index')
|
|
229
248
|
{}
|
|
230
249
|
end
|
|
231
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
|
+
|
|
232
261
|
def fetch_new_messages(conn:, chat_id:, depth: 50)
|
|
233
262
|
hwm = get_extended_hwm(chat_id: chat_id)
|
|
234
263
|
params = { '$top' => depth, '$orderby' => 'createdDateTime desc' }
|
|
235
|
-
params['$filter'] = "createdDateTime gt #{hwm[:last_message_at]}" if hwm&.dig(:last_message_at)
|
|
236
264
|
|
|
237
265
|
resp = conn.get("chats/#{chat_id}/messages", params)
|
|
238
|
-
|
|
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)
|
|
277
|
+
# rubocop:disable Legion/RescueLogging/NoCapture
|
|
278
|
+
# Re-raise unlogged: propagate to full_ingest so the per-chat
|
|
279
|
+
# fan-out stops on the first throttle; full_ingest logs it once.
|
|
280
|
+
rescue Errors::Throttled
|
|
281
|
+
raise
|
|
282
|
+
# rubocop:enable Legion/RescueLogging/NoCapture
|
|
239
283
|
rescue StandardError => e
|
|
240
284
|
handle_exception(e, level: :warn, operation: 'ProfileIngest#fetch_new_messages',
|
|
241
285
|
chat_id: chat_id)
|
|
286
|
+
@fetch_failures = (@fetch_failures || 0) + 1
|
|
242
287
|
[]
|
|
243
288
|
end
|
|
244
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
|
+
|
|
245
299
|
def extract_conversation(messages:, peer_name:)
|
|
246
300
|
return nil if messages.empty?
|
|
247
301
|
|
|
@@ -265,7 +319,7 @@ module Legion
|
|
|
265
319
|
llm_ask(message: "#{definition[:prompt]}\n\nConversation with #{peer_name}:\n#{text}")
|
|
266
320
|
end
|
|
267
321
|
rescue StandardError => e
|
|
268
|
-
handle_exception(e, level: :
|
|
322
|
+
handle_exception(e, level: :warn, operation: 'ProfileIngest#extract_conversation')
|
|
269
323
|
nil
|
|
270
324
|
end
|
|
271
325
|
|
|
@@ -4,6 +4,7 @@ require 'legion/extensions/identity/entra/helpers/token_manager'
|
|
|
4
4
|
require 'legion/extensions/microsoft_teams/version'
|
|
5
5
|
require 'legion/extensions/microsoft_teams/errors'
|
|
6
6
|
require 'legion/extensions/microsoft_teams/faraday/retry_after'
|
|
7
|
+
require 'legion/extensions/microsoft_teams/helpers/throttle_aware'
|
|
7
8
|
require 'legion/extensions/microsoft_teams/helpers/client'
|
|
8
9
|
require 'legion/extensions/microsoft_teams/runners/auth'
|
|
9
10
|
require 'legion/extensions/microsoft_teams/runners/teams'
|
|
@@ -106,7 +107,8 @@ module Legion
|
|
|
106
107
|
interval: 60
|
|
107
108
|
},
|
|
108
109
|
cache: {
|
|
109
|
-
graph_ttl:
|
|
110
|
+
graph_ttl: 300,
|
|
111
|
+
members_ttl: 86_400
|
|
110
112
|
},
|
|
111
113
|
client: {
|
|
112
114
|
throttle_circuit: {
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lex-microsoft_teams
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.6.
|
|
4
|
+
version: 0.6.53
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -218,6 +218,7 @@ files:
|
|
|
218
218
|
- lib/legion/extensions/microsoft_teams/helpers/prompt_resolver.rb
|
|
219
219
|
- lib/legion/extensions/microsoft_teams/helpers/session_manager.rb
|
|
220
220
|
- lib/legion/extensions/microsoft_teams/helpers/subscription_registry.rb
|
|
221
|
+
- lib/legion/extensions/microsoft_teams/helpers/throttle_aware.rb
|
|
221
222
|
- lib/legion/extensions/microsoft_teams/helpers/trace_retriever.rb
|
|
222
223
|
- lib/legion/extensions/microsoft_teams/helpers/transform_definitions.rb
|
|
223
224
|
- lib/legion/extensions/microsoft_teams/local_cache/extractor.rb
|