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
@@ -9,8 +9,8 @@ module Legion
9
9
 
10
10
  DEFAULT_INGEST_INTERVAL = 300
11
11
 
12
- def runner_class = Legion::Extensions::MicrosoftTeams::Helpers::TokenCache
13
- def runner_function = 'cached_delegated_token'
12
+ def runner_class = self.class
13
+ def runner_function = 'manual'
14
14
  def run_now? = false
15
15
  def use_runner? = false
16
16
  def check_subtask? = false
@@ -25,16 +25,16 @@ module Legion
25
25
  settings = begin
26
26
  Legion::Settings[:microsoft_teams] || {}
27
27
  rescue StandardError => e
28
- log.debug("MeetingIngest#time: #{e.message}")
28
+ handle_exception(e, level: :debug, operation: 'MeetingIngest#time')
29
29
  {}
30
30
  end
31
31
  settings.dig(:meetings, :ingest_interval) || DEFAULT_INGEST_INTERVAL
32
32
  end
33
33
 
34
34
  def enabled?
35
- defined?(Legion::Extensions::MicrosoftTeams::Helpers::TokenCache)
35
+ Legion::Extensions::Identity::Entra::Helpers::TokenManager.respond_to?(:load_token)
36
36
  rescue StandardError => e
37
- log.debug("MeetingIngest#enabled?: #{e.message}")
37
+ handle_exception(e, level: :debug, operation: 'MeetingIngest#enabled?')
38
38
  false
39
39
  end
40
40
 
@@ -46,19 +46,15 @@ module Legion
46
46
  @memory_runner ||= Object.new.extend(Legion::Extensions::Agentic::Memory::Trace::Runners::Traces)
47
47
  end
48
48
 
49
- def token_cache
50
- Legion::Extensions::MicrosoftTeams::Helpers::TokenCache.instance
51
- end
52
-
53
49
  def manual
54
50
  log.info('MeetingIngest polling for meetings')
55
- token = token_cache.cached_delegated_token
51
+ token = Legion::Extensions::Identity::Entra::Helpers::TokenManager.load_token(:delegated)
56
52
  return if token.nil?
57
53
 
58
54
  conn = graph_connection(token: token)
59
55
  response = conn.get("#{user_path('me')}/onlineMeetings")
60
56
  meetings = response.body&.dig('value') || []
61
- log.info("Found #{meetings.length} online meeting(s)")
57
+ log.info("MeetingIngest found #{meetings.length} online meeting(s)")
62
58
 
63
59
  meetings.each do |meeting|
64
60
  meeting_id = meeting['id']
@@ -68,20 +64,22 @@ module Legion
68
64
  process_meeting(meeting_id: meeting_id, subject: meeting['subject'], token: token)
69
65
  @processed_meetings.add(meeting_id)
70
66
  rescue StandardError => e
71
- log.error("Failed to process meeting #{meeting_id}: #{e.message}")
67
+ handle_exception(e, level: :error, operation: 'MeetingIngest#manual',
68
+ meeting_id: meeting_id)
72
69
  end
73
70
  end
74
71
  rescue StandardError => e
75
- log.error("MeetingIngest: #{e.message}")
72
+ handle_exception(e, level: :error, operation: 'MeetingIngest#manual')
76
73
  end
77
74
 
78
75
  private
79
76
 
80
77
  def process_meeting(meeting_id:, subject:, token:)
78
+ log.debug("MeetingIngest#process_meeting meeting_id=#{meeting_id} subject=#{subject}")
81
79
  conn = graph_connection(token: token)
82
80
 
83
81
  transcripts = fetch_transcripts(conn: conn, meeting_id: meeting_id)
84
- log.info("Meeting '#{subject}' (#{meeting_id}): #{transcripts.length} transcript(s)")
82
+ log.info("MeetingIngest '#{subject}' (#{meeting_id}): #{transcripts.length} transcript(s)")
85
83
 
86
84
  transcripts.each do |transcript|
