lex-microsoft_teams 0.6.50 → 0.6.52

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.
@@ -0,0 +1,185 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'monitor'
4
+ require 'legion/extensions/microsoft_teams/errors'
5
+
6
+ module Legion
7
+ module Extensions
8
+ module MicrosoftTeams
9
+ module Helpers
10
+ # Mixin that lets an `Actors::Every`-style poller honour a Graph
11
+ # throttle by *deferring its own next run* instead of re-firing on
12
+ # its fixed timer interval.
13
+ #
14
+ # Background: `Faraday::RetryAfter` raises `Errors::Throttled`
15
+ # centrally when it exhausts retries (or when the advertised wait
16
+ # alone exceeds the retry budget), and `Faraday::ThrottleCircuit`
17
+ # opens a shared circuit so the whole fleet stops hammering a quota
18
+ # that Graph has already flagged. But the actors that *drive* those
19
+ # calls schedule themselves with a `Concurrent::TimerTask` whose
20
+ # `execution_interval` is fixed at boot. Catching `Throttled` in a
21
+ # `rescue` block stops the current tick, but the next tick still
22
+ # fires on the original cadence — so a poller on a 30–300s interval
23
+ # keeps walking straight back into an open circuit, emitting an
24
+ # ERROR every interval and contributing nothing but log noise (and,
25
+ # via the shared circuit, refreshing the block for cheaper callers).
26
+ #
27
+ # This mixin closes that gap. An actor wraps its Graph work in
28
+ # {#with_throttle_deferral}. On `Errors::Throttled` it records a
29
+ # "suppress until" instant of `now + retry_after` (falling back to a
30
+ # bounded default when the server gave no usable `Retry-After`).
31
+ # Subsequent ticks short-circuit via {#throttle_suppressed?} until
32
+ # that instant passes, so the actor effectively reschedules itself
33
+ # at `now + retry_after` rather than `now + interval` — exactly the
34
+ # behaviour the throttle integration spec and the 0.6.51 CHANGELOG
35
+ # flagged as the outstanding follow-up.
36
+ #
37
+ # State is per-actor-instance and guarded by a monitor because an
38
+ # actor's `manual` runs on a `Concurrent` thread-pool worker. The
39
+ # mixin reads the carried `retry_after` only when
40
+ # `retry_after_known?` is true; otherwise it applies
41
+ # {DEFAULT_DEFERRAL} so a throttle with a missing/garbage header
42
+ # still backs the poller off rather than letting it spin.
43
+ module ThrottleAware
44
+ # Fallback deferral (seconds) used when a `Throttled` carries no
45
+ # usable `retry_after` (header absent or unparseable). One minute
46
+ # matches Microsoft Graph's documented typical Retry-After and the
47
+ # circuit middleware's `DEFAULT_FALLBACK_TTL`.
48
+ DEFAULT_DEFERRAL = 60.0
49
+
50
+ # Hard ceiling (seconds) on a single deferral so a pathological
51
+ # advertised `Retry-After` can't park a poller for an unbounded
52
+ # time. Mirrors the circuit middleware's 600s cap in
53
+ # `ThrottleCircuit#set_hard_circuit`.
54
+ MAX_DEFERRAL = 600.0
55
+
56
+ # Run `block` unless the actor is currently deferring after a
57
+ # recent throttle. If a `Throttled` escapes the block, record the
58
+ # deferral window and swallow it (the throttle has already been
59
+ # logged at the middleware/circuit layer; re-raising would just
60
+ # trip the actor's generic `rescue StandardError` and double-log).
61
+ #
62
+ # @param now [Time] injectable clock for deterministic tests
63
+ # @return [Object, nil] the block's value, or nil when suppressed
64
+ # or when a throttle was caught
65
+ def with_throttle_deferral(now: Time.now)
66
+ if throttle_suppressed?(now: now)
67
+ log_throttle_skip(now: now)
68
+ return nil
69
+ end
70
+
71
+ yield
72
+ rescue Legion::Extensions::MicrosoftTeams::Errors::Throttled => e
73
+ defer_after_throttle(e, now: now)
74
+ nil
75
+ end
76
+
77
+ # @return [Boolean] true while the actor is inside a deferral
78
+ # window opened by a previous throttle.
79
+ def throttle_suppressed?(now: Time.now)
80
+ until_at = throttled_until
81
+ !until_at.nil? && now < until_at
82
+ end
83
+
84
+ # The instant before which the actor should not poll again, or nil
85
+ # if no deferral is active. Thread-safe read.
86
+ def throttled_until
87
+ throttle_monitor.synchronize { @throttled_until }
88
+ end
89
+
90
+ # Seconds remaining in the current deferral window (0.0 if none /
91
+ # elapsed). Useful for logging and for tests.
92
+ def throttle_remaining(now: Time.now)
93
+ until_at = throttled_until
94
+ return 0.0 if until_at.nil?
95
+
96
+ remaining = until_at - now
97
+ remaining.positive? ? remaining : 0.0
98
+ end
99
+
100
+ # Open (or extend) a deferral window of `seconds` from `now`. A
101
+ # later throttle never *shortens* an existing window — we keep the
102
+ # furthest-out instant so overlapping throttles compose safely.
103
+ def defer_for(seconds, now: Time.now)
104
+ window = clamp_deferral(seconds)
105
+ target = now + window
106
+ throttle_monitor.synchronize do
107
+ @throttled_until = if @throttled_until.nil? || target > @throttled_until
108
+ target
109
+ else
110
+ @throttled_until
111
+ end
112
+ end
113
+ window
114
+ end
115
+
116
+ # Clear any active deferral. Called implicitly is unnecessary —
117
+ # {#throttle_suppressed?} expires on its own once the clock passes
118
+ # `throttled_until` — but exposed for tests and explicit resets.
119
+ def reset_throttle_deferral
120
+ throttle_monitor.synchronize { @throttled_until = nil }
121
+ end
122
+
123
+ private
124
+
125
+ def defer_after_throttle(error, now: Time.now)
126
+ seconds = deferral_seconds_for(error)
127
+ window = defer_for(seconds, now: now)
128
+ log.warn(
129
+ "[microsoft_teams][throttle_defer] #{actor_label}: " \
130
+ "deferring next run #{format('%.1f', window)}s " \
131
+ "(retry_after=#{error.retry_after_known? ? format('%.1f', error.retry_after) : 'unknown'} " \
132
+ "path=#{error.request})"
133
+ )
134
+ end
135
+
136
+ # Short, log-friendly name for the including actor. Falls back to
137
+ # the full class string for anonymous classes (whose `name` is
138
+ # nil), so the log line never raises on `name.split`.
139
+ def actor_label
140
+ (self.class.name || self.class.to_s).split('::').last
141
+ end
142
+
143
+ # Prefer the server-advised wait; fall back to {DEFAULT_DEFERRAL}
144
+ # only when the header was absent or unparseable.
145
+ def deferral_seconds_for(error)
146
+ if error.respond_to?(:retry_after_known?) && error.retry_after_known?
147
+ error.retry_after
148
+ else
149
+ DEFAULT_DEFERRAL
150
+ end
151
+ end
152
+
153
+ def clamp_deferral(seconds)
154
+ value = begin
155
+ Float(seconds)
156
+ # rubocop:disable Legion/RescueLogging/NoCapture
157
+ # Pure coercion fallback for a non-numeric deferral; the value
158
+ # is bounded immediately below, so a bad input degrades to the
159
+ # default rather than warranting its own log line.
160
+ rescue ArgumentError, TypeError
161
+ DEFAULT_DEFERRAL
162
+ # rubocop:enable Legion/RescueLogging/NoCapture
163
+ end
164
+ value = DEFAULT_DEFERRAL if value <= 0
165
+ [value, MAX_DEFERRAL].min
166
+ end
167
+
168
+ def log_throttle_skip(now: Time.now)
169
+ log.debug(
170
+ "[microsoft_teams][throttle_defer] #{actor_label}: " \
171
+ "skipping run, #{format('%.1f', throttle_remaining(now: now))}s deferral remaining"
172
+ )
173
+ end
174
+
175
+ def throttle_monitor
176
+ # `||=` is safe here: the first tick of an actor runs before any
177
+ # concurrent re-entry (the `Every` base guards re-entry with its
178
+ # own AtomicBoolean), so the monitor is initialised single-threaded.
179
+ @throttle_monitor ||= Monitor.new
180
+ end
181
+ end
182
+ end
183
+ end
184
+ end
185
+ end
@@ -2,7 +2,9 @@
2
2
 
