lex-microsoft_teams 0.6.49 → 0.6.51

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 (33) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +35 -0
  3. data/lib/legion/extensions/microsoft_teams/actors/api_ingest.rb +8 -13
  4. data/lib/legion/extensions/microsoft_teams/actors/channel_poller.rb +5 -18
  5. data/lib/legion/extensions/microsoft_teams/actors/direct_chat_poller.rb +4 -13
  6. data/lib/legion/extensions/microsoft_teams/actors/incremental_sync.rb +6 -17
  7. data/lib/legion/extensions/microsoft_teams/actors/meeting_ingest.rb +3 -10
  8. data/lib/legion/extensions/microsoft_teams/actors/observed_chat_poller.rb +4 -14
  9. data/lib/legion/extensions/microsoft_teams/actors/presence_poller.rb +3 -6
  10. data/lib/legion/extensions/microsoft_teams/actors/profile_ingest.rb +5 -10
  11. data/lib/legion/extensions/microsoft_teams/errors.rb +84 -0
  12. data/lib/legion/extensions/microsoft_teams/faraday/retry_after.rb +209 -0
  13. data/lib/legion/extensions/microsoft_teams/faraday/throttle_circuit.rb +150 -0
  14. data/lib/legion/extensions/microsoft_teams/helpers/client.rb +80 -3
  15. data/lib/legion/extensions/microsoft_teams/helpers/graph_cache.rb +65 -0
  16. data/lib/legion/extensions/microsoft_teams/helpers/graph_client.rb +20 -0
  17. data/lib/legion/extensions/microsoft_teams/runners/api_ingest.rb +34 -22
  18. data/lib/legion/extensions/microsoft_teams/runners/app_installations.rb +17 -7
  19. data/lib/legion/extensions/microsoft_teams/runners/call_events.rb +70 -11
  20. data/lib/legion/extensions/microsoft_teams/runners/channel_messages.rb +67 -12
  21. data/lib/legion/extensions/microsoft_teams/runners/channels.rb +12 -4
  22. data/lib/legion/extensions/microsoft_teams/runners/chats.rb +42 -6
  23. data/lib/legion/extensions/microsoft_teams/runners/files.rb +72 -9
  24. data/lib/legion/extensions/microsoft_teams/runners/meeting_artifacts.rb +32 -5
  25. data/lib/legion/extensions/microsoft_teams/runners/meetings.rb +34 -4
  26. data/lib/legion/extensions/microsoft_teams/runners/messages.rb +73 -15
  27. data/lib/legion/extensions/microsoft_teams/runners/people.rb +14 -2
  28. data/lib/legion/extensions/microsoft_teams/runners/profile_ingest.rb +23 -11
  29. data/lib/legion/extensions/microsoft_teams/runners/teams.rb +46 -8
  30. data/lib/legion/extensions/microsoft_teams/runners/transcripts.rb +35 -6
  31. data/lib/legion/extensions/microsoft_teams/version.rb +1 -1
  32. data/lib/legion/extensions/microsoft_teams.rb +59 -0
  33. metadata +5 -1
@@ -15,21 +15,53 @@ module Legion
15
15
  end
16
16
 
17
17
  definition :list_chat_messages,
18
- desc: 'List messages in a Teams chat thread',
18
+ desc: 'List messages in a Teams chat thread with pagination, ordering, and filtering',
19
19
  mcp_prefix: 'teams.list_chat_messages',
20
20
  mcp_category: 'teams_messages',
21
21
  mcp_tier: :standard,
22
22
  idempotent: true,