87
85
  fetch_and_log_transcript_content(
@@ -97,15 +95,18 @@ module Legion
97
95
  end
98
96
 
99
97
  def fetch_transcripts(conn:, meeting_id:)
98
+ log.debug("MeetingIngest#fetch_transcripts meeting_id=#{meeting_id}")
100
99
  response = conn.get("#{user_path('me')}/onlineMeetings/#{meeting_id}/transcripts")
101
100
  response.body&.dig('value') || []
102
101
  rescue StandardError => e
103
- log.warn("Could not fetch transcripts for meeting #{meeting_id}: #{e.message}")
102
+ handle_exception(e, level: :warn, operation: 'MeetingIngest#fetch_transcripts',
103
+ meeting_id: meeting_id)
104
104
  []
105
105
  end
106
106
 
107
107
  def fetch_and_log_transcript_content(conn:, meeting_id:, subject:, token:, transcript:) # rubocop:disable Lint/UnusedMethodArgument
108
108
  tid = transcript['id']
109
+ log.debug("MeetingIngest#fetch_and_log_transcript_content meeting_id=#{meeting_id} transcript_id=#{tid}")
109
110
  content_conn = graph_connection(token: token)
110
111
  content_response = content_conn.get(
111
112
  "#{user_path('me')}/onlineMeetings/#{meeting_id}/transcripts/#{tid}/content",
@@ -114,22 +115,24 @@ module Legion
114
115
  )
115
116
  content = content_response.body.to_s
116
117
  preview = content[0, 200]
117
- log.debug("Meeting '#{subject}' transcript #{tid}: #{preview}")
118
+ log.debug("MeetingIngest '#{subject}' transcript #{tid}: #{preview}")
118
119
  store_transcript_trace(meeting_id: meeting_id, subject: subject, transcript_id: tid, content: content) if memory_available?
119
120
  rescue StandardError => e
120
- log.warn("Could not fetch transcript content #{tid} for meeting #{meeting_id}: #{e.message}")
121
+ handle_exception(e, level: :warn, operation: 'MeetingIngest#fetch_and_log_transcript_content',
122
+ meeting_id: meeting_id, transcript_id: tid)
121
123
  end
122
124
 
123
125
  def fetch_and_log_ai_insights(conn:, meeting_id:, subject:)
126
+ log.debug("MeetingIngest#fetch_and_log_ai_insights meeting_id=#{meeting_id}")
124
127
  response = conn.get("#{user_path('me')}/onlineMeetings/#{meeting_id}/aiInsights")
125
128
  insights = response.body&.dig('value') || []
126
- log.info("Meeting '#{subject}' (#{meeting_id}): #{insights.length} AI insight(s)")
129
+ log.info("MeetingIngest '#{subject}' (#{meeting_id}): #{insights.length} AI insight(s)")
127
130
 
128
131
  insights.each do |insight|
129
132
  action_items = insight['actionItems'] || []
130
133
  next if action_items.empty?
131
134
 
132
- log.info("Meeting '#{subject}' AI insight action items (#{action_items.length}):")
135
+ log.info("MeetingIngest '#{subject}' AI insight action items (#{action_items.length}):")
133
136
  action_items.each do |item|
134
137
  log.info(" - #{item['text'] || item.inspect}")
135
138
  end
@@ -137,10 +140,12 @@ module Legion
137
140
  store_insight_trace(meeting_id: meeting_id, subject: subject, insight: insight) if memory_available?
138
141
  end
139
142
  rescue StandardError => e
140
- log.warn("Could not fetch AI insights for meeting #{meeting_id}: #{e.message}")
143
+ handle_exception(e, level: :warn, operation: 'MeetingIngest#fetch_and_log_ai_insights',
144
+ meeting_id: meeting_id)
141
145
  end
142
146
 
143
147
  def store_transcript_trace(meeting_id:, subject:, transcript_id:, content:) # rubocop:disable Lint/UnusedMethodArgument
148
+ log.debug("MeetingIngest#store_transcript_trace meeting_id=#{meeting_id} transcript_id=#{transcript_id}")
144
149
  memory_runner.store_trace(
145
150
  type: :episodic,
146
151
  content_payload: content[0, 10_000],
@@ -149,10 +154,12 @@ module Legion
149
154
  confidence: 0.9
150
155
  )
151
156
  rescue StandardError => e
152
- log.warn("Could not store transcript trace for meeting #{meeting_id}: #{e.message}")
157
+ handle_exception(e, level: :warn, operation: 'MeetingIngest#store_transcript_trace',
158
+ meeting_id: meeting_id, transcript_id: transcript_id)
153
159
  end
154
160
 
155
161
  def store_insight_trace(meeting_id:, subject:, insight:) # rubocop:disable Lint/UnusedMethodArgument
162
+ log.debug("MeetingIngest#store_insight_trace meeting_id=#{meeting_id}")
156
163
  memory_runner.store_trace(
157
164
  type: :semantic,
158
165
  content_payload: insight.to_s,
@@ -161,7 +168,8 @@ module Legion
161
168
  confidence: 0.8
162
169
  )
163
170
  rescue StandardError => e
164
- log.warn("Could not store insight trace for meeting #{meeting_id}: #{e.message}")
171
+ handle_exception(e, level: :warn, operation: 'MeetingIngest#store_insight_trace',
172
+ meeting_id: meeting_id)
165
173
  end
166
174
  end
167
175
  end
@@ -32,23 +32,21 @@ module Legion
32
32
 
33
33
  Legion::Settings.dig(:microsoft_teams, :bot, :observe, :enabled) == true
34
34
  rescue StandardError => e
35
- log.debug("ObservedChatPoller#enabled?: #{e.message}")
35
+ handle_exception(e, level: :debug, operation: 'ObservedChatPoller#enabled?')
36
36
  false
37
37
  end
38
38
 
39
- def token_cache
40
- Legion::Extensions::MicrosoftTeams::Helpers::TokenCache.instance
41
- end
42
-
43
39
  def subscription_registry
44
40
  @subscription_registry ||= Legion::Extensions::MicrosoftTeams::Helpers::SubscriptionRegistry.new
45
41
  end
46
42
 
47
43
  def manual
48
- token = token_cache.cached_app_token
44
+ log.debug('ObservedChatPoller#manual starting')
45
+ token = delegated_token
49
46
  return unless token
50
47
 
51
48
  subscriptions = subscription_registry.active_subscriptions
49
+ log.debug("ObservedChatPoller#manual subscriptions=#{subscriptions.length}")
52
50
  subscriptions.each do |sub|
53
51
  poll_observed_chat(
54
52
  chat_id: sub[:chat_id], owner_id: sub[:owner_id],
@@ -56,12 +54,13 @@ module Legion
56
54
  )
57
55
  end
58
56
  rescue StandardError => e
59
- log.error("ObservedChatPoller: #{e.message}")
57
+ handle_exception(e, level: :error, operation: 'ObservedChatPoller#manual')
60
58
  end
61
59
 
62
60
  private
63
61
 
64
62
  def poll_observed_chat(chat_id:, owner_id:, peer_name:, token:)
63
+ log.debug("ObservedChatPoller#poll_observed_chat chat_id=#{chat_id} peer_name=#{peer_name}")
65
64
  conn = graph_connection(token: token)
66
65
  response = conn.get("chats/#{chat_id}/messages",
67
66
  { '$top' => 10, '$orderby' => 'createdDateTime desc' })
@@ -85,7 +84,7 @@ module Legion
85
84
  def publish_message(payload)
86
85
  Legion::Extensions::MicrosoftTeams::Transport::Messages::TeamsMessage.new.publish(payload)
87
86
  rescue StandardError => e
88
- log.error("ObservedChatPoller publish failed: #{e.message}")
87
+ handle_exception(e, level: :error, operation: 'ObservedChatPoller#publish_message')
89
88
  end
90
89
 
91
90
  def normalize_messages(messages)
@@ -101,6 +100,13 @@ module Legion
101
100
  end
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: 'ObservedChatPoller#delegated_token')
107
+ nil
108
+ end
109
+
104
110
  def settings_interval(key, default)