3
3
  require 'json'
4
4
  require 'digest'
5
+ require 'legion/extensions/microsoft_teams/errors'
5
6
  require 'legion/extensions/microsoft_teams/helpers/client'
7
+ require 'legion/extensions/microsoft_teams/helpers/graph_cache'
6
8
  require 'legion/extensions/microsoft_teams/helpers/permission_guard'
7
9
  require 'legion/extensions/microsoft_teams/helpers/high_water_mark'
8
10
 
@@ -15,6 +17,7 @@ module Legion
15
17
  include Helpers::Client
16
18
  include Helpers::PermissionGuard
17
19
  include Helpers::HighWaterMark
20
+ include Helpers::GraphCache
18
21
  extend self
19
22
 
20
23
  definition :ingest_api, mcp_exposed: false
@@ -41,6 +44,7 @@ module Legion
41
44
 
42
45
  existing_hashes = load_existing_hashes
43
46
  conn = graph_connection(token: token)
47
+ chat_index = build_chat_member_index(conn: conn, chats: chats)
44
48
  stored = 0
45
49
  skipped = 0
46
50
  people_ingested = 0
@@ -48,7 +52,7 @@ module Legion
48
52
  person_texts = Hash.new { |h, k| h[k] = [] }
49
53
 
50
54
  people.each do |person|
