lex-microsoft_teams 0.6.20 → 0.6.23

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: beeb83d06cdfce621a29d635ac797893d31d9d8403c88a488ee076b278d4a87c
4
- data.tar.gz: 759f1937d7693a07e1ad6fa5172583f9d13c58eadce7c12aac3eb42cfb0c75c1
3
+ metadata.gz: cc24206d1c97c648d8c3cf0b5ab0acac653195153903e901e1d89e89445b6618
4
+ data.tar.gz: 5768ef9650b4bc861d193c8477e80f2e96962501c11246aaf1178b00c43f1432
5
5
  SHA512:
6
- metadata.gz: da442c0b860020406dd65a43b5e6ef9928b0f45e7123709fc2340e81b7288a0a8b94a7e6bb7781bde4c64bc8e071dfcc96b2e24a9b60a0974dc540e3454fd41c
7
- data.tar.gz: dcf5a9cf6e7d8a9171131833588b9a41fa5a7a864dc26681b9eee0b31cd6da0e841456d84aefa153dfac51bf16f07350cad3eeed04702f4d85a1376ac062da54
6
+ metadata.gz: eb531084dcff80d75c9e624ab02f2db647d18758cf7621f7ee3ea8f79be593b5db2d4297aef2ac716aba394a76945e766ee13015b4558ed50b57dacc401d4fff
7
+ data.tar.gz: f7e5df0a0e9b251043b14e1dac114c24c797b23fc8097402508bfacf326f6903f125ffcbd6df474b9927542b2ba4fd123d8aabf9355ac3b302a1b47cb2510694
data/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # Changelog
2
2
 
3
+ ## [Unreleased]
4
+
5
+ ### Fixed
6
+ - `Absorbers::Meeting#graph_token` — rescue now captures the exception as `=> e` and logs a warning, satisfying the rescue-logging lint rule
7
+
8
+ ## [0.6.23] - 2026-03-27
9
+
10
+ ### Changed
11
+ - `Absorbers::Meeting` — all Graph API runner calls now pass `token: graph_token` so requests carry an `Authorization` header in production. `graph_token` resolves from `Helpers::TokenCache.instance.cached_graph_token` when available, falling back to `nil` (unauthenticated) with a rescued `StandardError` to prevent test-environment boot failures
12
+ - `CLAUDE.md` — version field updated to 0.6.23
13
+
14
+ ## [0.6.22] - 2026-03-27
15
+
16
+ ### Changed
17
+ - `Absorbers::Meeting#handle` now fails fast with `{ success: false, error: 'meeting has no id' }` when the resolved meeting item has no `id` field, preventing subsequent runner calls from building invalid URLs
18
+ - `spec/legion/extensions/microsoft_teams/absorbers/meeting_spec.rb` — added spec covering the blank `meeting_id` guard path
19
+
20
+ ## [0.6.21] - 2026-03-27
21
+
22
+ ### Added
23
+ - `Absorbers::Meeting` — reference implementation of the absorber framework for Teams meetings. Resolves a Teams join URL to a meeting via Graph API, then ingests transcripts (VTT), AI insights, and participant lists into Apollo knowledge store. Two URL patterns registered: `teams.microsoft.com/l/meetup-join/*` and `*.teams.microsoft.com/meet/*`. Guard on `Legion::Extensions::Absorbers` ensures the absorber only loads when the framework base class is available.
24
+ - `spec/spec_helper.rb` — inline stubs for `Legion::Extensions::Absorbers::Base` and `Matchers::Url` so absorber specs run without the full `legionio` gem in the test environment
25
+
26
+ ### Changed
27
+ - `lib/legion/extensions/microsoft_teams/absorbers/meeting.rb` — runner calls now go through `meetings_runner`, `transcripts_runner`, and `ai_insights_runner` instance accessors (`Object.new.extend(Runners::*)`) instead of calling runner modules directly as class methods, which would raise `NoMethodError` at runtime
28
+ - `spec/legion/extensions/microsoft_teams/absorbers/meeting_spec.rb` — specs stub runner instances via `absorber.meetings_runner` / `absorber.transcripts_runner` / `absorber.ai_insights_runner` rather than the module constants; `.patterns` spec no longer relies on `patterns.first` ordering; now asserts both expected pattern values are present in the set
29
+
3
30
  ## [0.6.19] - 2026-03-26
4
31
 
5
32
  ### Changed
data/CLAUDE.md CHANGED
@@ -10,7 +10,7 @@ Legion Extension that connects LegionIO to Microsoft Teams via Graph API and Bot
10
10
 
