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
@@ -7,33 +7,95 @@ module Legion
7
7
  module MicrosoftTeams
8
8
  module Runners
9
9
  module AiInsights
10
+ extend Legion::Extensions::Definitions
10
11
  include Legion::Extensions::MicrosoftTeams::Helpers::Client
11
12
 
13
+ def self.trigger_words
14
+ %w[insight insights summary callrecord recorded]
15
+ end
16
+
17
+ definition :list_meeting_ai_insights,
18
+ desc: 'List AI-generated insights for an online meeting',
19
+ mcp_prefix: 'teams.list_meeting_ai_insights',
20
+ mcp_category: 'teams_meetings',
21
+ mcp_tier: :standard,
22
+ idempotent: true,
23
+ inputs: { properties: { meeting_id: { type: 'string' } }, required: ['meeting_id'] },
24
+ trigger_words: %w[insights summary ai]
25
+
12
26
  def list_meeting_ai_insights(meeting_id:, user_id: 'me', **)
13
27
  response = graph_connection(**).get("#{user_path(user_id)}/onlineMeetings/#{meeting_id}/aiInsights")
14
28
  { result: response.body }
15
29
  end
16
30
 
31
+ definition :get_meeting_ai_insight,
32
+ desc: 'Get a specific AI insight for an online meeting',
33
+ mcp_prefix: 'teams.get_meeting_ai_insight',
34
+ mcp_category: 'teams_meetings',
35
+ mcp_tier: :standard,
36
+ idempotent: true,
37
+ inputs: { properties: { meeting_id: { type: 'string' },
38
+ insight_id: { type: 'string' } },
39
+ required: %w[meeting_id insight_id] },
40
+ trigger_words: %w[insight details]
41
+
17
42
  def get_meeting_ai_insight(meeting_id:, insight_id:, user_id: 'me', **)
18
43
  response = graph_connection(**).get("#{user_path(user_id)}/onlineMeetings/#{meeting_id}/aiInsights/#{insight_id}")
19
44
  { result: response.body }
20
45
  end
21
46
 
47
+ definition :list_meeting_recordings,
48
+ desc: 'List recordings for an online meeting',
49
+ mcp_prefix: 'teams.list_meeting_recordings',
50
+ mcp_category: 'teams_meetings',
51
+ mcp_tier: :standard,
52
+ idempotent: true,
53
+ inputs: { properties: { meeting_id: { type: 'string' } }, required: ['meeting_id'] },
54
+ trigger_words: %w[recordings recorded]
55
+
22
56
  def list_meeting_recordings(meeting_id:, user_id: 'me', **)
23
57
  response = graph_connection(**).get("#{user_path(user_id)}/onlineMeetings/#{meeting_id}/recordings")
24
58
  { result: response.body }
25
59
  end
26
60
 
61
+ definition :get_meeting_recording,
62
+ desc: 'Get a specific recording for an online meeting',
63
+ mcp_prefix: 'teams.get_meeting_recording',
64
+ mcp_category: 'teams_meetings',
65
+ mcp_tier: :standard,
66
+ idempotent: true,
67
+ inputs: { properties: { meeting_id: { type: 'string' },
68
+ recording_id: { type: 'string' } },
69
+ required: %w[meeting_id recording_id] },
70
+ trigger_words: %w[recording download]
71
+
27
72
  def get_meeting_recording(meeting_id:, recording_id:, user_id: 'me', **)
28
73
  response = graph_connection(**).get("#{user_path(user_id)}/onlineMeetings/#{meeting_id}/recordings/#{recording_id}")
29
74
  { result: response.body }
30
75
  end
31
76
 
77
+ definition :list_call_records,
78
+ desc: 'List Teams call records from communications API',
79
+ mcp_prefix: 'teams.list_call_records',
80
+ mcp_category: 'teams_meetings',
81
+ mcp_tier: :standard,
82
+ idempotent: true,
83
+ trigger_words: %w[records calls history]
84
+
32
85
  def list_call_records(**)
33
86
  response = graph_connection(**).get('communications/callRecords')