23
- inputs: { properties: { chat_id: { type: 'string',
24
- description: 'Teams chat ID' } },
23
+ inputs: { properties: { chat_id: { type: 'string',
24
+ description: 'Teams chat ID' },
25
+ top: { type: 'integer',
26
+ description: 'Messages per page (default 50, max 50)' },
27
+ max_pages: { type: 'integer',
28
+ description: 'Maximum pages to fetch (default 1)' },
29
+ orderby: { type: 'string',
30
+ description: 'Sort order: lastModifiedDateTime desc or createdDateTime desc' },
31
+ filter: { type: 'string',
32
+ description: 'OData $filter on lastModifiedDateTime or createdDateTime' } },
25
33
  required: ['chat_id'] },
26
34
  trigger_words: %w[messages history read]
27
35
 
28
- def list_chat_messages(chat_id:, top: 50, **)
29
- log.debug "list_chat_messages(chat_id: #{chat_id}, top: #{top})"
30
- params = { '$top' => top }
31
- response = graph_connection(**).get("chats/#{chat_id}/messages", params)
32
- { result: response.body }
36
+ def list_chat_messages(chat_id:, top: 50, max_pages: 1, orderby: nil, filter: nil, **)
37
+ log.debug "list_chat_messages(chat_id: #{chat_id}, top: #{top}, max_pages: #{max_pages})"
38
+ per_page = [top, 50].min
39
+ params = { '$top' => per_page }
40
+ params['$orderby'] = orderby if orderby
41
+ params['$filter'] = filter if filter
42
+ conn = graph_connection(**)
43
+ response = conn.get("chats/#{chat_id}/messages", params)
44
+ body = response.body
45
+
46
+ return { result: body } if max_pages <= 1
47
+
48
+ all_values = Array(body['value'] || body[:value])
49
+ next_link = body['@odata.nextLink'] || body[:'@odata.nextLink']
50
+ pages_fetched = 1
51
+
52
+ while next_link && pages_fetched < max_pages
53
+ response = conn.get(next_link)
54
+ page_body = response.body
55
+ items = page_body['value'] || page_body[:value]
56
+ all_values.concat(Array(items)) if items
57
+ next_link = page_body['@odata.nextLink'] || page_body[:'@odata.nextLink']
58
+ pages_fetched += 1
59
+ end
60
+
61
+ result = { '@odata.context' => body['@odata.context'] || body[:'@odata.context'],
62
+ 'value' => all_values }
63
+ result['@odata.nextLink'] = next_link if next_link
64
+ { result: result }
33
65
  end
34
66
 
35
67
  definition :get_chat_message,
@@ -89,21 +121,47 @@ module Legion
89
121
  end
90
122
 
91
123
  definition :list_message_replies,
92
- desc: 'List replies to a message in a Teams chat',
124
+ desc: 'List replies to a message in a Teams chat with pagination support',
93
125
  mcp_prefix: 'teams.list_message_replies',
94
126
  mcp_category: 'teams_messages',
95
127
  mcp_tier: :standard,
96
128
  idempotent: true,
97
129
  inputs: { properties: { chat_id: { type: 'string' },
98
- message_id: { type: 'string' } },
130
+ message_id: { type: 'string' },
131
+ top: { type: 'integer',
132
+ description: 'Number of replies to return per page (default 50)' },
133
+ max_pages: { type: 'integer',
134
+ description: 'Maximum pages to fetch (default 1)' } },
99
135
  required: %w[chat_id message_id] },
100
136
  trigger_words: %w[replies thread]
101
137
 
102
- def list_message_replies(chat_id:, message_id:, top: 50, **)
103
- log.debug "list_message_replies(chat_id: #{chat_id}, message_id: #{message_id}, top: #{top})"
104
- params = { '$top' => top }
105
- response = graph_connection(**).get("chats/#{chat_id}/messages/#{message_id}/replies", params)
106
- { result: response.body }
138
+ def list_message_replies(chat_id:, message_id:, top: 50, max_pages: 1, **)
139
+ log.debug "list_message_replies(chat_id: #{chat_id}, message_id: #{message_id}, top: #{top}, max_pages: #{max_pages})"
140
+ per_page = [top, 50].min
141
+ params = { '$top' => per_page }
142
+ conn = graph_connection(**)
143
+ response = conn.get("chats/#{chat_id}/messages/#{message_id}/replies", params)
144
+ body = response.body
145
+
146
+ return { result: body } if max_pages <= 1
147
+
148
+ all_values = Array(body['value'] || body[:value])
149
+ next_link = body['@odata.nextLink'] || body[:'@odata.nextLink']
150
+ pages_fetched = 1
151
+
152
+ while next_link && pages_fetched < max_pages
153
+ response = conn.get(next_link)
154
+ page_body = response.body
155
+ items = page_body['value'] || page_body[:value]
156
+ all_values.concat(Array(items)) if items
157
+ next_link = page_body['@odata.nextLink'] || page_body[:'@odata.nextLink']
158
+ pages_fetched += 1
159
+ end
160
+
161
+ result = { '@odata.context' => body['@odata.context'] || body[:'@odata.context'],
162
+ 'value' => all_values }
163
+ result['@odata.nextLink'] = next_link if next_link
164
+ { result: result }
107
165
  end