11
11
  **GitHub**: https://github.com/LegionIO/lex-microsoft_teams
12
12
  **License**: MIT
13
- **Version**: 0.6.18
13
+ **Version**: 0.6.23
14
14
 
15
15
  ## Architecture
16
16
 
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module MicrosoftTeams
6
+ module Absorbers
7
+ class Meeting < Legion::Extensions::Absorbers::Base
8
+ pattern :url, 'teams.microsoft.com/l/meetup-join/*'
9
+ pattern :url, '*.teams.microsoft.com/meet/*'
10
+ description 'Absorbs Teams meeting transcripts, AI insights, and participants into Apollo'
11
+
12
+ def handle(url: nil, content: nil, metadata: {}, context: {}) # rubocop:disable Lint/UnusedMethodArgument
13
+ report_progress(message: 'resolving meeting from link')
14
+ meeting = resolve_meeting(url)
15
+ return { success: false, error: 'could not resolve meeting' } unless meeting
16
+
17
+ subject = meeting['subject'] || meeting[:subject] || 'untitled meeting'
18
+ meeting_id = meeting['id'] || meeting[:id]
19
+ return { success: false, error: 'meeting has no id' } if meeting_id.nil? || meeting_id.to_s.empty?
20
+
21
+ results = { meeting_id: meeting_id, subject: subject, chunks: 0 }
22
+
23
+ ingest_transcript(meeting_id, subject, results)
24
+ ingest_ai_insights(meeting_id, subject, results)
25
+ ingest_participants(meeting, subject, results)
26
+
27
+ report_progress(message: 'done', percent: 100)
28
+ results.merge(success: true)
29
+ rescue StandardError => e
30
+ log.error("Meeting absorber failed: #{e.message}")
31
+ { success: false, error: e.message }
32
+ end
33
+
34
+ private
35
+
36
+ def meetings_runner
37
+ @meetings_runner ||= Object.new.extend(Runners::Meetings)
38
+ end
39
+
40
+ def transcripts_runner
41
+ @transcripts_runner ||= Object.new.extend(Runners::Transcripts)
42
+ end
43
+
44
+ def ai_insights_runner
45
+ @ai_insights_runner ||= Object.new.extend(Runners::AiInsights)
46
+ end
47
+
48
+ def graph_token
49
+ return @graph_token if defined?(@graph_token)
50
+
51
+ @graph_token = begin
52
+ Helpers::TokenCache.instance.cached_graph_token if defined?(Helpers::TokenCache)
53
+ rescue StandardError => e
54
+ log.warn("graph_token unavailable: #{e.message}")
55
+ nil
56
+ end
57
+ end
58
+
59
+ def resolve_meeting(url)
60
+ report_progress(message: 'looking up meeting by join URL', percent: 5)
61
+ response = meetings_runner.get_meeting_by_join_url(join_url: url, token: graph_token)
62
+ return nil unless response.is_a?(Hash)
63
+
64
+ body = response[:result]
65
+ return nil unless body.is_a?(Hash)
66
+
67
+ items = body['value'] || body[:value]
68
+ return nil unless items.is_a?(Array) && !items.empty?
69
+
70
+ items.first
71
+ rescue StandardError => e
72
+ log.warn("Could not resolve meeting: #{e.message}")
73
+ nil
74
+ end
75
+
76
+ def ingest_transcript(meeting_id, subject, results)
77
+ report_progress(message: 'fetching transcripts', percent: 20)
78
+ transcripts_response = transcripts_runner.list_transcripts(meeting_id: meeting_id, token: graph_token)
79
+ transcripts_body = transcripts_response.is_a?(Hash) ? transcripts_response[:result] : nil
80
+ return unless transcripts_body.is_a?(Hash)
81
+
82
+ transcript_items = transcripts_body['value'] || transcripts_body[:value]
83
+ return unless transcript_items.is_a?(Array) && transcript_items.any?
84
+
85
+ transcript_items.each do |t|
86
+ transcript_id = t['id'] || t[:id]
87
+ next unless transcript_id
88
+
89
+ report_progress(message: "pulling transcript #{transcript_id}", percent: 40)
90
+ vtt_result = transcripts_runner.get_transcript_content(
91
+ meeting_id: meeting_id, transcript_id: transcript_id, format: :vtt, token: graph_token
92
+ )
93
+ vtt = vtt_result.is_a?(Hash) ? vtt_result[:result] : vtt_result
94
+ next unless vtt.is_a?(String) && !vtt.empty?
95
+
96
+ absorb_to_knowledge(
97
+ content: vtt,
98
+ tags: ['meeting', 'transcript', subject],
99
+ source_file: "teams://meetings/#{meeting_id}/transcripts/#{transcript_id}",
100
+ heading: "Transcript: #{subject}",
101
+ content_type: 'meeting_transcript'
102
+ )
103
+ results[:chunks] += 1
104
+ end
105
+ rescue StandardError => e
106
+ log.warn("Transcript ingest failed: #{e.message}")
107
+ end
108
+
109
+ def ingest_ai_insights(meeting_id, subject, results)
110
+ report_progress(message: 'fetching AI insights', percent: 60)
111
+ insights = ai_insights_runner.list_meeting_ai_insights(meeting_id: meeting_id, token: graph_token)
112
+ return unless insights.is_a?(Hash)
113
+
114
+ body = insights[:result] || insights
115
+ items = body.is_a?(Hash) ? (body['value'] || body[:value]) : nil
116
+ return unless items.is_a?(Array) && items.any?
117
+
118
+ items.each { |item| absorb_insight_item(item, meeting_id, subject, results) }
119
+ rescue StandardError => e
120
+ log.warn("AI insights ingest failed: #{e.message}")
121
+ end
122
+
123
+ def absorb_insight_item(item, meeting_id, subject, results)
124
+ return unless item.is_a?(Hash)
125
+
126
+ insight_id = item['id'] || item[:id]
127
+ action_items = item['actionItems'] || item[:actionItems] || []
128
+ return if action_items.empty?
129
+
130
+ content = action_items.filter_map { |a| a.is_a?(Hash) ? (a['text'] || a[:text]) : a.to_s }.join("\n")
131
+ return if content.empty?
132
+
133
+ absorb_to_knowledge(
134
+ content: content,
135
+ tags: ['meeting', 'ai-insight', 'action-item', subject],
136
+ source_file: "teams://meetings/#{meeting_id}/insights/#{insight_id}",
137
+ heading: "AI Insight: #{subject}",
138
+ content_type: 'meeting_insight'
139
+ )
140
+ results[:chunks] += 1
141
+ end
142
+
143
+ def ingest_participants(meeting, subject, results)
144
+ report_progress(message: 'recording participants', percent: 80)
145
+ participants = meeting.dig('participants', 'attendees') || meeting.dig(:participants, :attendees)
146
+ return unless participants.is_a?(Array) && participants.any?
147
+
148
+ names = participants.filter_map do |p|
149
+ p.dig('identity', 'user', 'displayName') || p.dig(:identity, :user, :displayName)
150
+ end
151
+ return if names.empty?
152
+
153
+ meeting_id = meeting['id'] || meeting[:id]
154
+ absorb_raw(
155
+ content: "Meeting participants for '#{subject}': #{names.join(', ')}",
156
+ tags: ['meeting', 'participants', subject],
157
+ content_type: 'meeting_participants',
158
+ metadata: { meeting_id: meeting_id, participant_count: names.length }
159
+ )
160
+ results[:chunks] += 1
161
+ rescue StandardError => e
162
+ log.warn("Participant ingest failed: #{e.message}")
163
+ end
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end
@@ -3,7 +3,7 @@
3
3
  module Legion