34
87
  { result: response.body }
35
88
  end
36
89
 
90
+ definition :get_call_record,
91
+ desc: 'Get a specific Teams call record',
92
+ mcp_prefix: 'teams.get_call_record',
93
+ mcp_category: 'teams_meetings',
94
+ mcp_tier: :standard,
95
+ idempotent: true,
96
+ inputs: { properties: { call_id: { type: 'string' } }, required: ['call_id'] },
97
+ trigger_words: %w[callrecord record]
98
+
37
99
  def get_call_record(call_id:, **)
38
100
  response = graph_connection(**).get("communications/callRecords/#{call_id}")
39
101
  { result: response.body }
@@ -11,17 +11,21 @@ module Legion
11
11
  module MicrosoftTeams
12
12
  module Runners
13
13
  module ApiIngest
14
+ extend Legion::Extensions::Definitions
14
15
  include Helpers::Client
15
16
  include Helpers::PermissionGuard
16
17
  include Helpers::HighWaterMark
17
18
  extend self
18
19
 
20
+ definition :ingest_api, mcp_exposed: false
21
+
19
22
  # Fetch top contacts via /me/people, then pull recent messages from
20
23
  # their 1:1 chats. Stores each message as an individual memory trace
21
24
  # (same format as CacheIngest) with dedup by content hash.
22
25
  #
23
26
  # Requires a delegated token with Chat.Read and People.Read scopes.
24
27
  def ingest_api(token:, top_people: 15, message_depth: 50, skip_bots: true, imprint_active: false, **) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
28
+ log.debug("ApiIngest#ingest_api top_people=#{top_people} message_depth=#{message_depth}")
25
29
  return error_result('lex-memory not loaded') unless memory_available?
26
30
  return error_result('no token provided') unless token && !token.empty?
27
31
 
@@ -98,8 +102,7 @@ module Legion
98
102
  people_found: people.length, chats_found: chats.length,
99
103
  apollo: apollo_results } }
100
104
  rescue StandardError => e
101
- log_msg = "ApiIngest failed: #{e.class} — #{e.message}"
102
- log.error(log_msg)
105
+ handle_exception(e, level: :error, operation: 'ApiIngest#ingest_api')
103
106
  { result: { stored: stored || 0, skipped: skipped || 0, error: e.message } }
104
107
  end
105
108
 
@@ -111,6 +114,7 @@ module Legion
111
114
  private
112
115
 
113
116
  def fetch_top_people(token:, top:)
117
+ log.debug("ApiIngest#fetch_top_people top=#{top}")
114
118
  return [] if permission_denied?('/me/people')
115
119
 
116
120
  conn = graph_connection(token: token)
@@ -125,7 +129,7 @@ module Legion
125
129
  people = (resp.body || {}).fetch('value', [])
126
130
  people.sort_by { |p| -(p.dig('scoredEmailAddresses', 0, 'relevanceScore') || 0) }
127
131
  rescue StandardError => e
128
- log.warn("ApiIngest: fetch_top_people failed: #{e.message}")
132
+ handle_exception(e, level: :warn, operation: 'ApiIngest#fetch_top_people')
129
133
  []
130
134
  end
131
135
 
@@ -156,7 +160,7 @@ module Legion
156
160
  log.info("ApiIngest: fetched #{all_chats.size} chats (#{pages} pages), #{filtered.size} eligible (1:1/group/meeting)")
157
161
  filtered
158
162
  rescue StandardError => e
159
- log.warn("ApiIngest: fetch_chats failed: #{e.message}")
163
+ handle_exception(e, level: :warn, operation: 'ApiIngest#fetch_one_on_one_chats')
160
164
  []
161
165
  end
162
166
 
@@ -174,7 +178,7 @@ module Legion
174
178
  end
175
179
  end
176
180
  rescue StandardError => e
177
- log.debug("ApiIngest: match_chat_to_person failed: #{e.message}")
181
+ handle_exception(e, level: :debug, operation: 'ApiIngest#match_chat_to_person')
178
182
  nil