108
166
 
109
167
  include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers, false) &&
@@ -32,16 +32,28 @@ module Legion
32
32
  end
33
33
 
34
34
  definition :list_people,
35
- desc: 'List people relevant to the current user (colleagues, contacts)',
35
+ desc: 'List people relevant to the current user with search and filter support',
36
36
  mcp_prefix: 'teams.list_people',
37
37
  mcp_category: 'teams_people',
38
38
  mcp_tier: :standard,
39
39
  idempotent: true,
40
+ inputs: { properties: { top: { type: 'integer',
41
+ description: 'Number of people to return (default 25)' },
42
+ search: { type: 'string',
43
+ description: 'Search term to find people by name or email' },
44
+ filter: { type: 'string',
45
+ description: 'OData $filter expression' },
46
+ select: { type: 'string',
47
+ description: 'Comma-separated fields to return' } },
48
+ required: [] },
40
49
  trigger_words: %w[people colleagues contacts]
41
50
 
42
- def list_people(user_id: 'me', top: 25, **)
51
+ def list_people(user_id: 'me', top: 25, search: nil, filter: nil, select: nil, **)
43
52
  log.debug("People#list_people user_id=#{user_id} top=#{top}")
44
53
  params = { '$top' => top }
54
+ params['$search'] = "\"#{search}\"" if search
55
+ params['$filter'] = filter if filter
56
+ params['$select'] = select if select
45
57
  response = graph_connection(**).get("#{user_path(user_id)}/people", params)
46
58
  { result: response.body }
47
59
  rescue StandardError => e
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'json'
4
4
  require 'legion/extensions/microsoft_teams/helpers/client'
5
+ require 'legion/extensions/microsoft_teams/helpers/graph_cache'
5
6
  require 'legion/extensions/microsoft_teams/helpers/permission_guard'
6
7
  require 'legion/extensions/microsoft_teams/helpers/high_water_mark'
7
8
  require 'legion/extensions/microsoft_teams/helpers/transform_definitions'
@@ -15,6 +16,7 @@ module Legion
15
16
  include Helpers::Client
16
17
  include Helpers::PermissionGuard
17
18
  include Helpers::HighWaterMark
19
+ include Helpers::GraphCache
18
20
  extend self
19
21
 
20
22
  definition :full_ingest, mcp_exposed: false
@@ -106,15 +108,19 @@ module Legion
106
108
  return { ingested: 0 } if people.empty?
107
109
 
108
110
  conn = graph_connection(token: token)
109
- chats_resp = conn.get('me/chats', { '$top' => 50 })
110
- chats = (chats_resp.body || {}).fetch('value', [])
111
+ chats = cached_graph_get(conn: conn, path: 'me/chats',
112
+ params: { '$top' => 50 })
113
+ .then { |body| (body || {}).fetch('value', []) }
114
+ one_on_ones = chats.select { |c| c['chatType'] == 'oneOnOne' }
115
+
116
+ email_to_chat = build_chat_member_index(conn: conn, chats: one_on_ones)
111
117
  ingested = 0
112
118
 
113
119
  people.first(top_people).each do |person|
114
- email = person.dig('scoredEmailAddresses', 0, 'address')
120
+ email = person.dig('scoredEmailAddresses', 0, 'address')&.downcase
115
121
  next unless email
116
122
 
117
- chat = find_chat_for_person(chats: chats, email: email, conn: conn)
123
+ chat = email_to_chat[email]
118
124
  next unless chat
119
125
 