51
- chat = match_chat_to_person(chats: chats, person: person, conn: conn)
55
+ chat = find_chat_for_person_indexed(person: person, chat_index: chat_index)
52
56
  unless chat
53
57
  log.debug("ApiIngest: no chat match for #{person['displayName']} " \
54
58
  "(email=#{person.dig('scoredEmailAddresses', 0, 'address')}, id=#{person['id']})")
@@ -101,6 +105,16 @@ module Legion
101
105
  { result: { stored: stored, skipped: skipped, people_ingested: people_ingested,
102
106
  people_found: people.length, chats_found: chats.length,
103
107
  apollo: apollo_results } }
108
+ # rubocop:disable Legion/RescueLogging/NoCapture
109
+ # Re-raise unlogged: surface the typed throttle to the caller (the
110
+ # ApiIngest actor) so it can defer its next scheduled run by the
111
+ # advertised retry_after. The throttle is already logged at the
112
+ # middleware/circuit layer; folding it into an error result here
113
+ # would hide the one signal the actor needs to stop re-charging
114
+ # the shared Graph circuit on its fixed interval.
115
+ rescue Errors::Throttled
116
+ raise
117
+ # rubocop:enable Legion/RescueLogging/NoCapture
104
118
  rescue StandardError => e
105
119
  handle_exception(e, level: :error, operation: 'ApiIngest#ingest_api')
106
120
  { result: { stored: stored || 0, skipped: skipped || 0, error: e.message } }
@@ -164,33 +178,42 @@ module Legion
164
178
  []
165
179
  end
166
180
 
