lex-microsoft_teams 0.6.44 → 0.6.46
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 +10 -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 +42 -0
- data/lib/legion/extensions/microsoft_teams/runners/ai_insights.rb +63 -2
- data/lib/legion/extensions/microsoft_teams/runners/api_ingest.rb +19 -14
- data/lib/legion/extensions/microsoft_teams/runners/app_installations.rb +85 -0
- data/lib/legion/extensions/microsoft_teams/runners/auth.rb +3 -107
- data/lib/legion/extensions/microsoft_teams/runners/bot.rb +20 -12
- data/lib/legion/extensions/microsoft_teams/runners/cache_ingest.rb +7 -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 +68 -0
- data/lib/legion/extensions/microsoft_teams/runners/chats.rb +56 -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 +5 -0
- data/lib/legion/extensions/microsoft_teams/runners/meeting_artifacts.rb +54 -0
- data/lib/legion/extensions/microsoft_teams/runners/meetings.rb +112 -0
- data/lib/legion/extensions/microsoft_teams/runners/messages.rb +61 -0
- data/lib/legion/extensions/microsoft_teams/runners/ownership.rb +11 -0
- data/lib/legion/extensions/microsoft_teams/runners/people.rb +24 -0
- data/lib/legion/extensions/microsoft_teams/runners/presence.rb +14 -0
- data/lib/legion/extensions/microsoft_teams/runners/profile_ingest.rb +17 -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
|
@@ -9,31 +9,92 @@ module Legion
|
|
|
9
9
|
module AiInsights
|
|
10
10
|
include Legion::Extensions::MicrosoftTeams::Helpers::Client
|
|
11
11
|
|
|
12
|
+
def self.trigger_words
|
|
13
|
+
%w[insight insights summary callrecord recorded]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
definition :list_meeting_ai_insights,
|
|
17
|
+
desc: 'List AI-generated insights for an online meeting',
|
|
18
|
+
mcp_prefix: 'teams.list_meeting_ai_insights',
|
|
19
|
+
mcp_category: 'teams_meetings',
|
|
20
|
+
mcp_tier: :standard,
|
|
21
|
+
idempotent: true,
|
|
22
|
+
inputs: { properties: { meeting_id: { type: 'string' } }, required: ['meeting_id'] },
|
|
23
|
+
trigger_words: %w[insights summary ai]
|
|
24
|
+
|
|
12
25
|
def list_meeting_ai_insights(meeting_id:, user_id: 'me', **)
|
|
13
26
|
response = graph_connection(**).get("#{user_path(user_id)}/onlineMeetings/#{meeting_id}/aiInsights")
|
|
14
27
|
{ result: response.body }
|
|
15
28
|
end
|
|
16
29
|
|
|
30
|
+
definition :get_meeting_ai_insight,
|
|
31
|
+
desc: 'Get a specific AI insight for an online meeting',
|
|
32
|
+
mcp_prefix: 'teams.get_meeting_ai_insight',
|
|
33
|
+
mcp_category: 'teams_meetings',
|
|
34
|
+
mcp_tier: :standard,
|
|
35
|
+
idempotent: true,
|
|
36
|
+
inputs: { properties: { meeting_id: { type: 'string' },
|
|
37
|
+
insight_id: { type: 'string' } },
|
|
38
|
+
required: %w[meeting_id insight_id] },
|
|
39
|
+
trigger_words: %w[insight details]
|
|
40
|
+
|
|
17
41
|
def get_meeting_ai_insight(meeting_id:, insight_id:, user_id: 'me', **)
|
|
18
42
|
response = graph_connection(**).get("#{user_path(user_id)}/onlineMeetings/#{meeting_id}/aiInsights/#{insight_id}")
|
|
19
43
|
{ result: response.body }
|
|
20
44
|
end
|
|
21
45
|
|
|
46
|
+
definition :list_meeting_recordings,
|
|
47
|
+
desc: 'List recordings for an online meeting',
|
|
48
|
+
mcp_prefix: 'teams.list_meeting_recordings',
|
|
49
|
+
mcp_category: 'teams_meetings',
|
|
50
|
+
mcp_tier: :standard,
|
|
51
|
+
idempotent: true,
|
|
52
|
+
inputs: { properties: { meeting_id: { type: 'string' } }, required: ['meeting_id'] },
|
|
53
|
+
trigger_words: %w[recordings recorded]
|
|
54
|
+
|
|
22
55
|
def list_meeting_recordings(meeting_id:, user_id: 'me', **)
|
|
23
56
|
response = graph_connection(**).get("#{user_path(user_id)}/onlineMeetings/#{meeting_id}/recordings")
|
|
24
57
|
{ result: response.body }
|
|
25
58
|
end
|
|
26
59
|
|
|
60
|
+
definition :get_meeting_recording,
|
|
61
|
+
desc: 'Get a specific recording for an online meeting',
|
|
62
|
+
mcp_prefix: 'teams.get_meeting_recording',
|
|
63
|
+
mcp_category: 'teams_meetings',
|
|
64
|
+
mcp_tier: :standard,
|
|
65
|
+
idempotent: true,
|
|
66
|
+
inputs: { properties: { meeting_id: { type: 'string' },
|
|
67
|
+
recording_id: { type: 'string' } },
|
|
68
|
+
required: %w[meeting_id recording_id] },
|
|
69
|
+
trigger_words: %w[recording download]
|
|
70
|
+
|
|
27
71
|
def get_meeting_recording(meeting_id:, recording_id:, user_id: 'me', **)
|
|
28
72
|
response = graph_connection(**).get("#{user_path(user_id)}/onlineMeetings/#{meeting_id}/recordings/#{recording_id}")
|
|
29
73
|
{ result: response.body }
|
|
30
74
|
end
|
|
31
75
|
|
|
32
|
-
|
|
33
|
-
|
|
76
|
+
definition :list_call_records,
|
|
77
|
+
desc: 'List Teams call records from communications API',
|
|
78
|
+
mcp_prefix: 'teams.list_call_records',
|
|
79
|
+
mcp_category: 'teams_meetings',
|
|
80
|
+
mcp_tier: :standard,
|
|
81
|
+
idempotent: true,
|
|
82
|
+
trigger_words: %w[records calls history]
|
|
83
|
+
|
|
84
|
+
def list_call_records(**)
|
|
85
|
+
response = graph_connection(**).get('communications/callRecords')
|
|
34
86
|
{ result: response.body }
|
|
35
87
|
end
|
|
36
88
|
|
|
89
|
+
definition :get_call_record,
|
|
90
|
+
desc: 'Get a specific Teams call record',
|
|
91
|
+
mcp_prefix: 'teams.get_call_record',
|
|
92
|
+
mcp_category: 'teams_meetings',
|
|
93
|
+
mcp_tier: :standard,
|
|
94
|
+
idempotent: true,
|
|
95
|
+
inputs: { properties: { call_id: { type: 'string' } }, required: ['call_id'] },
|
|
96
|
+
trigger_words: %w[callrecord record]
|
|
97
|
+
|
|
37
98
|
def get_call_record(call_id:, **)
|
|
38
99
|
response = graph_connection(**).get("communications/callRecords/#{call_id}")
|
|
39
100
|
{ result: response.body }
|
|
@@ -16,12 +16,15 @@ module Legion
|
|
|
16
16
|
include Helpers::HighWaterMark
|
|
17
17
|
extend self
|
|
18
18
|
|
|
19
|
+
definition :ingest_api, mcp_exposed: false
|
|
20
|
+
|
|
19
21
|
# Fetch top contacts via /me/people, then pull recent messages from
|
|
20
22
|
# their 1:1 chats. Stores each message as an individual memory trace
|
|
21
23
|
# (same format as CacheIngest) with dedup by content hash.
|
|
22
24
|
#
|
|
23
25
|
# Requires a delegated token with Chat.Read and People.Read scopes.
|
|
24
26
|
def ingest_api(token:, top_people: 15, message_depth: 50, skip_bots: true, imprint_active: false, **) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
|
|
27
|
+
log.debug("ApiIngest#ingest_api top_people=#{top_people} message_depth=#{message_depth}")
|
|
25
28
|
return error_result('lex-memory not loaded') unless memory_available?
|
|
26
29
|
return error_result('no token provided') unless token && !token.empty?
|
|
27
30
|
|
|
@@ -98,8 +101,7 @@ module Legion
|
|
|
98
101
|
people_found: people.length, chats_found: chats.length,
|
|
99
102
|
apollo: apollo_results } }
|
|
100
103
|
rescue StandardError => e
|
|
101
|
-
|
|
102
|
-
log.error(log_msg)
|
|
104
|
+
handle_exception(e, level: :error, operation: 'ApiIngest#ingest_api')
|
|
103
105
|
{ result: { stored: stored || 0, skipped: skipped || 0, error: e.message } }
|
|
104
106
|
end
|
|
105
107
|
|
|
@@ -111,6 +113,7 @@ module Legion
|
|
|
111
113
|
private
|
|
112
114
|
|
|
113
115
|
def fetch_top_people(token:, top:)
|
|
116
|
+
log.debug("ApiIngest#fetch_top_people top=#{top}")
|
|
114
117
|
return [] if permission_denied?('/me/people')
|
|
115
118
|
|
|
116
119
|
conn = graph_connection(token: token)
|
|
@@ -125,7 +128,7 @@ module Legion
|
|
|
125
128
|
people = (resp.body || {}).fetch('value', [])
|
|
126
129
|
people.sort_by { |p| -(p.dig('scoredEmailAddresses', 0, 'relevanceScore') || 0) }
|
|
127
130
|
rescue StandardError => e
|
|
128
|
-
|
|
131
|
+
handle_exception(e, level: :warn, operation: 'ApiIngest#fetch_top_people')
|
|
129
132
|
[]
|
|
130
133
|
end
|
|
131
134
|
|
|
@@ -156,7 +159,7 @@ module Legion
|
|
|
156
159
|
log.info("ApiIngest: fetched #{all_chats.size} chats (#{pages} pages), #{filtered.size} eligible (1:1/group/meeting)")
|
|
157
160
|
filtered
|
|
158
161
|
rescue StandardError => e
|
|
159
|
-
|
|
162
|
+
handle_exception(e, level: :warn, operation: 'ApiIngest#fetch_one_on_one_chats')
|
|
160
163
|
[]
|
|
161
164
|
end
|
|
162
165
|
|
|
@@ -174,7 +177,7 @@ module Legion
|
|
|
174
177
|
end
|
|
175
178
|
end
|
|
176
179
|
rescue StandardError => e
|
|
177
|
-
|
|
180
|
+
handle_exception(e, level: :debug, operation: 'ApiIngest#match_chat_to_person')
|
|
178
181
|
nil
|
|
179
182
|
end
|
|
180
183
|
|
|
@@ -198,7 +201,8 @@ module Legion
|
|
|
198
201
|
log.debug("ApiIngest: fetch_messages chat=#{chat_id} count=#{(resp.body || {}).fetch('value', []).size}")
|
|
199
202
|
(resp.body || {}).fetch('value', [])
|
|
200
203
|
rescue StandardError => e
|
|
201
|
-
|
|
204
|
+
handle_exception(e, level: :warn, operation: 'ApiIngest#fetch_chat_messages',
|
|
205
|
+
chat_id: chat_id)
|
|
202
206
|
[]
|
|
203
207
|
end
|
|
204
208
|
|
|
@@ -242,7 +246,7 @@ module Legion
|
|
|
242
246
|
imprint_active: imprint_active
|
|
243
247
|
)
|
|
244
248
|
rescue StandardError => e
|
|
245
|
-
|
|
249
|
+
handle_exception(e, level: :warn, operation: 'ApiIngest#store_graph_message')
|
|
246
250
|
nil
|
|
247
251
|
end
|
|
248
252
|
|
|
@@ -267,7 +271,7 @@ module Legion
|
|
|
267
271
|
end
|
|
268
272
|
hashes
|
|
269
273
|
rescue StandardError => e
|
|
270
|
-
|
|
274
|
+
handle_exception(e, level: :debug, operation: 'ApiIngest#load_existing_hashes')
|
|
271
275
|
Set.new
|
|
272
276
|
end
|
|
273
277
|
|
|
@@ -283,7 +287,7 @@ module Legion
|
|
|
283
287
|
store = Legion::Extensions::Agentic::Memory::Trace.shared_store
|
|
284
288
|
store.flush if store.respond_to?(:flush)
|
|
285
289
|
rescue StandardError => e
|
|
286
|
-
|
|
290
|
+
handle_exception(e, level: :warn, operation: 'ApiIngest#flush_trace_store')
|
|
287
291
|
end
|
|
288
292
|
|
|
289
293
|
def coactivate_thread_traces(thread_groups)
|
|
@@ -296,12 +300,12 @@ module Legion
|
|
|
296
300
|
trace_ids.each_cons(2) do |id_a, id_b|
|
|
297
301
|
store.record_coactivation(id_a, id_b)
|
|
298
302
|
rescue StandardError => e
|
|
299
|
-
|
|
300
|
-
|
|
303
|
+
handle_exception(e, level: :debug, operation: 'ApiIngest#coactivate_thread_traces',
|
|
304
|
+
id_a: id_a, id_b: id_b)
|
|
301
305
|
end
|
|
302
306
|
end
|
|
303
307
|
rescue StandardError => e
|
|
304
|
-
|
|
308
|
+
handle_exception(e, level: :debug, operation: 'ApiIngest#coactivate_thread_traces')
|
|
305
309
|
end
|
|
306
310
|
|
|
307
311
|
def publish_to_apollo(person_texts)
|
|
@@ -332,7 +336,7 @@ module Legion
|
|
|
332
336
|
|
|
333
337
|
{ ingested: ingested, entities_found: entities_found }
|
|
334
338
|
rescue StandardError => e
|
|
335
|
-
|
|
339
|
+
handle_exception(e, level: :warn, operation: 'ApiIngest#publish_to_apollo')
|
|
336
340
|
{ skipped: true, reason: :error, error: e.message }
|
|
337
341
|
end
|
|
338
342
|
|
|
@@ -358,7 +362,8 @@ module Legion
|
|
|
358
362
|
|
|
359
363
|
{ success: true, count: result[:entities].length }
|
|
360
364
|
rescue StandardError => e
|
|
361
|
-
|
|
365
|
+
handle_exception(e, level: :debug, operation: 'ApiIngest#extract_and_ingest_entities',
|
|
366
|
+
person_name: person_name)
|
|
362
367
|
{ success: false, count: 0 }
|
|
363
368
|
end
|
|
364
369
|
|
|
@@ -0,0 +1,85 @@
|
|
|
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
|
+
include Legion::Extensions::MicrosoftTeams::Helpers::Client
|
|
11
|
+
|
|
12
|
+
def self.trigger_words
|
|
13
|
+
%w[app apps addon addons installation installed]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
definition :list_installed_apps_for_user,
|
|
17
|
+
desc: 'List Teams apps installed for a user',
|
|
18
|
+
mcp_prefix: 'teams.list_installed_apps_for_user',
|
|
19
|
+
mcp_category: 'teams_apps',
|
|
20
|
+
mcp_tier: :low,
|
|
21
|
+
idempotent: true,
|
|
22
|
+
trigger_words: %w[apps installed]
|
|
23
|
+
|
|
24
|
+
def list_installed_apps_for_user(user_id: 'me', **)
|
|
25
|
+
response = graph_connection(**).get("#{user_path(user_id)}/teamwork/installedApps")
|
|
26
|
+
{ result: response.body }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
definition :list_installed_apps_in_chat,
|
|
30
|
+
desc: 'List Teams apps installed in a specific chat',
|
|
31
|
+
mcp_prefix: 'teams.list_installed_apps_in_chat',
|
|
32
|
+
mcp_category: 'teams_apps',
|
|
33
|
+
mcp_tier: :low,
|
|
34
|
+
idempotent: true,
|
|
35
|
+
inputs: { properties: { chat_id: { type: 'string' } }, required: ['chat_id'] },
|
|
36
|
+
trigger_words: %w[apps chat]
|
|
37
|
+
|
|
38
|
+
def list_installed_apps_in_chat(chat_id:, **)
|
|
39
|
+
response = graph_connection(**).get("chats/#{chat_id}/installedApps")
|
|
40
|
+
{ result: response.body }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
definition :install_app_for_user,
|
|
44
|
+
desc: 'Install a Teams app for a user',
|
|
45
|
+
mcp_prefix: 'teams.install_app_for_user',
|
|
46
|
+
mcp_category: 'teams_apps',
|
|
47
|
+
mcp_tier: :elevated,
|
|
48
|
+
idempotent: false,
|
|
49
|
+
inputs: { properties: { app_id: { type: 'string',
|
|
50
|
+
description: 'Teams app catalog ID' } },
|
|
51
|
+
required: ['app_id'] },
|
|
52
|
+
trigger_words: %w[install add app]
|
|
53
|
+
|
|
54
|
+
def install_app_for_user(app_id:, user_id: 'me', **)
|
|
55
|
+
payload = {
|
|
56
|
+
'teamsApp@odata.bind' => "https://graph.microsoft.com/v1.0/appCatalogs/teamsApps/#{app_id}"
|
|
57
|
+
}
|
|
58
|
+
response = graph_connection(**).post("#{user_path(user_id)}/teamwork/installedApps", payload)
|
|
59
|
+
{ result: response.body }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
definition :uninstall_app_for_user,
|
|
63
|
+
desc: 'Uninstall a Teams app for a user',
|
|
64
|
+
mcp_prefix: 'teams.uninstall_app_for_user',
|
|
65
|
+
mcp_category: 'teams_apps',
|
|
66
|
+
mcp_tier: :elevated,
|
|
67
|
+
idempotent: false,
|
|
68
|
+
inputs: { properties: { installation_id: { type: 'string' } },
|
|
69
|
+
required: ['installation_id'] },
|
|
70
|
+
trigger_words: %w[uninstall remove]
|
|
71
|
+
|
|
72
|
+
def uninstall_app_for_user(installation_id:, user_id: 'me', **)
|
|
73
|
+
response = graph_connection(**).delete(
|
|
74
|
+
"#{user_path(user_id)}/teamwork/installedApps/#{installation_id}"
|
|
75
|
+
)
|
|
76
|
+
{ result: response.body }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers, false) &&
|
|
80
|
+
Legion::Extensions::Helpers.const_defined?(:Lex, false)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -9,6 +9,9 @@ module Legion
|
|
|
9
9
|
module Auth
|
|
10
10
|
include Legion::Extensions::MicrosoftTeams::Helpers::Client
|
|
11
11
|
|
|
12
|
+
definition :acquire_token, mcp_exposed: false
|
|
13
|
+
definition :acquire_bot_token, mcp_exposed: false
|
|
14
|
+
|
|
12
15
|
def acquire_token(tenant_id:, client_id:, client_secret:, scope: 'https://graph.microsoft.com/.default', **)
|
|
13
16
|
response = oauth_connection(tenant_id: tenant_id).post('oauth2/v2.0/token', {
|
|
14
17
|
grant_type: 'client_credentials',
|
|
@@ -30,113 +33,6 @@ module Legion
|
|
|
30
33
|
{ result: response.body }
|
|
31
34
|
end
|
|
32
35
|
|
|
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
36
|
include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers, false) &&
|
|
141
37
|
Legion::Extensions::Helpers.const_defined?(:Lex, false)
|
|
142
38
|
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,8 @@ module Legion
|
|
|
7
7
|
module MicrosoftTeams
|
|
8
8
|
module Runners
|
|
9
9
|
module CacheIngest
|
|
10
|
+
definition :ingest_cache, mcp_exposed: false
|
|
11
|
+
|
|
10
12
|
# Ingest Teams messages from local cache into lex-memory traces.
|
|
11
13
|
# Returns count of new traces stored and the latest compose_time seen.
|
|
12
14
|
def ingest_cache(since: nil, skip_bots: true, db_path: nil, imprint_active: false, **)
|
|
@@ -82,7 +84,7 @@ module Legion
|
|
|
82
84
|
imprint_active: imprint_active
|
|
83
85
|
)
|
|
84
86
|
rescue StandardError => e
|
|
85
|
-
|
|
87
|
+
handle_exception(e, level: :warn, operation: 'CacheIngest#store_message_trace')
|
|
86
88
|
nil
|
|
87
89
|
end
|
|
88
90
|
|
|
@@ -105,7 +107,7 @@ module Legion
|
|
|
105
107
|
store = Legion::Extensions::Agentic::Memory::Trace.shared_store
|
|
106
108
|
store.flush if store.respond_to?(:flush)
|
|
107
109
|
rescue StandardError => e
|
|
108
|
-
|
|
110
|
+
handle_exception(e, level: :warn, operation: 'CacheIngest#flush_trace_store')
|
|
109
111
|
end
|
|
110
112
|
|
|
111
113
|
# Seed Hebbian coactivation links between messages in the same thread.
|
|
@@ -119,12 +121,12 @@ module Legion
|
|
|
119
121
|
trace_ids.each_cons(2) do |id_a, id_b|
|
|
120
122
|
store.record_coactivation(id_a, id_b)
|
|
121
123
|
rescue StandardError => e
|
|
122
|
-
|
|
123
|
-
|
|
124
|
+
handle_exception(e, level: :debug, operation: 'CacheIngest#coactivate_thread_traces',
|
|
125
|
+
id_a: id_a, id_b: id_b)
|
|
124
126
|
end
|
|
125
127
|
end
|
|
126
128
|
rescue StandardError => e
|
|
127
|
-
|
|
129
|
+
handle_exception(e, level: :debug, operation: 'CacheIngest#coactivate_thread_traces')
|
|
128
130
|
end
|
|
129
131
|
end
|
|
130
132
|
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
|