105
111
  return default unless defined?(Legion::Settings)
106
112
 
@@ -23,18 +23,15 @@ module Legion
23
23
  end
24
24
 
25
25
  def enabled?
26
- defined?(Legion::Extensions::MicrosoftTeams::Helpers::TokenCache)
26
+ Legion::Extensions::Identity::Entra::Helpers::TokenManager.respond_to?(:load_token)
27
27
  rescue StandardError => e
28
- log.debug("PresencePoller#enabled?: #{e.message}")
28
+ handle_exception(e, level: :debug, operation: 'PresencePoller#enabled?')
29
29
  false
30
30
  end
31
31
 
32
- def token_cache
33
- Legion::Extensions::MicrosoftTeams::Helpers::TokenCache.instance
34
- end
35
-
36
32
  def manual
37
- token = token_cache.cached_delegated_token
33
+ log.debug('PresencePoller#manual starting')
34
+ token = delegated_token
38
35
  unless token
39
36
  log.debug('No token available, skipping presence poll')
40
37
  return
@@ -56,7 +53,16 @@ module Legion
56
53
  @last_presence = current
57
54
  end
58
55
  rescue StandardError => e
59
- log.error("PresencePoller: #{e.message}")
56
+ handle_exception(e, level: :error, operation: 'PresencePoller#manual')
57
+ end
58
+
59
+ private
60
+
61
+ def delegated_token
62
+ Legion::Extensions::Identity::Entra::Helpers::TokenManager.load_token(:delegated)
63
+ rescue StandardError => e
64
+ handle_exception(e, level: :warn, operation: 'PresencePoller#delegated_token')
65
+ nil
60
66
  end