167
- def match_chat_to_person(chats:, person:, conn:)
168
- email = person.dig('scoredEmailAddresses', 0, 'address')&.downcase
169
- display_name = person['displayName']&.downcase
170
- user_id = person['id']
171
- return nil unless email || user_id || display_name
172
-
173
- chats.find do |chat|
174
- members_resp = conn.get("chats/#{chat['id']}/members")
175
- members = (members_resp.body || {}).fetch('value', [])
176
- members.any? do |m|
177
- match_member?(m, email: email, user_id: user_id, display_name: display_name)
181
+ def build_chat_member_index(conn:, chats:)
182
+ by_email = {}
183
+ by_user_id = {}
184
+ by_name = {}
185
+
186
+ chats.each do |chat|
187
+ members = cached_graph_get(conn: conn, path: "chats/#{chat['id']}/members",
188
+ shared: true)
189
+ .then { |body| (body || {}).fetch('value', []) }
190
+ members.each do |m|
191
+ email = m['email']&.downcase
192
+ alt_email = m.dig('additionalData', 'email')&.downcase
193
+ uid = m['userId']
194
+ name = m['displayName']&.downcase
195
+
196
+ by_email[email] ||= chat if email
197
+ by_email[alt_email] ||= chat if alt_email
198
+ by_user_id[uid] ||= chat if uid
199
+ by_name[name] ||= chat if name
178
200
  end
179
201
  end
202
+
203
+ { email: by_email, user_id: by_user_id, name: by_name }
180
204
  rescue StandardError => e
181
- handle_exception(e, level: :debug, operation: 'ApiIngest#match_chat_to_person')
182
- nil
205
+ handle_exception(e, level: :warn, operation: 'ApiIngest#build_chat_member_index')
206
+ { email: {}, user_id: {}, name: {} }
183
207
  end
184
208
 
185
- def match_member?(member, email:, user_id:, display_name:)
186
- return true if email && member['email']&.downcase == email
187
- return true if user_id && member['userId'] == user_id
188
- return true if email && member.dig('additionalData', 'email')&.downcase == email
189
-
190
- member_name = member['displayName']&.downcase
191
- return true if display_name && member_name && member_name == display_name
209
+ def find_chat_for_person_indexed(person:, chat_index:)
210
+ email = person.dig('scoredEmailAddresses', 0, 'address')&.downcase
211
+ user_id = person['id']
212
+ display_name = person['displayName']&.downcase
192
213
 
193
- false
214
+ chat_index[:email][email] ||
215
+ chat_index[:user_id][user_id] ||
216
+ chat_index[:name][display_name]
194
217
  end
195
218
 
196
219
  def fetch_chat_messages(conn:, chat_id:, depth: 50)
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'json'
4
+ require 'legion/extensions/microsoft_teams/errors'
4
5
  require 'legion/extensions/microsoft_teams/helpers/client'
6
+ require 'legion/extensions/microsoft_teams/helpers/graph_cache'
5
7
  require 'legion/extensions/microsoft_teams/helpers/permission_guard'
6
8
  require 'legion/extensions/microsoft_teams/helpers/high_water_mark'
7
9
  require 'legion/extensions/microsoft_teams/helpers/transform_definitions'
@@ -15,6 +17,7 @@ module Legion
15
17
  include Helpers::Client
16
18
  include Helpers::PermissionGuard
17
19
  include Helpers::HighWaterMark
20
+ include Helpers::GraphCache
18
21
  extend self
19
22
 
20
23
  definition :full_ingest, mcp_exposed: false
@@ -34,6 +37,15 @@ module Legion
34
37
  teams_result = ingest_teams_and_meetings(token: token)
35
38
 
36
39
  { self: self_result, people: people_result, conversations: conv_result, teams: teams_result }
40
+ rescue Errors::Throttled => e
41
+ # Once Graph throttles us, the shared circuit is open and every
42
+ # remaining stage would just raise Throttled(attempts: 0)
43
+ # instantly — a burst of identical errors that ingests nothing.
44
+ # Stop the fan-out at the first throttled stage and report it.
45
+ log.warn('[microsoft_teams][profile_ingest] throttled mid-ingest; ' \
46
+ 'aborting remaining stages (retry_after=' \
47
+ "#{e.retry_after_known? ? format('%.1f', e.retry_after) : 'unknown'} path=#{e.request})")
48
+ { throttled: true, retry_after: e.retry_after, request: e.request }
37
49
  end