179
183
  end
180
184
 
@@ -198,7 +202,8 @@ module Legion
198
202
  log.debug("ApiIngest: fetch_messages chat=#{chat_id} count=#{(resp.body || {}).fetch('value', []).size}")
199
203
  (resp.body || {}).fetch('value', [])
200
204
  rescue StandardError => e
201
- log.warn("ApiIngest: fetch_messages failed for #{chat_id}: #{e.message}")
205
+ handle_exception(e, level: :warn, operation: 'ApiIngest#fetch_chat_messages',
206
+ chat_id: chat_id)
202
207
  []
203
208
  end
204
209
 
@@ -242,7 +247,7 @@ module Legion
242
247
  imprint_active: imprint_active
243
248
  )
244
249
  rescue StandardError => e
245
- log.warn("ApiIngest: store trace failed: #{e.message}")
250
+ handle_exception(e, level: :warn, operation: 'ApiIngest#store_graph_message')
246
251
  nil
247
252
  end
248
253
 
@@ -267,7 +272,7 @@ module Legion
267
272
  end
268
273
  hashes
269
274
  rescue StandardError => e
270
- log.debug("ApiIngest: load_existing_hashes failed: #{e.message}")
275
+ handle_exception(e, level: :debug, operation: 'ApiIngest#load_existing_hashes')
271
276
  Set.new
272
277
  end
273
278
 
@@ -283,7 +288,7 @@ module Legion
283
288
  store = Legion::Extensions::Agentic::Memory::Trace.shared_store
284
289
  store.flush if store.respond_to?(:flush)
285
290
  rescue StandardError => e
286
- log.warn("ApiIngest: flush failed: #{e.message}")
291
+ handle_exception(e, level: :warn, operation: 'ApiIngest#flush_trace_store')
287
292
  end
288
293
 
289
294
  def coactivate_thread_traces(thread_groups)
@@ -296,12 +301,12 @@ module Legion
296
301
  trace_ids.each_cons(2) do |id_a, id_b|
297
302
  store.record_coactivation(id_a, id_b)
298
303
  rescue StandardError => e
299
- log.debug("ApiIngest: coactivation link failed for #{id_a}/#{id_b}: #{e.message}")
300
- nil
304
+ handle_exception(e, level: :debug, operation: 'ApiIngest#coactivate_thread_traces',
305
+ id_a: id_a, id_b: id_b)
301
306
  end
302
307
  end
303
308
  rescue StandardError => e
304
- log.debug("ApiIngest: coactivation skipped: #{e.message}")
309
+ handle_exception(e, level: :debug, operation: 'ApiIngest#coactivate_thread_traces')
305
310
  end
306
311
 
307
312
  def publish_to_apollo(person_texts)
@@ -332,7 +337,7 @@ module Legion
332
337
 
333
338
  { ingested: ingested, entities_found: entities_found }
334
339
  rescue StandardError => e
335
- log.warn("ApiIngest: publish_to_apollo failed: #{e.message}")
340
+ handle_exception(e, level: :warn, operation: 'ApiIngest#publish_to_apollo')
336
341
  { skipped: true, reason: :error, error: e.message }
337
342
  end
338
343
 
@@ -358,7 +363,8 @@ module Legion
358
363
 
359
364
  { success: true, count: result[:entities].length }
360
365
  rescue StandardError => e
361
- log.debug("ApiIngest: entity extraction failed for #{person_name}: #{e.message}")
366
+ handle_exception(e, level: :debug, operation: 'ApiIngest#extract_and_ingest_entities',
367
+ person_name: person_name)
362
368
  { success: false, count: 0 }
363
369
  end
364
370
 