61
67
  end
62
68
  end
@@ -12,20 +12,19 @@ module Legion
12
12
  def generate_task? = false
13
13
 
14
14
  def delay
15
- if defined?(Legion::Extensions::MicrosoftTeams::Actor::AuthValidator)
16
- auth_validator = Legion::Extensions::MicrosoftTeams::Actor::AuthValidator.allocate
17
- base_delay = auth_validator.respond_to?(:delay) ? auth_validator.delay.to_f : 90.0
18
- base_delay + 5.0
19
- else
20
- 95.0
21
- end
15
+ auth_validator = Legion::Extensions::Identity::Entra::Delegated::Actor::AuthValidator.allocate
16
+ base_delay = auth_validator.respond_to?(:delay) ? auth_validator.delay.to_f : 9.0
17
+ base_delay + 5.0
18
+ rescue StandardError => e
19
+ handle_exception(e, level: :debug, operation: 'ProfileIngest#delay')
20
+ 14.0
22
21
  end
23
22
 
24
23
  def enabled?
25
24
  defined?(Legion::Extensions::Agentic::Memory::Trace::Runners::Traces) &&
26
25
  token_available?
27
26
  rescue StandardError => e
28
- log.debug("ProfileIngest#enabled?: #{e.message}")
27
+ handle_exception(e, level: :debug, operation: 'ProfileIngest#enabled?')
29
28
  false
30
29
  end
31
30
 
@@ -33,15 +32,15 @@ module Legion
33
32
  log.info('ProfileIngest firing')
34
33
  token = resolve_token
35
34
  unless token
36
- log.warn('No token available, skipping')
35
+ log.warn('ProfileIngest: no token available, skipping')
37
36
  return
38
37
  end
39
- log.info('Token acquired, starting ingest')
38
+ log.info('ProfileIngest: token acquired, starting ingest')
40
39
 
41
40
  settings = begin
42
41
  Legion::Settings[:microsoft_teams] || {}
43
42
  rescue StandardError => e
44
- log.debug("ProfileIngest#manual settings: #{e.message}")
43
+ handle_exception(e, level: :debug, operation: 'ProfileIngest#manual settings')
45
44
  {}
46
45
  end
47
46
  ingest = settings[:ingest] || {}
@@ -51,7 +50,7 @@ module Legion
51
50
  message_depth: ingest.fetch(:message_depth, 50)
52
51
  )
53
52
  rescue StandardError => e
54
- log.error("ProfileIngest: #{e.message}")
53
+ handle_exception(e, level: :error, operation: 'ProfileIngest#manual')
55
54
  end
56
55
 
57
56
  private
@@ -61,11 +60,9 @@ module Legion
61
60
  end
62
61
 
63
62
  def resolve_token
64
- if defined?(Legion::Extensions::MicrosoftTeams::Helpers::TokenCache)
65
- Legion::Extensions::MicrosoftTeams::Helpers::TokenCache.instance.cached_delegated_token
66
- end
63
+ Legion::Extensions::Identity::Entra::Helpers::TokenManager.load_token(:delegated)
67
64
  rescue StandardError => e
68
- log.warn("ProfileIngest#resolve_token: #{e.message}")
65
+ handle_exception(e, level: :warn, operation: 'ProfileIngest#resolve_token')
69
66
  nil
70
67
  end
71
68
  end
@@ -11,8 +11,7 @@ module Legion
11
11
  Legion::Extensions::Helpers.const_defined?(:Lex, false)
12
12
 
13
13
  def graph_connection(token: nil, api_url: 'https://graph.microsoft.com/v1.0', **_opts)
14
- token ||= settings&.dig(:auth, :delegated, :token)
15
- token ||= TokenCache.instance.cached_delegated_token if defined?(TokenCache)
14
+ token ||= entra_delegated_token
16
15
  Faraday.new(url: api_url) do |conn|
17
16
  conn.request :json
18
17
  conn.response :json, content_type: /\bjson$/
@@ -22,8 +21,6 @@ module Legion
22
21
  end
23
22
 
24
23
  def bot_connection(token: nil, service_url: 'https://smba.trafficmanager.net/teams/', **_opts)