38
50
 
39
51
  def ingest_self(token:, **)
@@ -65,6 +77,13 @@ module Legion
65
77
  end
66
78
 
67
79
  { profile: profile, presence: presence }
80
+ # rubocop:disable Legion/RescueLogging/NoCapture
81
+ # Re-raise unlogged: full_ingest logs the throttle once and aborts
82
+ # the fan-out. A throttle is a fleet signal, not a per-stage error;
83
+ # logging it here too would just duplicate the line per stage.
84
+ rescue Errors::Throttled
85
+ raise
86
+ # rubocop:enable Legion/RescueLogging/NoCapture
68
87
  rescue StandardError => e
69
88
  handle_exception(e, level: :error, operation: 'ProfileIngest#ingest_self')
70
89
  { error: e.message }
@@ -97,6 +116,13 @@ module Legion
97
116
  end
98
117
 
99
118
  { people: people, count: people.length }
119
+ # rubocop:disable Legion/RescueLogging/NoCapture
120
+ # Re-raise unlogged: full_ingest logs the throttle once and aborts
121
+ # the fan-out. A throttle is a fleet signal, not a per-stage error;
122
+ # logging it here too would just duplicate the line per stage.
123
+ rescue Errors::Throttled
124
+ raise
125
+ # rubocop:enable Legion/RescueLogging/NoCapture
100
126
  rescue StandardError => e
101
127
  handle_exception(e, level: :error, operation: 'ProfileIngest#ingest_people')
102
128
  { error: e.message, skipped: false }
@@ -106,15 +132,19 @@ module Legion
106
132
  return { ingested: 0 } if people.empty?
107
133
 
108
134
  conn = graph_connection(token: token)
109
- chats_resp = conn.get('me/chats', { '$top' => 50 })
110
- chats = (chats_resp.body || {}).fetch('value', [])
135
+ chats = cached_graph_get(conn: conn, path: 'me/chats',
136
+ params: { '$top' => 50 })
137
+ .then { |body| (body || {}).fetch('value', []) }
138
+ one_on_ones = chats.select { |c| c['chatType'] == 'oneOnOne' }
139
+
140
+ email_to_chat = build_chat_member_index(conn: conn, chats: one_on_ones)
111
141
  ingested = 0
112
142
 
113
143
  people.first(top_people).each do |person|
114
- email = person.dig('scoredEmailAddresses', 0, 'address')
144
+ email = person.dig('scoredEmailAddresses', 0, 'address')&.downcase
115
145
  next unless email
116
146
 
117
- chat = find_chat_for_person(chats: chats, email: email, conn: conn)
147
+ chat = email_to_chat[email]
118
148
  next unless chat
119
149
 
120
150
  messages = fetch_new_messages(conn: conn, chat_id: chat['id'], depth: message_depth)
@@ -142,6 +172,13 @@ module Legion
142
172
  end
143
173
 
144
174
  { ingested: ingested }
175
+ # rubocop:disable Legion/RescueLogging/NoCapture
176
+ # Re-raise unlogged: full_ingest logs the throttle once and aborts
177
+ # the fan-out. A throttle is a fleet signal, not a per-stage error;
178
+ # logging it here too would just duplicate the line per stage.
179
+ rescue Errors::Throttled
180
+ raise
181
+ # rubocop:enable Legion/RescueLogging/NoCapture
145
182
  rescue StandardError => e
146
183
  handle_exception(e, level: :error, operation: 'ProfileIngest#ingest_conversations')
147
184
  { error: e.message, ingested: ingested || 0 }
@@ -188,6 +225,13 @@ module Legion
188
225
  end
189
226
 
190
227
  { teams: teams_count, meetings: meetings_count }
