lex-microsoft_teams 0.6.45 → 0.6.47

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.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/CHANGELOG.md +2 -0
  4. data/CLAUDE.md +29 -266
  5. data/lex-microsoft_teams.gemspec +1 -0
  6. data/lib/legion/extensions/microsoft_teams/absorbers/channel.rb +29 -17
  7. data/lib/legion/extensions/microsoft_teams/absorbers/chat.rb +20 -14
  8. data/lib/legion/extensions/microsoft_teams/absorbers/meeting.rb +21 -14
  9. data/lib/legion/extensions/microsoft_teams/actors/absorb_channel.rb +7 -4
  10. data/lib/legion/extensions/microsoft_teams/actors/absorb_chat.rb +7 -4
  11. data/lib/legion/extensions/microsoft_teams/actors/absorb_meeting.rb +7 -4
  12. data/lib/legion/extensions/microsoft_teams/actors/api_ingest.rb +13 -15
  13. data/lib/legion/extensions/microsoft_teams/actors/cache_bulk_ingest.rb +3 -3
  14. data/lib/legion/extensions/microsoft_teams/actors/cache_sync.rb +2 -1
  15. data/lib/legion/extensions/microsoft_teams/actors/channel_poller.rb +25 -16
  16. data/lib/legion/extensions/microsoft_teams/actors/direct_chat_poller.rb +16 -10
  17. data/lib/legion/extensions/microsoft_teams/actors/incremental_sync.rb +8 -8
  18. data/lib/legion/extensions/microsoft_teams/actors/meeting_ingest.rb +30 -22
  19. data/lib/legion/extensions/microsoft_teams/actors/observed_chat_poller.rb +14 -8
  20. data/lib/legion/extensions/microsoft_teams/actors/presence_poller.rb +14 -8
  21. data/lib/legion/extensions/microsoft_teams/actors/profile_ingest.rb +13 -16
  22. data/lib/legion/extensions/microsoft_teams/helpers/client.rb +10 -4
  23. data/lib/legion/extensions/microsoft_teams/helpers/high_water_mark.rb +3 -2
  24. data/lib/legion/extensions/microsoft_teams/helpers/prompt_resolver.rb +4 -1
  25. data/lib/legion/extensions/microsoft_teams/helpers/session_manager.rb +8 -2
  26. data/lib/legion/extensions/microsoft_teams/helpers/subscription_registry.rb +5 -3
  27. data/lib/legion/extensions/microsoft_teams/helpers/trace_retriever.rb +6 -1
  28. data/lib/legion/extensions/microsoft_teams/local_cache/extractor.rb +1 -1
  29. data/lib/legion/extensions/microsoft_teams/local_cache/sstable_reader.rb +2 -1
  30. data/lib/legion/extensions/microsoft_teams/runners/activities.rb +43 -0
  31. data/lib/legion/extensions/microsoft_teams/runners/ai_insights.rb +62 -0
  32. data/lib/legion/extensions/microsoft_teams/runners/api_ingest.rb +20 -14
  33. data/lib/legion/extensions/microsoft_teams/runners/app_installations.rb +86 -0
  34. data/lib/legion/extensions/microsoft_teams/runners/auth.rb +4 -107
  35. data/lib/legion/extensions/microsoft_teams/runners/bot.rb +20 -12
  36. data/lib/legion/extensions/microsoft_teams/runners/cache_ingest.rb +9 -5
  37. data/lib/legion/extensions/microsoft_teams/runners/call_events.rb +72 -0
  38. data/lib/legion/extensions/microsoft_teams/runners/channel_messages.rb +85 -0
  39. data/lib/legion/extensions/microsoft_teams/runners/channels.rb +69 -0
  40. data/lib/legion/extensions/microsoft_teams/runners/chats.rb +57 -0
  41. data/lib/legion/extensions/microsoft_teams/runners/files.rb +77 -0
  42. data/lib/legion/extensions/microsoft_teams/runners/local_cache.rb +4 -0
  43. data/lib/legion/extensions/microsoft_teams/runners/loop.rb +6 -0
  44. data/lib/legion/extensions/microsoft_teams/runners/meeting_artifacts.rb +54 -0
  45. data/lib/legion/extensions/microsoft_teams/runners/meetings.rb +92 -0
  46. data/lib/legion/extensions/microsoft_teams/runners/messages.rb +62 -0
  47. data/lib/legion/extensions/microsoft_teams/runners/ownership.rb +11 -0
  48. data/lib/legion/extensions/microsoft_teams/runners/people.rb +25 -0
  49. data/lib/legion/extensions/microsoft_teams/runners/presence.rb +14 -0
  50. data/lib/legion/extensions/microsoft_teams/runners/profile_ingest.rb +18 -4
  51. data/lib/legion/extensions/microsoft_teams/runners/subscriptions.rb +86 -0
  52. data/lib/legion/extensions/microsoft_teams/runners/teams.rb +30 -0
  53. data/lib/legion/extensions/microsoft_teams/runners/transcripts.rb +35 -0
  54. data/lib/legion/extensions/microsoft_teams/version.rb +1 -1
  55. data/lib/legion/extensions/microsoft_teams.rb +10 -3
  56. metadata +20 -8
  57. data/lib/legion/extensions/microsoft_teams/actors/auth_validator.rb +0 -123
  58. data/lib/legion/extensions/microsoft_teams/actors/token_refresher.rb +0 -122
  59. data/lib/legion/extensions/microsoft_teams/cli/auth.rb +0 -94
  60. data/lib/legion/extensions/microsoft_teams/helpers/browser_auth.rb +0 -270
  61. data/lib/legion/extensions/microsoft_teams/helpers/callback_server.rb +0 -90
  62. data/lib/legion/extensions/microsoft_teams/helpers/token_cache.rb +0 -412
  63. data/lib/legion/extensions/microsoft_teams/hooks/auth.rb +0 -17