@@ -0,0 +1,86 @@
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 AppInstallations
10
+ extend Legion::Extensions::Definitions
11
+ include Legion::Extensions::MicrosoftTeams::Helpers::Client
12
+
13
+ def self.trigger_words
14
+ %w[app apps addon addons installation installed]
15
+ end
16
+
17
+ definition :list_installed_apps_for_user,
18
+ desc: 'List Teams apps installed for a user',
19
+ mcp_prefix: 'teams.list_installed_apps_for_user',
20
+ mcp_category: 'teams_apps',
21
+ mcp_tier: :low,
22
+ idempotent: true,
23
+ trigger_words: %w[apps installed]
24
+
25
+ def list_installed_apps_for_user(user_id: 'me', **)
26
+ response = graph_connection(**).get("#{user_path(user_id)}/teamwork/installedApps")
27
+ { result: response.body }
28
+ end
29
+
30
+ definition :list_installed_apps_in_chat,
31
+ desc: 'List Teams apps installed in a specific chat',
32
+ mcp_prefix: 'teams.list_installed_apps_in_chat',
33
+ mcp_category: 'teams_apps',
34
+ mcp_tier: :low,
35
+ idempotent: true,
36
+ inputs: { properties: { chat_id: { type: 'string' } }, required: ['chat_id'] },
37
+ trigger_words: %w[apps chat]
38
+
39
+ def list_installed_apps_in_chat(chat_id:, **)
40
+ response = graph_connection(**).get("chats/#{chat_id}/installedApps")
41
+ { result: response.body }
42
+ end
43
+
44
+ definition :install_app_for_user,
45
+ desc: 'Install a Teams app for a user',
46
+ mcp_prefix: 'teams.install_app_for_user',
47
+ mcp_category: 'teams_apps',
48
+ mcp_tier: :elevated,
49
+ idempotent: false,
50
+ inputs: { properties: { app_id: { type: 'string',
51
+ description: 'Teams app catalog ID' } },
52
+ required: ['app_id'] },
53
+ trigger_words: %w[install add app]
54
+
55
+ def install_app_for_user(app_id:, user_id: 'me', **)
56
+ payload = {
57
+ 'teamsApp@odata.bind' => "https://graph.microsoft.com/v1.0/appCatalogs/teamsApps/#{app_id}"
58
+ }
59
+ response = graph_connection(**).post("#{user_path(user_id)}/teamwork/installedApps", payload)
60
+ { result: response.body }
61
+ end
62
+
63
+ definition :uninstall_app_for_user,
64
+ desc: 'Uninstall a Teams app for a user',
65
+ mcp_prefix: 'teams.uninstall_app_for_user',
66
+ mcp_category: 'teams_apps',
67
+ mcp_tier: :elevated,
68
+ idempotent: false,
69
+ inputs: { properties: { installation_id: { type: 'string' } },
70
+ required: ['installation_id'] },
71
+ trigger_words: %w[uninstall remove]
72
+
73
+ def uninstall_app_for_user(installation_id:, user_id: 'me', **)
74
+ response = graph_connection(**).delete(
75
+ "#{user_path(user_id)}/teamwork/installedApps/#{installation_id}"
76
+ )
77
+ { result: response.body }
78
+ end
79
+
80
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers, false) &&
81
+ Legion::Extensions::Helpers.const_defined?(:Lex, false)
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -7,8 +7,12 @@ module Legion
7
7
  module MicrosoftTeams
8
8
  module Runners
9
9
  module Auth
10
+ extend Legion::Extensions::Definitions
10
11
  include Legion::Extensions::MicrosoftTeams::Helpers::Client
11
12
 
13
+ definition :acquire_token, mcp_exposed: false
14
+ definition :acquire_bot_token, mcp_exposed: false
15
+
12
16
  def acquire_token(tenant_id:, client_id:, client_secret:, scope: 'https://graph.microsoft.com/.default', **)