120
126
  messages = fetch_new_messages(conn: conn, chat_id: chat['id'], depth: message_depth)
@@ -206,15 +212,21 @@ module Legion
206
212
 
207
213
  private
208
214
 
209
- def find_chat_for_person(chats:, email:, conn:)
210
- chats.select { |c| c['chatType'] == 'oneOnOne' }.find do |chat|
211
- members_resp = conn.get("chats/#{chat['id']}/members")
212
- members = (members_resp.body || {}).fetch('value', [])
213
- members.any? { |m| m['email']&.downcase == email.downcase }
215
+ def build_chat_member_index(conn:, chats:)
216
+ index = {}
217
+ chats.each do |chat|
218
+ members = cached_graph_get(conn: conn, path: "chats/#{chat['id']}/members",
219
+ shared: true)
220
+ .then { |body| (body || {}).fetch('value', []) }
221
+ members.each do |m|
222
+ email = m['email']&.downcase
223
+ index[email] = chat if email && !index.key?(email)
224
+ end
214
225
  end
226
+ index
215
227
  rescue StandardError => e
216
- handle_exception(e, level: :debug, operation: 'ProfileIngest#find_chat_for_person')
217
- nil
228
+ handle_exception(e, level: :warn, operation: 'ProfileIngest#build_chat_member_index')
229
+ {}
218
230
  end
219
231
 
220
232
  def fetch_new_messages(conn:, chat_id:, depth: 50)
@@ -15,15 +15,23 @@ module Legion
15
15
  end
16
16
 
17
17
  definition :list_joined_teams,
18
- desc: 'List Teams the current user has joined',
18
+ desc: 'List Teams the current user has joined with optional filtering and select',
19
19
  mcp_prefix: 'teams.list_joined_teams',
20
20
  mcp_category: 'teams_teams',
21
21
  mcp_tier: :low,
22
22
  idempotent: true,
23
+ inputs: { properties: { filter: { type: 'string',
24
+ description: 'OData $filter expression' },
25
+ select: { type: 'string',
26
+ description: 'Comma-separated fields to return' } },
27
+ required: [] },
23
28
  trigger_words: %w[teams joined membership]
24
29
 
25
- def list_joined_teams(user_id: 'me', **)
26
- response = graph_connection(**).get("#{user_path(user_id)}/joinedTeams")
30
+ def list_joined_teams(user_id: 'me', filter: nil, select: nil, **)
31
+ params = {}
32
+ params['$filter'] = filter if filter
33
+ params['$select'] = select if select
34
+ response = graph_connection(**).get("#{user_path(user_id)}/joinedTeams", params)
27
35
  { result: response.body }
28
36
  end
29
37
 
@@ -42,17 +50,47 @@ module Legion
42
50
  end
43
51
 
44
52
  definition :list_team_members,
45
- desc: 'List members of a Team',
53
+ desc: 'List members of a Team with pagination',
46
54
  mcp_prefix: 'teams.list_team_members',
47
55
  mcp_category: 'teams_teams',
48
56
  mcp_tier: :standard,
49
57
  idempotent: true,
50
- inputs: { properties: { team_id: { type: 'string' } }, required: ['team_id'] },
58
+ inputs: { properties: { team_id: { type: 'string' },
59
+ top: { type: 'integer',
60
+ description: 'Members per page (default 100)' },
61
+ max_pages: { type: 'integer',
62
+ description: 'Maximum pages to fetch (default 1)' },
63
+ filter: { type: 'string',
64
+ description: 'OData $filter expression' } },
65
+ required: ['team_id'] },
51
66
  trigger_words: %w[members roster]
52
67
 