@@ -11,6 +11,7 @@ module Legion
11
11
  description 'Absorbs Teams meeting transcripts, AI insights, and participants into Apollo'
12
12
 
13
13
  def absorb(url: nil, content: nil, metadata: {}, context: {}) # rubocop:disable Lint/UnusedMethodArgument
14
+ log.debug("Meeting#absorb url=#{url.inspect}")
14
15
  report_progress(message: 'resolving meeting from link')
15
16
  meeting = resolve_meeting(url)
16
17
  return { success: false, error: 'could not resolve meeting' } unless meeting
@@ -26,9 +27,10 @@ module Legion
26
27
  ingest_participants(meeting, subject, results)
27
28
 
28
29
  report_progress(message: 'done', percent: 100)
30
+ log.info("Meeting#absorb complete meeting_id=#{meeting_id} subject=#{subject} chunks=#{results[:chunks]}")
29
31
  results.merge(success: true)
30
32
  rescue StandardError => e
31
- log.error("Meeting absorber failed: #{e.message}")
33
+ handle_exception(e, level: :error, operation: 'Meeting#absorb', url: url)
32
34
  { success: false, error: e.message }
33
35
  end
34
36
 
@@ -47,17 +49,14 @@ module Legion
47
49
  end
48
50
 
49
51
  def graph_token
50
- return @graph_token if defined?(@graph_token)
51
-
52
- @graph_token = begin
53
- Helpers::TokenCache.instance.cached_graph_token if defined?(Helpers::TokenCache)
54
- rescue StandardError => e
55
- log.warn("graph_token unavailable: #{e.message}")
56
- nil
57
- end
52
+ Legion::Extensions::Identity::Entra::Helpers::TokenManager.load_token(:delegated)
53
+ rescue StandardError => e
54
+ handle_exception(e, level: :debug, operation: 'Meeting#graph_token')
55
+ nil
58
56
  end
59
57
 
60
58
  def resolve_meeting(url)
59
+ log.debug("Meeting#resolve_meeting url=#{url.inspect}")
61
60
  thread_id = extract_meeting_thread_id(url)
62
61
  if thread_id
63
62
  report_progress(message: 'resolving meeting from chat thread', percent: 5)
@@ -67,7 +66,7 @@ module Legion
67
66
  report_progress(message: 'looking up meeting by join URL', percent: 5)
68
67
  resolve_meeting_by_join_url(url)
69
68
  rescue StandardError => e
70
- log.warn("Could not resolve meeting: #{e.message}")
69
+ handle_exception(e, level: :warn, operation: 'Meeting#resolve_meeting', url: url)
71
70
  nil
72
71
  end
73
72
 
@@ -76,11 +75,12 @@ module Legion
76
75
  match = uri.path.match(%r{/l/chat/(19:meeting_[^/]+)})
77
76
  match&.[](1)
78
77
  rescue URI::InvalidURIError => e
79
- log.debug("extract_meeting_thread_id: #{e.message}")
78
+ handle_exception(e, level: :debug, operation: 'Meeting#extract_meeting_thread_id', url: url)
80
79
  nil
81
80
  end
82
81
 
83
82
  def resolve_meeting_from_chat(thread_id)
83
+ log.debug("Meeting#resolve_meeting_from_chat thread_id=#{thread_id}")
84
84
  chats_runner = Object.new.extend(Runners::Chats)
85
85
  response = chats_runner.get_chat(chat_id: thread_id, token: graph_token)