13
17
  response = oauth_connection(tenant_id: tenant_id).post('oauth2/v2.0/token', {
14
18
  grant_type: 'client_credentials',
@@ -30,113 +34,6 @@ module Legion
30
34
  { result: response.body }
31
35
  end
32
36
 
33
- def request_device_code(tenant_id:, client_id:,
34
- scope: 'OnlineMeetings.Read OnlineMeetingTranscript.Read.All offline_access', **)
35
- response = oauth_connection(tenant_id: tenant_id).post('oauth2/v2.0/devicecode', {
36
- client_id: client_id,
37
- scope: scope
38
- })
39
- { result: response.body }
40
- end
41
-
42
- def poll_device_code(tenant_id:, client_id:, device_code:, interval: 5, timeout: 300, **)
43
- conn = oauth_connection(tenant_id: tenant_id)
44
- deadline = Time.now + timeout
45
- current_interval = interval
46
-
47
- loop do
48
- response = conn.post('oauth2/v2.0/token', {
49
- grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
50
- client_id: client_id,
51
- device_code: device_code
52
- })
53
- body = response.body
54
-
55
- return { result: body } if body['access_token']
56
-
57
- case body['error']
58
- when 'authorization_pending'
59
- return { error: 'timeout', description: "Device code flow timed out after #{timeout}s" } if Time.now > deadline
60
-
61
- sleep(current_interval)
62
- when 'slow_down'
63
- current_interval += 5
64
- sleep(current_interval)
65
- else
66
- return { error: body['error'], description: body['error_description'] }
67
- end
68
- end
69
- end
70
-
71
- def authorize_url(tenant_id:, client_id:, redirect_uri:, scope:, state:,
72
- code_challenge:, code_challenge_method: 'S256', **)
73
- require 'uri'
74
- params = URI.encode_www_form(
75
- client_id: client_id,
76
- response_type: 'code',
77
- redirect_uri: redirect_uri,
78
- scope: scope,
79
- state: state,
80
- code_challenge: code_challenge,
81
- code_challenge_method: code_challenge_method
82
- )
83
- "https://login.microsoftonline.com/#{tenant_id}/oauth2/v2.0/authorize?#{params}"
84
- end
85
-
86
- def exchange_code(tenant_id:, client_id:, code:, redirect_uri:, code_verifier:,
87
- scope: 'OnlineMeetings.Read OnlineMeetingTranscript.Read.All offline_access', **)
88
- response = oauth_connection(tenant_id: tenant_id).post('oauth2/v2.0/token', {
89
- grant_type: 'authorization_code',
90
- client_id: client_id,
91
- code: code,
92
- redirect_uri: redirect_uri,
93
- code_verifier: code_verifier,
94
- scope: scope
95
- })
96
- { result: response.body }
97
- end
98
-
99
- def refresh_delegated_token(tenant_id:, client_id:, refresh_token:,
100
- scope: 'OnlineMeetings.Read OnlineMeetingTranscript.Read.All offline_access', **)
101
- response = oauth_connection(tenant_id: tenant_id).post('oauth2/v2.0/token', {
102
- grant_type: 'refresh_token',
103
- client_id: client_id,
104
- refresh_token: refresh_token,
105
- scope: scope
106
- })
107
- { result: response.body }
108
- end
109
-
110
- def auth_callback(code: nil, state: nil, **)
111
- unless code && state
112
- return {
113
- result: { error: 'missing_params' },
114
- response: { status: 400, content_type: 'text/html',
115
- body: '<html><body><h2>Missing code or state parameter</h2></body></html>' }
116
- }
117
- end
118
-
119
- Legion::Events.emit('microsoft_teams.oauth.callback', code: code, state: state) if defined?(Legion::Events)
120
-
121
- {
122
- result: { authenticated: true, code: code, state: state },
123
- response: { status: 200, content_type: 'text/html',
124
- body: callback_success_html }
125
- }
126
- end
127
- alias handle auth_callback
128
-
129
- private
130
-
131
- def callback_success_html
132
- <<~HTML
133
- <html><body style="font-family:sans-serif;text-align:center;padding:40px;">
134
- <h2>Authentication complete</h2>
135
- <p>You can close this window.</p>
136
- </body></html>
137
- HTML
138
- end
139
-
140
37
  include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers, false) &&
141
38
  Legion::Extensions::Helpers.const_defined?(:Lex, false)
142
39
  end
@@ -180,16 +180,18 @@ module Legion
180
180
  instructions = session[:system_prompt]
181
181
  instructions = "#{instructions}\n\n#{trace_context}" if trace_context && !trace_context.empty?
182
182
 
183
- response = llm_chat(
184
- text,
185
- instructions: instructions,
186
- model: config[:model],
187
- intent: config[:intent],
188
- caller: { id: session[:owner_id], extension: 'lex-microsoft_teams', mode: :bot_response }
183
+ response = Legion::LLM.chat( # rubocop:disable Legion/HelperMigration/DirectLlm
184
+ message: [
185
+ { role: 'system', content: instructions },
186
+ { role: 'user', content: text }
187
+ ],
188
+ model: config[:model],
189
+ intent: config[:intent],
190
+ caller: { id: session[:owner_id], extension: 'lex-microsoft_teams', mode: :bot_response }
189
191
  )
190
192
  response.content
191
193
  rescue StandardError => e
192
- log.error("LLM call failed: #{e.message}")
194
+ handle_exception(e, level: :error, operation: 'Bot#llm_respond')
193
195
  'I encountered an error processing your message. Please try again.'
194
196
  end
195
197
 
@@ -198,7 +200,7 @@ module Legion
198
200
 
199
201
  retrieve_context(message: message, owner_id: owner_id, chat_id: chat_id)
200
202
  rescue StandardError => e
201
- log.debug("retrieve_trace_context failed: #{e.message}") if defined?(log)
203
+ handle_exception(e, level: :debug, operation: 'Bot#retrieve_trace_context') if defined?(handle_exception)
202
204
  nil
203
205
  end
204
206
 
@@ -241,10 +243,16 @@ module Legion
241
243
  prompt = resolve_prompt(mode: :observe, conversation_id: nil)
242
244
  context = "#{from[:name] || peer_name} said: #{text}"
243
245
 
244
- response = llm_chat(context, instructions: prompt, caller: { id: owner_id, extension: 'lex-microsoft_teams', mode: :observe })
246
+ response = Legion::LLM.chat( # rubocop:disable Legion/HelperMigration/DirectLlm
247
+ message: [
248
+ { role: 'system', content: prompt },
249
+ { role: 'user', content: context }
250
+ ],
251
+ caller: { id: owner_id, extension: 'lex-microsoft_teams', mode: :observe }
252
+ )
245
253
  parse_extraction(response.content)
246
254
  rescue StandardError => e
247
- log.error("Observation extraction failed: #{e.message}")
255
+ handle_exception(e, level: :error, operation: 'Bot#extract_from_message')
248
256
  nil
249
257
  end
250
258
 
@@ -275,7 +283,7 @@ module Legion
275
283
  confidence: 0.6
276
284
  )
277
285
  rescue StandardError => e
278
- log.error("Observation store failed: #{e.message}")
286
+ handle_exception(e, level: :error, operation: 'Bot#store_observation')
279
287
  end
280
288
 
281
289
  def notify_owner(owner_id:, peer_name:, extraction: nil) # rubocop:disable Lint/UnusedMethodArgument
@@ -413,7 +421,7 @@ module Legion
413
421
  end
414
422
  nil
415
423
  rescue StandardError => e
416
- log.error("find_chat_with_person failed: #{e.message}")
424
+ handle_exception(e, level: :error, operation: 'Bot#find_chat_with_person', name: name)
417
425
  nil
418
426
  end
419
427
 
@@ -7,6 +7,10 @@ module Legion
7
7
  module MicrosoftTeams
8
8
  module Runners
9
9
  module CacheIngest
10
+ extend Legion::Extensions::Definitions
11
+
12
+ definition :ingest_cache, mcp_exposed: false
13
+
10
14
  # Ingest Teams messages from local cache into lex-memory traces.
11
15
  # Returns count of new traces stored and the latest compose_time seen.
12
16
  def ingest_cache(since: nil, skip_bots: true, db_path: nil, imprint_active: false, **)
@@ -82,7 +86,7 @@ module Legion
82
86
  imprint_active: imprint_active
83
87
  )