4
4
  module Extensions
5
5
  module MicrosoftTeams
6
- VERSION = '0.6.20'
6
+ VERSION = '0.6.23'
7
7
  end
8
8
  end
9
9
  end
@@ -43,6 +43,12 @@ end
43
43
 
44
44
  require 'legion/extensions/microsoft_teams/client'
45
45
 
46
+ if defined?(Legion::Extensions) &&
47
+ Legion::Extensions.const_defined?(:Absorbers, false) &&
48
+ Legion::Extensions::Absorbers.const_defined?(:Base, false)
49
+ require_relative 'microsoft_teams/absorbers/meeting'
50
+ end
51
+
46
52
  module Legion
47
53
  module Extensions
48
54
  module MicrosoftTeams
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.20
4
+ version: 0.6.23
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -175,6 +175,7 @@ files:
175
175
  - docs/plans/2026-03-19-teams-token-lifecycle-implementation.md
176
176
  - lex-microsoft_teams.gemspec
177
177
  - lib/legion/extensions/microsoft_teams.rb
178
+ - lib/legion/extensions/microsoft_teams/absorbers/meeting.rb
178
179
  - lib/legion/extensions/microsoft_teams/actors/api_ingest.rb
179
180
  - lib/legion/extensions/microsoft_teams/actors/auth_validator.rb
180
181
  - lib/legion/extensions/microsoft_teams/actors/cache_bulk_ingest.rb