86
86
  body = response.is_a?(Hash) ? response[:result] : nil
@@ -95,6 +95,7 @@ module Legion
95
95
  end
96
96
 
97
97
  def resolve_meeting_by_join_url(url)
98
+ log.debug("Meeting#resolve_meeting_by_join_url url=#{url.inspect}")
98
99
  response = meetings_runner.get_meeting_by_join_url(join_url: url, token: graph_token)
99
100
  return nil unless response.is_a?(Hash)
100
101
 
@@ -108,6 +109,7 @@ module Legion
108
109
  end
109
110
 
110
111
  def ingest_transcript(meeting_id, subject, results)
112
+ log.debug("Meeting#ingest_transcript meeting_id=#{meeting_id} subject=#{subject}")
111
113
  report_progress(message: 'fetching transcripts', percent: 20)
112
114
  transcripts_response = transcripts_runner.list_transcripts(meeting_id: meeting_id, token: graph_token)
113
115
  transcripts_body = transcripts_response.is_a?(Hash) ? transcripts_response[:result] : nil
@@ -136,11 +138,13 @@ module Legion
136
138
  )
137
139
  results[:chunks] += 1
138
140
  end
141
+ log.info("Meeting#ingest_transcript stored #{results[:chunks]} transcript(s) for meeting_id=#{meeting_id}")
139
142
  rescue StandardError => e
140
- log.warn("Transcript ingest failed: #{e.message}")
143
+ handle_exception(e, level: :warn, operation: 'Meeting#ingest_transcript', meeting_id: meeting_id)
141
144
  end
142
145
 
143
146
  def ingest_ai_insights(meeting_id, subject, results)
147
+ log.debug("Meeting#ingest_ai_insights meeting_id=#{meeting_id}")
144
148
  report_progress(message: 'fetching AI insights', percent: 60)
145
149
  insights = ai_insights_runner.list_meeting_ai_insights(meeting_id: meeting_id, token: graph_token)
146
150
  return unless insights.is_a?(Hash)
@@ -150,8 +154,9 @@ module Legion
150
154
  return unless items.is_a?(Array) && items.any?
151
155
 
152
156
  items.each { |item| absorb_insight_item(item, meeting_id, subject, results) }
157
+ log.info("Meeting#ingest_ai_insights processed #{items.length} insight(s) for meeting_id=#{meeting_id}")
153
158
  rescue StandardError => e
154
- log.warn("AI insights ingest failed: #{e.message}")
159
+ handle_exception(e, level: :warn, operation: 'Meeting#ingest_ai_insights', meeting_id: meeting_id)
155
160
  end
156
161
 
157
162
  def absorb_insight_item(item, meeting_id, subject, results)
@@ -175,6 +180,7 @@ module Legion
175
180
  end
176
181
 
177
182
  def ingest_participants(meeting, subject, results)
183
+ log.debug("Meeting#ingest_participants subject=#{subject}")
178
184
  report_progress(message: 'recording participants', percent: 80)
179
185
  participants = meeting.dig('participants', 'attendees') || meeting.dig(:participants, :attendees)
180
186
  return unless participants.is_a?(Array) && participants.any?
@@ -192,8 +198,9 @@ module Legion
192
198
  metadata: { meeting_id: meeting_id, participant_count: names.length }
193
199
  )
194
200
  results[:chunks] += 1
201
+ log.info("Meeting#ingest_participants stored #{names.length} participants for meeting_id=#{meeting_id}")
195
202
  rescue StandardError => e
196
- log.warn("Participant ingest failed: #{e.message}")
203
+ handle_exception(e, level: :warn, operation: 'Meeting#ingest_participants', subject: subject)
197
204
  end
198
205
  end
199
206
  end
@@ -14,11 +14,12 @@ module Legion
14
14
  defined?(Legion::Extensions::Absorbers::Base) &&
15
15
  defined?(Legion::Extensions::MicrosoftTeams::Absorbers::Channel)
16
16
  rescue StandardError => e
17
- log.debug("AbsorbChannel#enabled?: #{e.message}")
17
+ handle_exception(e, level: :debug, operation: 'AbsorbChannel#enabled?')
18
18
  false
19
19
  end
20
20
 
21
21
  def work(payload)
22
+ log.debug("AbsorbChannel#work payload=#{payload.inspect[0, 100]}")
22
23
  parsed = parse_payload(payload)
23
24
  absorber = Absorbers::Channel.new