84
88
  rescue StandardError => e
85
- log.warn("CacheIngest: failed to store trace: #{e.message}")
89
+ handle_exception(e, level: :warn, operation: 'CacheIngest#store_message_trace')
86
90
  nil
87
91
  end
88
92
 
@@ -105,7 +109,7 @@ module Legion
105
109
  store = Legion::Extensions::Agentic::Memory::Trace.shared_store
106
110
  store.flush if store.respond_to?(:flush)
107
111
  rescue StandardError => e
108
- log.warn("CacheIngest: flush failed: #{e.message}")
112
+ handle_exception(e, level: :warn, operation: 'CacheIngest#flush_trace_store')
109
113
  end
110
114
 
111
115
  # Seed Hebbian coactivation links between messages in the same thread.
@@ -119,12 +123,12 @@ module Legion
119
123
  trace_ids.each_cons(2) do |id_a, id_b|
120
124
  store.record_coactivation(id_a, id_b)
121
125
  rescue StandardError => e
122
- log.debug("CacheIngest: coactivation link failed for #{id_a}/#{id_b}: #{e.message}")
123
- nil
126
+ handle_exception(e, level: :debug, operation: 'CacheIngest#coactivate_thread_traces',
127
+ id_a: id_a, id_b: id_b)
124
128
  end
