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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cc24206d1c97c648d8c3cf0b5ab0acac653195153903e901e1d89e89445b6618
|
|
4
|
+
data.tar.gz: 5768ef9650b4bc861d193c8477e80f2e96962501c11246aaf1178b00c43f1432
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
@@ -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
|
|
@@ -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.
|
|
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
|