53
- def list_team_members(team_id:, **)
54
- response = graph_connection(**).get("teams/#{team_id}/members")
55
- { result: response.body }
68
+ def list_team_members(team_id:, top: 100, max_pages: 1, filter: nil, **)
69
+ params = { '$top' => top }
70
+ params['$filter'] = filter if filter
71
+ conn = graph_connection(**)
72
+ response = conn.get("teams/#{team_id}/members", params)
73
+ body = response.body
74
+
75
+ return { result: body } if max_pages <= 1
76
+
77
+ all_values = Array(body['value'] || body[:value])
78
+ next_link = body['@odata.nextLink'] || body[:'@odata.nextLink']
79
+ pages_fetched = 1
80
+
81
+ while next_link && pages_fetched < max_pages
82
+ response = conn.get(next_link)
83
+ page_body = response.body
84
+ items = page_body['value'] || page_body[:value]
85
+ all_values.concat(Array(items)) if items
86
+ next_link = page_body['@odata.nextLink'] || page_body[:'@odata.nextLink']
87
+ pages_fetched += 1
88
+ end
89
+
90
+ result = { '@odata.context' => body['@odata.context'] || body[:'@odata.context'],
91
+ 'value' => all_values }
92
+ result['@odata.nextLink'] = next_link if next_link
93
+ { result: result }
56
94
  end
57
95
 
58
96
  include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers, false) &&
@@ -20,17 +20,44 @@ module Legion
20
20
  end
21
21
 
22
22
  definition :list_transcripts,
23
- desc: 'List transcripts for an online meeting',
23
+ desc: 'List transcripts for an online meeting with pagination',
24
24
  mcp_prefix: 'teams.list_transcripts',
25
25
  mcp_category: 'teams_meetings',
26
26
  mcp_tier: :standard,
27
27
  idempotent: true,
28
- inputs: { properties: { meeting_id: { type: 'string' } }, required: ['meeting_id'] },
28
+ inputs: { properties: { meeting_id: { type: 'string' },
29
+ top: { type: 'integer',
30
+ description: 'Transcripts per page (default 50)' },
31
+ max_pages: { type: 'integer',
32
+ description: 'Maximum pages to fetch (default 1)' } },
33
+ required: ['meeting_id'] },
29
34
  trigger_words: ['transcripts']
30
35
 
31
- def list_transcripts(meeting_id:, user_id: 'me', **)
32
- response = graph_connection(**).get("#{user_path(user_id)}/onlineMeetings/#{meeting_id}/transcripts")
33
- { result: response.body }
36
+ def list_transcripts(meeting_id:, user_id: 'me', top: 50, max_pages: 1, **)
37
+ params = { '$top' => top }
38
+ conn = graph_connection(**)
39
+ response = conn.get("#{user_path(user_id)}/onlineMeetings/#{meeting_id}/transcripts", params)
40
+ body = response.body
41
+
42
+ return { result: body } if max_pages <= 1
43
+
44
+ all_values = Array(body['value'] || body[:value])
45
+ next_link = body['@odata.nextLink'] || body[:'@odata.nextLink']
46
+ pages_fetched = 1
47
+
48
+ while next_link && pages_fetched < max_pages
49
+ response = conn.get(next_link)
50
+ page_body = response.body
51
+ items = page_body['value'] || page_body[:value]
52
+ all_values.concat(Array(items)) if items
53
+ next_link = page_body['@odata.nextLink'] || page_body[:'@odata.nextLink']
54
+ pages_fetched += 1
55
+ end
56
+
57
+ result = { '@odata.context' => body['@odata.context'] || body[:'@odata.context'],
58
+ 'value' => all_values }
59
+ result['@odata.nextLink'] = next_link if next_link
60
+ { result: result }
34
61
  end
35
62
 
36
63
  definition :get_transcript,
@@ -58,7 +85,9 @@ module Legion
58
85
  mcp_tier: :standard,
59
86
  idempotent: true,
60
87
  inputs: { properties: { meeting_id: { type: 'string' },
61
- transcript_id: { type: 'string' } },
88
+ transcript_id: { type: 'string' },
89
+ format: { type: 'string',
90
+ description: 'Output format: vtt (default) or docx' } },
62
91
  required: %w[meeting_id transcript_id] },
63
92
  trigger_words: %w[content vtt text read]
64
93
 
@@ -3,7 +3,7 @@
3
3
  module Legion
4
4
  module Extensions
5
5
  module MicrosoftTeams
6
- VERSION = '0.6.49'
6
+ VERSION = '0.6.51'
7
7
  end