25
- token ||= settings&.dig(:auth, :bot, :token)
26
- token ||= TokenCache.instance.cached_app_token if defined?(TokenCache)
27
24
  Faraday.new(url: service_url) do |conn|
28
25
  conn.request :json
29
26
  conn.response :json, content_type: /\bjson$/
@@ -42,6 +39,15 @@ module Legion
42
39
  conn.response :json, content_type: /\bjson$/
43
40
  end
44
41
  end
42
+
43
+ private
44
+
45
+ def entra_delegated_token
46
+ Legion::Extensions::Identity::Entra::Helpers::TokenManager.load_token(:delegated)
47
+ rescue StandardError => e
48
+ handle_exception(e, level: :debug, operation: 'Client#entra_delegated_token')
49
+ nil
50
+ end
45
51
  end
46
52
  end
47
53
  end
@@ -59,7 +59,8 @@ module Legion
59
59
 
60
60
  raw.is_a?(Hash) ? raw : ::JSON.parse(raw, symbolize_names: true)
61
61
  rescue StandardError => e
62
- log.debug("HighWaterMark: get_extended_hwm failed to parse cached value: #{e.message}")
62
+ handle_exception(e, level: :debug, operation: 'HighWaterMark#get_extended_hwm',
63
+ chat_id: chat_id)
63
64
  nil
64
65
  end
65
66
 
@@ -108,7 +109,7 @@ module Legion
108
109
  last_ingested_at: data[:last_ingested_at], message_count: data[:message_count] || 0)
109
110
  end
110
111
  rescue StandardError => e
111
- log.warn("Failed to restore HWM from traces: #{e.message}")
112
+ handle_exception(e, level: :warn, operation: 'HighWaterMark#restore_hwm_from_traces')
112
113
  end
113
114
 
114
115
  def memory_runner
@@ -54,7 +54,10 @@ module Legion
54
54
  profile = Legion::Extensions::Mesh::Helpers::PreferenceProfile.resolve(owner_id: owner_id)
55
55
  Legion::Extensions::Mesh::Helpers::PreferenceProfile.preference_instructions(profile: profile)
56
56
  rescue StandardError => e
57
- log.debug("PromptResolver: preference_instructions_for failed: #{e.message}") if defined?(log)
57
+ if defined?(handle_exception)
58
+ handle_exception(e, level: :debug, operation: 'PromptResolver#preference_instructions_for',
59
+ owner_id: owner_id)
60
+ end
58
61
  nil
59
62
  end
60
63
  end
@@ -115,6 +115,7 @@ module Legion
115
115
  def store_session_to_memory(conversation_id:, session:)
116
116
  return unless defined?(Legion::Extensions::Agentic::Memory::Trace::Runners::Traces)
117
117
 