228
+ # rubocop:disable Legion/RescueLogging/NoCapture
229
+ # Re-raise unlogged: full_ingest logs the throttle once and aborts
230
+ # the fan-out. A throttle is a fleet signal, not a per-stage error;
231
+ # logging it here too would just duplicate the line per stage.
232
+ rescue Errors::Throttled
233
+ raise
234
+ # rubocop:enable Legion/RescueLogging/NoCapture
191
235
  rescue StandardError => e
192
236
  handle_exception(e, level: :error, operation: 'ProfileIngest#ingest_teams_and_meetings')
193
237
  { error: e.message }
@@ -206,15 +250,27 @@ module Legion
206
250
 
207
251
  private
208
252
 
209
- def find_chat_for_person(chats:, email:, conn:)
210
- chats.select { |c| c['chatType'] == 'oneOnOne' }.find do |chat|
211
- members_resp = conn.get("chats/#{chat['id']}/members")
212
- members = (members_resp.body || {}).fetch('value', [])
213
- members.any? { |m| m['email']&.downcase == email.downcase }
253
+ def build_chat_member_index(conn:, chats:)
254
+ index = {}
255
+ chats.each do |chat|
256
+ members = cached_graph_get(conn: conn, path: "chats/#{chat['id']}/members",
257
+ shared: true)
258
+ .then { |body| (body || {}).fetch('value', []) }
259
+ members.each do |m|
260
+ email = m['email']&.downcase
261
+ index[email] = chat if email && !index.key?(email)
262
+ end
214
263
  end
264
+ index
265
+ # rubocop:disable Legion/RescueLogging/NoCapture
266
+ # Re-raise unlogged: propagate to full_ingest so the per-chat
267
+ # fan-out stops on the first throttle; full_ingest logs it once.
268
+ rescue Errors::Throttled
269
+ raise
270
+ # rubocop:enable Legion/RescueLogging/NoCapture
215
271
  rescue StandardError => e
216
- handle_exception(e, level: :debug, operation: 'ProfileIngest#find_chat_for_person')
217
- nil
272
+ handle_exception(e, level: :warn, operation: 'ProfileIngest#build_chat_member_index')
273
+ {}
218
274
  end
219
275
 
220
276
  def fetch_new_messages(conn:, chat_id:, depth: 50)
@@ -224,6 +280,12 @@ module Legion
224
280
 
225
281
  resp = conn.get("chats/#{chat_id}/messages", params)
226
282
  (resp.body || {}).fetch('value', [])
283
+ # rubocop:disable Legion/RescueLogging/NoCapture
284
+ # Re-raise unlogged: propagate to full_ingest so the per-chat
285
+ # fan-out stops on the first throttle; full_ingest logs it once.
286
+ rescue Errors::Throttled
287
+ raise
288
+ # rubocop:enable Legion/RescueLogging/NoCapture
227
289
  rescue StandardError => e
228
290
  handle_exception(e, level: :warn, operation: 'ProfileIngest#fetch_new_messages',
229
291
  chat_id: chat_id)
@@ -3,7 +3,7 @@
3
3
  module Legion
4
4
  module Extensions
5
5
  module MicrosoftTeams
6
- VERSION = '0.6.50'
6
+ VERSION = '0.6.52'
7
7
  end
8
8
  end
9
9
  end
@@ -2,6 +2,9 @@
2
2
 
3
3
  require 'legion/extensions/identity/entra/helpers/token_manager'
4
4
  require 'legion/extensions/microsoft_teams/version'
5
+ require 'legion/extensions/microsoft_teams/errors'
6
+ require 'legion/extensions/microsoft_teams/faraday/retry_after'
7
+ require 'legion/extensions/microsoft_teams/helpers/throttle_aware'
5
8
  require 'legion/extensions/microsoft_teams/helpers/client'
6
9
  require 'legion/extensions/microsoft_teams/runners/auth'
7
10
  require 'legion/extensions/microsoft_teams/runners/teams'
@@ -29,6 +32,7 @@ require 'legion/extensions/microsoft_teams/runners/app_installations'
29
32
  require 'legion/extensions/microsoft_teams/runners/files'
