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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/CHANGELOG.md +2 -0
- data/CLAUDE.md +29 -266
- data/lex-microsoft_teams.gemspec +1 -0
- data/lib/legion/extensions/microsoft_teams/absorbers/channel.rb +29 -17
- data/lib/legion/extensions/microsoft_teams/absorbers/chat.rb +20 -14
- data/lib/legion/extensions/microsoft_teams/absorbers/meeting.rb +21 -14
- data/lib/legion/extensions/microsoft_teams/actors/absorb_channel.rb +7 -4
- data/lib/legion/extensions/microsoft_teams/actors/absorb_chat.rb +7 -4
- data/lib/legion/extensions/microsoft_teams/actors/absorb_meeting.rb +7 -4
- data/lib/legion/extensions/microsoft_teams/actors/api_ingest.rb +13 -15
- data/lib/legion/extensions/microsoft_teams/actors/cache_bulk_ingest.rb +3 -3
- data/lib/legion/extensions/microsoft_teams/actors/cache_sync.rb +2 -1
- data/lib/legion/extensions/microsoft_teams/actors/channel_poller.rb +25 -16
- data/lib/legion/extensions/microsoft_teams/actors/direct_chat_poller.rb +16 -10
- data/lib/legion/extensions/microsoft_teams/actors/incremental_sync.rb +8 -8
- data/lib/legion/extensions/microsoft_teams/actors/meeting_ingest.rb +30 -22
- data/lib/legion/extensions/microsoft_teams/actors/observed_chat_poller.rb +14 -8
- data/lib/legion/extensions/microsoft_teams/actors/presence_poller.rb +14 -8
- data/lib/legion/extensions/microsoft_teams/actors/profile_ingest.rb +13 -16
- data/lib/legion/extensions/microsoft_teams/helpers/client.rb +10 -4
- data/lib/legion/extensions/microsoft_teams/helpers/high_water_mark.rb +3 -2
- data/lib/legion/extensions/microsoft_teams/helpers/prompt_resolver.rb +4 -1
- data/lib/legion/extensions/microsoft_teams/helpers/session_manager.rb +8 -2
- data/lib/legion/extensions/microsoft_teams/helpers/subscription_registry.rb +5 -3
- data/lib/legion/extensions/microsoft_teams/helpers/trace_retriever.rb +6 -1
- data/lib/legion/extensions/microsoft_teams/local_cache/extractor.rb +1 -1
- data/lib/legion/extensions/microsoft_teams/local_cache/sstable_reader.rb +2 -1
- data/lib/legion/extensions/microsoft_teams/runners/activities.rb +43 -0
- data/lib/legion/extensions/microsoft_teams/runners/ai_insights.rb +62 -0
- data/lib/legion/extensions/microsoft_teams/runners/api_ingest.rb +20 -14
- data/lib/legion/extensions/microsoft_teams/runners/app_installations.rb +86 -0
- data/lib/legion/extensions/microsoft_teams/runners/auth.rb +4 -107
- data/lib/legion/extensions/microsoft_teams/runners/bot.rb +20 -12
- data/lib/legion/extensions/microsoft_teams/runners/cache_ingest.rb +9 -5
- data/lib/legion/extensions/microsoft_teams/runners/call_events.rb +72 -0
- data/lib/legion/extensions/microsoft_teams/runners/channel_messages.rb +85 -0
- data/lib/legion/extensions/microsoft_teams/runners/channels.rb +69 -0
- data/lib/legion/extensions/microsoft_teams/runners/chats.rb +57 -0
- data/lib/legion/extensions/microsoft_teams/runners/files.rb +77 -0
- data/lib/legion/extensions/microsoft_teams/runners/local_cache.rb +4 -0
- data/lib/legion/extensions/microsoft_teams/runners/loop.rb +6 -0
- data/lib/legion/extensions/microsoft_teams/runners/meeting_artifacts.rb +54 -0
- data/lib/legion/extensions/microsoft_teams/runners/meetings.rb +92 -0
- data/lib/legion/extensions/microsoft_teams/runners/messages.rb +62 -0
- data/lib/legion/extensions/microsoft_teams/runners/ownership.rb +11 -0
- data/lib/legion/extensions/microsoft_teams/runners/people.rb +25 -0
- data/lib/legion/extensions/microsoft_teams/runners/presence.rb +14 -0
- data/lib/legion/extensions/microsoft_teams/runners/profile_ingest.rb +18 -4
- data/lib/legion/extensions/microsoft_teams/runners/subscriptions.rb +86 -0
- data/lib/legion/extensions/microsoft_teams/runners/teams.rb +30 -0
- data/lib/legion/extensions/microsoft_teams/runners/transcripts.rb +35 -0
- data/lib/legion/extensions/microsoft_teams/version.rb +1 -1
- data/lib/legion/extensions/microsoft_teams.rb +10 -3
- metadata +20 -8
- data/lib/legion/extensions/microsoft_teams/actors/auth_validator.rb +0 -123
- data/lib/legion/extensions/microsoft_teams/actors/token_refresher.rb +0 -122
- data/lib/legion/extensions/microsoft_teams/cli/auth.rb +0 -94
- data/lib/legion/extensions/microsoft_teams/helpers/browser_auth.rb +0 -270
- data/lib/legion/extensions/microsoft_teams/helpers/callback_server.rb +0 -90
- data/lib/legion/extensions/microsoft_teams/helpers/token_cache.rb +0 -412
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
300
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
123
|
-
|
|
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
|
-
|
|
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
|