24
25
  result = absorber.absorb(
@@ -30,7 +31,8 @@ module Legion
30
31
  if result[:success]
31
32
  ack!
32
33
  else
33
- log.error("AbsorbChannel actor absorb failed: #{result.inspect}")
34
+ handle_exception(RuntimeError.new(result.inspect), level: :error,
35
+ operation: 'AbsorbChannel#work')
34
36
  reject!(requeue: false)
35
37
  end
36
38
  else
@@ -38,19 +40,20 @@ module Legion
38
40
  end
39
41
  result
40
42
  rescue StandardError => e
41
- log.error("AbsorbChannel actor error: #{e.message}")
43
+ handle_exception(e, level: :error, operation: 'AbsorbChannel#work')
42
44
  reject!(requeue: false)
43
45
  end
44
46
 
45
47
  private
46
48
 
47
49
  def parse_payload(payload)
50
+ log.debug('AbsorbChannel#parse_payload')
48
51
  data = payload.is_a?(String) ? json_load(payload) : payload
49
52
  return {} unless data.is_a?(Hash)
50
53
 
51
54
  data.transform_keys(&:to_sym)
52
55
  rescue StandardError => e
53
- log.debug("AbsorbChannel#parse_payload: #{e.message}")
56
+ handle_exception(e, level: :debug, operation: 'AbsorbChannel#parse_payload')
54
57
  {}
55
58
  end
56
59
  end
@@ -14,11 +14,12 @@ module Legion
14
14
  defined?(Legion::Extensions::Absorbers::Base) &&
15
15
  defined?(Legion::Extensions::MicrosoftTeams::Absorbers::Chat)
16
16
  rescue StandardError => e
17
- log.debug("AbsorbChat#enabled?: #{e.message}")
17
+ handle_exception(e, level: :debug, operation: 'AbsorbChat#enabled?')
18
18
  false
19
19
  end
20
20
 
21
21
  def work(payload)
22
+ log.debug("AbsorbChat#work payload=#{payload.inspect[0, 100]}")
22
23
  parsed = parse_payload(payload)
23
24
  absorber = Absorbers::Chat.new
24
25
  result = absorber.absorb(
@@ -30,7 +31,8 @@ module Legion
30
31
  if result[:success]
31
32
  ack!
32
33
  else
33
- log.error("AbsorbChat actor absorb failed: #{result.inspect}")
34
+ handle_exception(RuntimeError.new(result.inspect), level: :error,
35
+ operation: 'AbsorbChat#work')
34
36
  reject!(requeue: false)
35
37
  end
36
38
  else
@@ -38,19 +40,20 @@ module Legion
38
40
  end
39
41
  result
40
42
  rescue StandardError => e
41
- log.error("AbsorbChat actor error: #{e.message}")
43
+ handle_exception(e, level: :error, operation: 'AbsorbChat#work')
42
44
  reject!(requeue: false)
43
45
  end
44
46
 
45
47
  private
46
48
 
47
49
  def parse_payload(payload)
50
+ log.debug('AbsorbChat#parse_payload')
48
51
  data = payload.is_a?(String) ? json_load(payload) : payload
49
52
  return {} unless data.is_a?(Hash)
50
53
 
51
54
  data.transform_keys(&:to_sym)
52
55
  rescue StandardError => e
53
- log.debug("AbsorbChat#parse_payload: #{e.message}")
56
+ handle_exception(e, level: :debug, operation: 'AbsorbChat#parse_payload')
54
57
  {}
55
58
  end
56
59
  end
@@ -14,11 +14,12 @@ module Legion
14
14
  defined?(Legion::Extensions::Absorbers::Base) &&
15
15
  defined?(Legion::Extensions::MicrosoftTeams::Absorbers::Meeting)
16
16
  rescue StandardError => e
17
- log.debug("AbsorbMeeting#enabled?: #{e.message}")
17
+ handle_exception(e, level: :debug, operation: 'AbsorbMeeting#enabled?')
18
18
  false
19
19
  end
20
20
 
21
21
  def work(payload)
22
+ log.debug("AbsorbMeeting#work payload=#{payload.inspect[0, 100]}")
22
23
  parsed = parse_payload(payload)
23
24
  absorber = Absorbers::Meeting.new
24
25
  result = absorber.absorb(
@@ -30,7 +31,8 @@ module Legion
30
31
  if result[:success]
31
32
  ack!
32
33
  else
33
- log.error("AbsorbMeeting actor absorb failed: #{result.inspect}")
34
+ handle_exception(RuntimeError.new(result.inspect), level: :error,
35
+ operation: 'AbsorbMeeting#work')
34
36
  reject!(requeue: false)
35
37
  end
36
38
  else
@@ -38,19 +40,20 @@ module Legion
38
40
  end
39
41
  result
40
42
  rescue StandardError => e
41
- log.error("AbsorbMeeting actor error: #{e.message}")
43
+ handle_exception(e, level: :error, operation: 'AbsorbMeeting#work')
42
44
  reject!(requeue: false)
43
45
  end
44
46
 
45
47
  private
46
48
 
47
49
  def parse_payload(payload)
50
+ log.debug('AbsorbMeeting#parse_payload')
48
51
  data = payload.is_a?(String) ? json_load(payload) : payload
49
52
  return {} unless data.is_a?(Hash)
50
53
 
51
54
  data.transform_keys(&:to_sym)
52
55
  rescue StandardError => e
53
- log.debug("AbsorbMeeting#parse_payload: #{e.message}")
56
+ handle_exception(e, level: :debug, operation: 'AbsorbMeeting#parse_payload')
54
57
  {}
55
58
  end
56
59
  end
@@ -18,13 +18,12 @@ module Legion
18
18
  def run_now? = true
19
19
 
20
20
  def delay
21
- if defined?(Legion::Extensions::MicrosoftTeams::Actor::AuthValidator)
22
- auth_validator = Legion::Extensions::MicrosoftTeams::Actor::AuthValidator.allocate
23
- base_delay = auth_validator.respond_to?(:delay) ? auth_validator.delay.to_f : 90.0
24
- [base_delay + 5.0, 30].max
25
- else
26
- 30
27
- end
21
+ auth_validator = Legion::Extensions::Identity::Entra::Delegated::Actor::AuthValidator.allocate
22
+ base_delay = auth_validator.respond_to?(:delay) ? auth_validator.delay.to_f : 9.0
23
+ [base_delay + 5.0, 14].max
24
+ rescue StandardError => e
25
+ handle_exception(e, level: :debug, operation: 'ApiIngest#delay')
26
+ 14
28
27
  end
29
28
 
30
29
  def time
@@ -35,11 +34,12 @@ module Legion
35
34
  def enabled?
36
35
  defined?(Legion::Extensions::Agentic::Memory::Trace::Runners::Traces)
37
36
  rescue StandardError => e
38
- log.warn("ApiIngest#enabled?: #{e.message}")
37
+ handle_exception(e, level: :warn, operation: 'ApiIngest#enabled?')
39
38
  false
40
39
  end
41
40
 
42
41
  def manual
42
+ log.debug('ApiIngest#manual starting')
43
43
  token = resolve_token
44
44
  unless token
45
45
  log.warn('ApiIngest: no delegated token, skipping')
@@ -58,7 +58,7 @@ module Legion
58
58
  log.info("ApiIngest: #{result.inspect[0, 200]}")
59
59
  result
60
60
  rescue StandardError => e
61
- log.error("ApiIngest: #{e.message}")
61
+ handle_exception(e, level: :error, operation: 'ApiIngest#manual')
62
62
  end
63
63
 
64
64
  private
@@ -68,11 +68,9 @@ module Legion
68
68
  end
69
69
 
70
70
  def resolve_token
71
- if defined?(Legion::Extensions::MicrosoftTeams::Helpers::TokenCache)
72
- Legion::Extensions::MicrosoftTeams::Helpers::TokenCache.instance.cached_delegated_token
73
- end
71
+ Legion::Extensions::Identity::Entra::Helpers::TokenManager.load_token(:delegated)
74
72
  rescue StandardError => e
75
- log.warn("ApiIngest#resolve_token: #{e.message}")
73
+ handle_exception(e, level: :warn, operation: 'ApiIngest#resolve_token')
76
74
  nil
77
75
  end
78
76
 
@@ -81,7 +79,7 @@ module Legion
81
79
 
82
80
  Legion::Settings[:microsoft_teams] || {}
83
81
  rescue StandardError => e
84
- log.warn("ApiIngest#teams_settings: #{e.message}")
82
+ handle_exception(e, level: :warn, operation: 'ApiIngest#teams_settings')
85
83
  {}
86
84
  end
87
85
 
@@ -90,7 +88,7 @@ module Legion
90
88
 
91
89
  Legion::Extensions::Coldstart::Helpers::Bootstrap.new.imprint_active?
92
90
  rescue StandardError => e
93
- log.debug("ApiIngest#imprint_active?: #{e.message}")
91
+ handle_exception(e, level: :debug, operation: 'ApiIngest#imprint_active?')
94
92
  false
95
93
  end
96
94
  end
@@ -24,10 +24,10 @@ module Legion
24
24
  def manual
25
25
  log.info('CacheBulkIngest firing')
26
26
  result = runner_class.ingest_cache(**args)
27
- log.info("Complete: #{result.inspect[0, 200]}")
27
+ log.info("CacheBulkIngest complete: #{result.inspect[0, 200]}")
28
28
  result
29
29
  rescue StandardError => e
30
- log.error("CacheBulkIngest error: #{e.message}")
30
+ handle_exception(e, level: :error, operation: 'CacheBulkIngest#manual')
31
31
  end
32
32
 
33
33
  def args
@@ -41,7 +41,7 @@ module Legion
41
41
 
42
42
  Legion::Extensions::Coldstart::Helpers::Bootstrap.new.imprint_active?
43
43
  rescue StandardError => e
44
- log.debug("CacheBulkIngest#imprint_active?: #{e.message}")
44
+ handle_exception(e, level: :debug, operation: 'CacheBulkIngest#imprint_active?')
45
45
  false
46
46
  end
47
47
  end
@@ -33,6 +33,7 @@ module Legion
33
33
  end
34
34
 
35
35
  def manual
36
+ log.debug('CacheSync#manual starting')
36
37
  result = runner_class.send(runner_function, since: @last_sync_time, skip_bots: true)
37
38
  if result.is_a?(Hash) && result[:result]
38
39
  latest = result[:result][:latest_time]
@@ -41,7 +42,7 @@ module Legion
41
42
  log.info("CacheSync: ingested #{stored} new Teams messages") if stored.positive?
42
43
  end
43
44
  rescue StandardError => e
44
- log.error("CacheSync: #{e.message}")
45
+ handle_exception(e, level: :error, operation: 'CacheSync#manual')
45
46
  end
46
47
  end
47
48
  end
@@ -36,52 +36,49 @@ module Legion
36
36
  end
37
37
 
38
38
  def enabled?
39
- return false unless defined?(Legion::Extensions::MicrosoftTeams::Helpers::TokenCache)
40
-
41
39
  channel_setting(:enabled, false) == true
42
40
  rescue StandardError => e
43
- log.debug("ChannelPoller#enabled?: #{e.message}")
41
+ handle_exception(e, level: :debug, operation: 'ChannelPoller#enabled?')
44
42
  false
45
43
  end
46
44
 
47
- def token_cache
48
- Legion::Extensions::MicrosoftTeams::Helpers::TokenCache.instance
49
- end
50
-
51
45
  def manual
52
46
  log.info('ChannelPoller polling team channels')
53
- token = token_cache.cached_delegated_token
47
+ token = delegated_token
54
48
  unless token
55
49
  log.debug('No token available, skipping poll')
56
50
  return
57
51
  end
58
52
 
59
53
  teams = fetch_joined_teams(token: token)
60
- log.debug("Found #{teams.length} joined team(s)")
54
+ log.debug("ChannelPoller#manual found #{teams.length} joined team(s)")
61
55
 
62
56
  teams.first(max_teams).each do |team|
63
57
  poll_team(team: team, token: token)
64
58
  rescue StandardError => e
65
- log.error("Error polling team #{team['displayName']}: #{e.message}")
59
+ handle_exception(e, level: :error, operation: 'ChannelPoller#manual',
60
+ team: team['displayName'])
66
61
  end
67
62
  rescue StandardError => e
68
- log.error("ChannelPoller: #{e.message}")
63
+ handle_exception(e, level: :error, operation: 'ChannelPoller#manual')
69
64
  end
70
65
 
71
66
  private
72
67
 
73
68
  def fetch_joined_teams(token:)
69
+ log.debug('ChannelPoller#fetch_joined_teams')
74
70
  conn = graph_connection(token: token)
75
71
  response = conn.get('me/joinedTeams')
76
72
  response.body&.dig('value') || []
77
73
  rescue StandardError => e
78
- log.error("Failed to fetch joined teams: #{e.message}")
74
+ handle_exception(e, level: :error, operation: 'ChannelPoller#fetch_joined_teams')
79
75
  []
80
76
  end
81
77
 
82
78
  def poll_team(team:, token:)
83
79
  team_id = team['id']
84
80
  team_name = team['displayName'] || team_id
81
+ log.debug("ChannelPoller#poll_team team_id=#{team_id} team_name=#{team_name}")
85
82
 
86
83
  channels = fetch_channels(team_id: team_id, token: token)
87
84
  selected = select_channels(channels)
@@ -89,16 +86,18 @@ module Legion
89
86
  selected.first(max_channels_per_team).each do |channel|
90
87
  poll_channel(team_id: team_id, team_name: team_name, channel: channel, token: token)
91
88
  rescue StandardError => e
92
- log.error("Error polling channel #{channel['displayName']} in #{team_name}: #{e.message}")
89
+ handle_exception(e, level: :error, operation: 'ChannelPoller#poll_team',
90
+ channel: channel['displayName'], team: team_name)
93
91
  end
94
92
  end
95
93
 
96
94
  def fetch_channels(team_id:, token:)
95
+ log.debug("ChannelPoller#fetch_channels team_id=#{team_id}")
97
96
  conn = graph_connection(token: token)
98
97
  response = conn.get("teams/#{team_id}/channels")
99
98
  response.body&.dig('value') || []
100
99
  rescue StandardError => e
101
- log.error("Failed to fetch channels for team #{team_id}: #{e.message}")
100
+ handle_exception(e, level: :error, operation: 'ChannelPoller#fetch_channels', team_id: team_id)
102
101
  []
103
102
  end
104
103
 
@@ -112,6 +111,7 @@ module Legion
112
111
  def poll_channel(team_id:, team_name:, channel:, token:)
113
112
  channel_id = channel['id']
114
113
  channel_name = channel['displayName'] || channel_id
114
+ log.debug("ChannelPoller#poll_channel team_id=#{team_id} channel_name=#{channel_name}")
115
115
 
116
116
  conn = graph_connection(token: token)
117
117
  response = conn.get(
@@ -155,12 +155,19 @@ module Legion
155
155
  channel_setting(:max_channels_per_team, DEFAULT_MAX_CHANNELS)
156
156
  end
157
157
 
158
+ def delegated_token
159
+ Legion::Extensions::Identity::Entra::Helpers::TokenManager.load_token(:delegated)
160
+ rescue StandardError => e
161
+ handle_exception(e, level: :warn, operation: 'ChannelPoller#delegated_token')
162
+ nil
163
+ end
164
+
158
165
  def channel_setting(key, default)
159
166
  return default unless defined?(Legion::Settings)
160
167
 
161
168
  Legion::Settings.dig(:microsoft_teams, :channels, key) || default
162
169
  rescue StandardError => e
163
- log.debug("ChannelPoller#channel_setting(#{key}): #{e.message}")
170
+ handle_exception(e, level: :debug, operation: "ChannelPoller#channel_setting(#{key})")
164
171
  default
165
172
  end
166
173
 
@@ -169,6 +176,7 @@ module Legion
169
176
  end
170
177
 
171
178
  def store_channel_message_trace(team_name:, channel_name:, msg:)
179
+ log.debug("ChannelPoller#store_channel_message_trace team=#{team_name} channel=#{channel_name}")
172
180
  sender = msg.dig('from', 'user', 'displayName') || 'Unknown'
173
181
  content = (msg.dig('body', 'content') || '').gsub(/<[^>]+>/, '').strip
174
182
  memory_runner.store_trace(
@@ -179,7 +187,8 @@ module Legion
179
187
  confidence: 0.7
180
188
  )
181
189
  rescue StandardError => e
182
- log.error("Failed to store channel message trace: #{e.message}")
190
+ handle_exception(e, level: :error, operation: 'ChannelPoller#store_channel_message_trace',
191
+ team: team_name, channel: channel_name)
183
192
  end
184
193
  end
185
194
  end
@@ -30,16 +30,13 @@ module Legion
30
30
  defined?(Legion::Extensions::MicrosoftTeams::Runners::Bot) &&
31
31
  Legion.const_defined?(:Transport, false)
32
32
  rescue StandardError => e
33
- log.debug("DirectChatPoller#enabled?: #{e.message}")
33
+ handle_exception(e, level: :debug, operation: 'DirectChatPoller#enabled?')
34
34
  false
35
35
  end
36
36
 
37
- def token_cache
38
- Legion::Extensions::MicrosoftTeams::Helpers::TokenCache.instance
39
- end
40
-
41
37
  def manual
42
- token = token_cache.cached_delegated_token
38
+ log.debug('DirectChatPoller#manual starting')
39
+ token = delegated_token
43
40
  unless token
44
41
  log.debug('No token available, skipping poll')
45
42
  return
@@ -47,21 +44,23 @@ module Legion
47
44
 
48
45
  log.info('Polling bot DM chats')
49
46
  chats = fetch_bot_chats(token: token)
50
- log.info("Found #{chats.length} bot chats")
47
+ log.info("DirectChatPoller found #{chats.length} bot chats")
51
48
  chats.each { |chat| poll_chat(chat_id: chat[:id], token: token) }
52
49
  rescue StandardError => e
53
- log.error("DirectChatPoller: #{e.message}")
50
+ handle_exception(e, level: :error, operation: 'DirectChatPoller#manual')
54
51
  end
55
52
 
56
53
  private
57
54
 
58
55
  def fetch_bot_chats(token:)
56
+ log.debug('DirectChatPoller#fetch_bot_chats')
59
57
  conn = graph_connection(token: token)
60
58
  response = conn.get('me/chats', { '$filter' => "chatType eq 'oneOnOne'", '$top' => 50 })
61
59
  response.body&.dig('value') || []
62
60
  end
63
61
 
64
62
  def poll_chat(chat_id:, token:)
63
+ log.debug("DirectChatPoller#poll_chat chat_id=#{chat_id}")
65
64
  conn = graph_connection(token: token)
66
65
  response = conn.get("chats/#{chat_id}/messages",
67
66
  { '$top' => 10, '$orderby' => 'createdDateTime desc' })
@@ -71,7 +70,7 @@ module Legion
71
70
  new_msgs.reject! { |m| m[:from_id] == @bot_id }
72
71
  return if new_msgs.empty?
73
72
 
74
- log.info("Chat #{chat_id}: #{new_msgs.length} new message(s)")
73
+ log.info("DirectChatPoller chat #{chat_id}: #{new_msgs.length} new message(s)")
75
74
  new_msgs.each { |msg| publish_message(msg.merge(chat_id: chat_id, mode: :direct)) }
76
75
  update_hwm_from_messages(chat_id: chat_id, messages: new_msgs)
77
76
  end
@@ -79,7 +78,7 @@ module Legion
79
78
  def publish_message(payload)
80
79
  Legion::Extensions::MicrosoftTeams::Transport::Messages::TeamsMessage.new.publish(payload)
81
80
  rescue StandardError => e
82
- log.error("DirectChatPoller publish failed: #{e.message}")
81
+ handle_exception(e, level: :error, operation: 'DirectChatPoller#publish_message')
83
82
  end
84
83
 
85
84
  def normalize_messages(messages)
@@ -101,6 +100,13 @@ module Legion
101
100
  Legion::Settings.dig(:microsoft_teams, :bot, :bot_id)
102
101
  end
103
102
 
103
+ def delegated_token
104
+ Legion::Extensions::Identity::Entra::Helpers::TokenManager.load_token(:delegated)
105
+ rescue StandardError => e
106
+ handle_exception(e, level: :warn, operation: 'DirectChatPoller#delegated_token')
107
+ nil
108
+ end
109
+
104
110
  def settings_interval(key, default)
105
111
  return default unless defined?(Legion::Settings)
106
112
 
@@ -17,7 +17,7 @@ module Legion
17
17
  settings = begin
18
18
  Legion::Settings[:microsoft_teams] || {}
19
19
  rescue StandardError => e
20
- log.debug("IncrementalSync#time: #{e.message}")
20
+ handle_exception(e, level: :debug, operation: 'IncrementalSync#time')
21
21
  {}
22
22
  end
23
23
  settings.dig(:ingest, :incremental_interval) || 120
@@ -27,17 +27,19 @@ module Legion
27
27
  defined?(Legion::Extensions::Agentic::Memory::Trace::Runners::Traces) &&
28
28
  token_available?
29
29
  rescue StandardError => e
30
- log.debug("IncrementalSync#enabled?: #{e.message}")
30
+ handle_exception(e, level: :debug, operation: 'IncrementalSync#enabled?')
31
31
  false
32
32
  end
33
33
 
34
34
  def manual
35
+ log.debug('IncrementalSync#manual starting')
35
36
  token = resolve_token
36
37
  return unless token
37
38
 
38
39
  settings = begin
39
40
  Legion::Settings[:microsoft_teams] || {}
40
- rescue StandardError => _e
41
+ rescue StandardError => e
42
+ handle_exception(e, level: :debug, operation: 'IncrementalSync#manual settings')
41
43
  {}
42
44
  end
43
45
  ingest = settings[:ingest] || {}
@@ -47,7 +49,7 @@ module Legion
47
49
  message_depth: ingest.fetch(:message_depth, 50)
48
50
  )
49
51
  rescue StandardError => e
50
- log.error("IncrementalSync: #{e.message}")
52
+ handle_exception(e, level: :error, operation: 'IncrementalSync#manual')
51
53
  end
52
54
 
53
55
  private
@@ -57,11 +59,9 @@ module Legion
57
59
  end
58
60
 
59
61
  def resolve_token
60
- if defined?(Legion::Extensions::MicrosoftTeams::Helpers::TokenCache)
61
- Legion::Extensions::MicrosoftTeams::Helpers::TokenCache.instance.cached_delegated_token
62
- end
62
+ Legion::Extensions::Identity::Entra::Helpers::TokenManager.load_token(:delegated)
63
63
  rescue StandardError => e
64
- log.warn("IncrementalSync#resolve_token: #{e.message}")
64
+ handle_exception(e, level: :warn, operation: 'IncrementalSync#resolve_token')
65
65
  nil
66
66
  end
67
67
  end