8
8
  end
9
9
  end
@@ -2,6 +2,8 @@
2
2
 
3
3
  require 'legion/extensions/identity/entra/helpers/token_manager'
4
4
  require 'legion/extensions/microsoft_teams/version'
5
+ require 'legion/extensions/microsoft_teams/errors'
6
+ require 'legion/extensions/microsoft_teams/faraday/retry_after'
5
7
  require 'legion/extensions/microsoft_teams/helpers/client'
6
8
  require 'legion/extensions/microsoft_teams/runners/auth'
7
9
  require 'legion/extensions/microsoft_teams/runners/teams'
@@ -29,6 +31,7 @@ require 'legion/extensions/microsoft_teams/runners/app_installations'
29
31
  require 'legion/extensions/microsoft_teams/runners/files'
30
32
 
31
33
  # Helpers (bot)
34
+ require 'legion/extensions/microsoft_teams/helpers/graph_cache'
32
35
  require 'legion/extensions/microsoft_teams/helpers/high_water_mark'
33
36
  require 'legion/extensions/microsoft_teams/helpers/prompt_resolver'
34
37
  require 'legion/extensions/microsoft_teams/helpers/trace_retriever'
@@ -60,6 +63,62 @@ module Legion
60
63
  module MicrosoftTeams
61
64
  extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core, false
62
65
 
66
+ def self.default_settings # rubocop:disable Metrics/MethodLength
67
+ {
68
+ api_ingest: {
69
+ enabled: true,
70
+ interval: 3600,
71
+ top_people: 15,
72
+ message_depth: 50,
73
+ skip_bots: true
74
+ },
75
+ incremental_sync: {
76
+ enabled: true,
77
+ interval: 900,
78
+ top_people: 10,
79
+ message_depth: 50
80
+ },
81
+ profile_ingest: {
82
+ enabled: true,
83
+ top_people: 10,
84
+ message_depth: 50
85
+ },
86
+ presence_poller: {
87
+ enabled: false,
88
+ interval: 300
89
+ },
90
+ meeting_ingest: {
91
+ enabled: true,
92
+ interval: 900
93
+ },
94
+ channel_poller: {
95
+ enabled: false,
96
+ interval: 120,
97
+ max_teams: 10,
98
+ max_channels_per_team: 5
99
+ },
100
+ direct_chat_poller: {
101
+ enabled: false,
102
+ interval: 30
103
+ },
104
+ observed_chat_poller: {
105
+ enabled: false,
106
+ interval: 60
107
+ },
108
+ cache: {
109
+ graph_ttl: 300
110
+ },
111
+ client: {
112
+ throttle_circuit: {
113
+ soft_percentage: 0.8,
114
+ soft_ttl: 60,
115
+ fallback_ttl: 60,
116
+ insights_ttl: 600
117
+ }
118
+ }
119
+ }
120
+ end
121
+
63
122
  def self.trigger_words
64
123
  %w[teams microsoft_teams microsoftteams microsoft-teams msteams ms-teams]
65
124
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lex-microsoft_teams
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.49
4
+ version: 0.6.51
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -207,7 +207,11 @@ files:
207
207
  - lib/legion/extensions/microsoft_teams/actors/presence_poller.rb
208
208
  - lib/legion/extensions/microsoft_teams/actors/profile_ingest.rb
209
209
  - lib/legion/extensions/microsoft_teams/client.rb
210
+ - lib/legion/extensions/microsoft_teams/errors.rb
211
+ - lib/legion/extensions/microsoft_teams/faraday/retry_after.rb
212
+ - lib/legion/extensions/microsoft_teams/faraday/throttle_circuit.rb
210
213
  - lib/legion/extensions/microsoft_teams/helpers/client.rb
214
+ - lib/legion/extensions/microsoft_teams/helpers/graph_cache.rb
211
215
  - lib/legion/extensions/microsoft_teams/helpers/graph_client.rb
212
216
  - lib/legion/extensions/microsoft_teams/helpers/high_water_mark.rb
213
217
  - lib/legion/extensions/microsoft_teams/helpers/permission_guard.rb