30
33
 
31
34
  # Helpers (bot)
35
+ require 'legion/extensions/microsoft_teams/helpers/graph_cache'
32
36
  require 'legion/extensions/microsoft_teams/helpers/high_water_mark'
33
37
  require 'legion/extensions/microsoft_teams/helpers/prompt_resolver'
34
38
  require 'legion/extensions/microsoft_teams/helpers/trace_retriever'
@@ -60,6 +64,62 @@ module Legion
60
64
  module MicrosoftTeams
61
65
  extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core, false
62
66
 
67
+ def self.default_settings # rubocop:disable Metrics/MethodLength
68
+ {
69
+ api_ingest: {
70
+ enabled: true,
71
+ interval: 3600,
72
+ top_people: 15,
73
+ message_depth: 50,
74
+ skip_bots: true
75
+ },
76
+ incremental_sync: {
77
+ enabled: true,
78
+ interval: 900,
79
+ top_people: 10,
80
+ message_depth: 50
81
+ },
82
+ profile_ingest: {
83
+ enabled: true,
84
+ top_people: 10,
85
+ message_depth: 50
86
+ },
87
+ presence_poller: {
88
+ enabled: false,
89
+ interval: 300
90
+ },
91
+ meeting_ingest: {
92
+ enabled: true,
93
+ interval: 900
94
+ },
95
+ channel_poller: {
96
+ enabled: false,
97
+ interval: 120,
98
+ max_teams: 10,
99
+ max_channels_per_team: 5
100
+ },
101
+ direct_chat_poller: {
102
+ enabled: false,
103
+ interval: 30
104
+ },
105
+ observed_chat_poller: {
106
+ enabled: false,
107
+ interval: 60
108
+ },
109
+ cache: {
110
+ graph_ttl: 300
111
+ },
112
+ client: {
113
+ throttle_circuit: {
114
+ soft_percentage: 0.8,
115
+ soft_ttl: 60,
116
+ fallback_ttl: 60,
117
+ insights_ttl: 600
118
+ }
119
+ }
120
+ }
121
+ end
122
+
63
123
  def self.trigger_words
64
124
  %w[teams microsoft_teams microsoftteams microsoft-teams msteams ms-teams]
65
125
  end
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.50
4
+ version: 0.6.52
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -207,13 +207,18 @@ files:
207
207
  - lib/legion/extensions/microsoft_teams/actors/presence_poller.rb
208
208
  - lib/legion/extensions/microsoft_teams/actors/profile_ingest.rb
209
209
  - lib/legion/extensions/microsoft_teams/client.rb
210
+ - lib/legion/extensions/microsoft_teams/errors.rb
211
+ - lib/legion/extensions/microsoft_teams/faraday/retry_after.rb
212
+ - lib/legion/extensions/microsoft_teams/faraday/throttle_circuit.rb
210
213
  - lib/legion/extensions/microsoft_teams/helpers/client.rb
214
+ - lib/legion/extensions/microsoft_teams/helpers/graph_cache.rb
211
215
  - lib/legion/extensions/microsoft_teams/helpers/graph_client.rb
212
216
  - lib/legion/extensions/microsoft_teams/helpers/high_water_mark.rb
213
217
  - lib/legion/extensions/microsoft_teams/helpers/permission_guard.rb
214
218
  - lib/legion/extensions/microsoft_teams/helpers/prompt_resolver.rb
215
219
  - lib/legion/extensions/microsoft_teams/helpers/session_manager.rb
216
220
  - lib/legion/extensions/microsoft_teams/helpers/subscription_registry.rb
221
+ - lib/legion/extensions/microsoft_teams/helpers/throttle_aware.rb
217
222
  - lib/legion/extensions/microsoft_teams/helpers/trace_retriever.rb
218
223
  - lib/legion/extensions/microsoft_teams/helpers/transform_definitions.rb
219
224
  - lib/legion/extensions/microsoft_teams/local_cache/extractor.rb