118
+ log.debug("SessionManager#store_session_to_memory conversation_id=#{conversation_id}")
118
119
  memory_runner.store_trace(
119
120
  type: :episodic,
120
121
  content_payload: {
@@ -129,8 +130,10 @@ module Legion
129
130
  origin: :direct_experience,
130
131
  confidence: 0.8
131
132
  )
133
+ log.info("SessionManager: stored session trace conversation_id=#{conversation_id}")
132
134
  rescue StandardError => e
133
- log.error("SessionManager persist failed: #{e.message}")
135
+ handle_exception(e, level: :error, operation: 'SessionManager#store_session_to_memory',
136
+ conversation_id: conversation_id)
134
137
  end
135
138
 
136
139
  def memory_runner
@@ -149,7 +152,10 @@ module Legion
149
152
 
150
153
  profile_traces.map { |t| { type: t[:trace_type], content: t[:content_payload].to_s[0, 200] } }
151
154
  rescue StandardError => e
152
- log.debug("trace_seed_for failed: #{e.message}") if defined?(log) && log.respond_to?(:debug)
155
+ if defined?(handle_exception)
156
+ handle_exception(e, level: :debug, operation: 'SessionManager#trace_seed_for',
157
+ owner_id: owner_id)
158
+ end
153
159
  nil
154
160
  end
155
161
 
@@ -88,12 +88,13 @@ module Legion
88
88
  parsed = parse_stored(stored[:content_payload])
89
89
  @mutex.synchronize { @subscriptions = parsed } if parsed.is_a?(Hash)
90
90
  rescue StandardError => e
91
- log.error("SubscriptionRegistry load failed: #{e.message}")
91
+ handle_exception(e, level: :error, operation: 'SubscriptionRegistry#load')
92
92
  end
93
93
 
94
94
  def persist
95
95
  return unless memory_available?
96
96
 
97
+ log.debug('SubscriptionRegistry#persist')
97
98
  memory_runner.store_trace(
98
99
  type: :semantic,
99
100
  content_payload: serialize_subscriptions,
@@ -101,8 +102,9 @@ module Legion
101
102
  origin: :system,
102
103
  confidence: 1.0
103
104
  )
105
+ log.info("SubscriptionRegistry: persisted #{@subscriptions.size} subscription(s)")
104
106
  rescue StandardError => e
105
- log.error("SubscriptionRegistry persist failed: #{e.message}")
107
+ handle_exception(e, level: :error, operation: 'SubscriptionRegistry#persist')
106
108
  end
107
109
 
108
110
  private
@@ -124,7 +126,7 @@ module Legion
124
126
  v
125
127
  end
126
128
  rescue ::JSON::ParserError => e
127
- log.warn("SubscriptionRegistry: corrupted subscription data, resetting: #{e.message}")
129
+ handle_exception(e, level: :warn, operation: 'SubscriptionRegistry#parse_stored')
128
130
  {}
129
131
  end
130
132
 
@@ -9,6 +9,7 @@ module Legion
9
9
  MAX_TRACES = 20
10
10
 
11
11
  def retrieve_context(message:, owner_id:, chat_id: nil, channel_id: nil) # rubocop:disable Lint/UnusedMethodArgument
12
+ log.debug("TraceRetriever#retrieve_context owner_id=#{owner_id} chat_id=#{chat_id}") if defined?(log)
12
13
  return nil unless memory_trace_available?
13
14
 
14
15
  traces = []
@@ -129,7 +130,11 @@ module Legion
129
130
  def log_trace_error(method, error)
130
131
  return unless defined?(log)
131
132
 
132
- log.debug("TraceRetriever##{method} failed: #{error.message}")
133
+ if defined?(handle_exception)
134
+ handle_exception(error, level: :debug, operation: "TraceRetriever##{method}")
135
+ else
136
+ log.debug("TraceRetriever##{method} failed: #{error.message}")
137
+ end
133
138
  end
134
139
  end
135
140
  end
@@ -71,7 +71,7 @@ module Legion
71
71
  extract_from_record(value, messages, seen_hashes)
72
72
  end
73
73
  rescue StandardError => e
74
- warn "LocalCache: error reading #{File.basename(path)}: #{e.message}"
74
+ warn "LocalCache: error reading #{File.basename(path)}: #{e.class} — #{e.message}"
75
75
  end
76
76
 
77
77
  messages = apply_filters(messages, since: since, channels: channels,
@@ -58,7 +58,8 @@ module Legion
58
58
  when 0x00 then block
59
59
  when 0x01 then Snappy.inflate(block)
60
60
  end
61
- rescue Snappy::Error => _e
61
+ rescue Snappy::Error => e
62
+ warn "SSTableReader: snappy decompression failed at offset=#{offset}: #{e.message}"
62
63
  nil
63
64
  end
64
65
 
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/microsoft_teams/helpers/client'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module MicrosoftTeams
8
+ module Runners
9
+ module Activities
10
+ extend Legion::Extensions::Definitions
11
+ include Legion::Extensions::MicrosoftTeams::Helpers::Client
12
+
13
+ def self.trigger_words
14
+ %w[notification notifications activity alert alerts]
15
+ end
16
+
17
+ definition :send_activity_notification,
18
+ desc: 'Send an activity notification to a Teams user',
19
+ mcp_prefix: 'teams.send_activity_notification',
20
+ mcp_category: 'teams_activities',
21
+ mcp_tier: :elevated,
22
+ idempotent: false,
23
+ inputs: { properties: { topic: { type: 'string',
24
+ description: 'Notification topic object' },
25
+ activity_type: { type: 'string',
26
+ description: 'Activity type registered in app manifest' } },
27
+ required: %w[topic activity_type] },
28
+ trigger_words: %w[notify notification activity]
29
+
30
+ def send_activity_notification(topic:, activity_type:, user_id: 'me', preview_text: nil, **)
31
+ payload = { topic: topic, activityType: activity_type }
32
+ payload[:previewText] = preview_text if preview_text
33
+ response = graph_connection(**).post("#{user_path(user_id)}/teamwork/sendActivityNotification", payload)
34
+ { result: response.body }
35
+ end
36
+
37
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers, false) &&
38
+ Legion::Extensions::Helpers.const_defined?(:Lex, false)
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end