125
129
  end
126
130
  rescue StandardError => e
127
- log.debug("CacheIngest: coactivation linking skipped: #{e.message}")
131
+ handle_exception(e, level: :debug, operation: 'CacheIngest#coactivate_thread_traces')
128
132
  end
129
133
  end
130
134
  end
@@ -0,0 +1,72 @@
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 CallEvents
10
+ include Legion::Extensions::MicrosoftTeams::Helpers::Client
11
+
12
+ def self.trigger_words
13
+ %w[call calls session sessions segment pstn]
14
+ end
15
+
16
+ definition :list_call_sessions,
17
+ desc: 'List sessions for a Teams call record',
18
+ mcp_prefix: 'teams.list_call_sessions',
19
+ mcp_category: 'teams_calls',
20
+ mcp_tier: :standard,
21
+ idempotent: true,
22
+ inputs: { properties: { call_id: { type: 'string' } }, required: ['call_id'] },
23
+ trigger_words: %w[sessions calls records]
24
+
25
+ def list_call_sessions(call_id:, **)
26
+ response = graph_connection(**).get("communications/callRecords/#{call_id}/sessions")
27
+ { result: response.body }
28
+ end
29
+
30
+ definition :get_call_session,
31
+ desc: 'Get a specific session from a Teams call record',
32
+ mcp_prefix: 'teams.get_call_session',
33
+ mcp_category: 'teams_calls',
34
+ mcp_tier: :standard,
35
+ idempotent: true,
36
+ inputs: { properties: { call_id: { type: 'string' },
37
+ session_id: { type: 'string' } },
38
+ required: %w[call_id session_id] },
39
+ trigger_words: %w[session call record]
40
+
41
+ def get_call_session(call_id:, session_id:, **)
42
+ response = graph_connection(**).get(
43
+ "communications/callRecords/#{call_id}/sessions/#{session_id}"
44
+ )
45
+ { result: response.body }
46
+ end
47
+
48
+ definition :list_session_segments,
49
+ desc: 'List segments for a session in a Teams call record',
50
+ mcp_prefix: 'teams.list_session_segments',
51
+ mcp_category: 'teams_calls',
52
+ mcp_tier: :standard,
53
+ idempotent: true,
54
+ inputs: { properties: { call_id: { type: 'string' },
55
+ session_id: { type: 'string' } },
56
+ required: %w[call_id session_id] },
57
+ trigger_words: %w[segments pstn]
58
+
59
+ def list_session_segments(call_id:, session_id:, **)
60
+ response = graph_connection(**).get(
61
+ "communications/callRecords/#{call_id}/sessions/#{session_id}/segments"
62
+ )
63
+ { result: response.body }
64
+ end
65
+
66
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers, false) &&
67
+ Legion::Extensions::Helpers.const_defined?(:Lex, false)
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end