lex-microsoft_teams 0.6.10 → 0.6.14
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/CHANGELOG.md +32 -0
- data/Gemfile +0 -1
- data/lex-microsoft_teams.gemspec +7 -0
- data/lib/legion/extensions/microsoft_teams/actors/api_ingest.rb +94 -0
- data/lib/legion/extensions/microsoft_teams/actors/auth_validator.rb +2 -1
- data/lib/legion/extensions/microsoft_teams/actors/cache_bulk_ingest.rb +4 -2
- data/lib/legion/extensions/microsoft_teams/actors/cache_sync.rb +2 -1
- data/lib/legion/extensions/microsoft_teams/actors/channel_poller.rb +4 -2
- data/lib/legion/extensions/microsoft_teams/actors/direct_chat_poller.rb +2 -1
- data/lib/legion/extensions/microsoft_teams/actors/incremental_sync.rb +7 -5
- data/lib/legion/extensions/microsoft_teams/actors/meeting_ingest.rb +4 -2
- data/lib/legion/extensions/microsoft_teams/actors/message_processor.rb +2 -1
- data/lib/legion/extensions/microsoft_teams/actors/observed_chat_poller.rb +2 -1
- data/lib/legion/extensions/microsoft_teams/actors/presence_poller.rb +2 -1
- data/lib/legion/extensions/microsoft_teams/actors/profile_ingest.rb +6 -3
- data/lib/legion/extensions/microsoft_teams/actors/token_refresher.rb +2 -1
- data/lib/legion/extensions/microsoft_teams/cli/auth.rb +2 -1
- data/lib/legion/extensions/microsoft_teams/helpers/high_water_mark.rb +2 -1
- data/lib/legion/extensions/microsoft_teams/helpers/prompt_resolver.rb +2 -1
- data/lib/legion/extensions/microsoft_teams/helpers/token_cache.rb +3 -2
- data/lib/legion/extensions/microsoft_teams/runners/api_ingest.rb +307 -0
- data/lib/legion/extensions/microsoft_teams/runners/cache_ingest.rb +15 -3
- data/lib/legion/extensions/microsoft_teams/runners/profile_ingest.rb +6 -3
- data/lib/legion/extensions/microsoft_teams/version.rb +1 -1
- data/lib/legion/extensions/microsoft_teams.rb +1 -0
- metadata +101 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7e4252f4382060712bb112a7d7beaf55b10947fe6200185a3fb6ee413e4f3927
|
|
4
|
+
data.tar.gz: af5fb381e122efd02feab899a484242c71f5b67fb620717fad6dcb12c372e2cd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 22a8726642d3a8209c09c4fc797812825cd21ae7dcc8818438333a62291fa9a3a08356125e5003a426e8098ba56ad8033e77a1fd0bb4a162bd6c989bd20b127d
|
|
7
|
+
data.tar.gz: 89f55fcbe2210cc4f55a77d4ee04407fe0a3ba437cb1ad14e4699468f0dfbdcf1481ae72d01f230305c0ded9c9e2b1a1558360502ae43a27428d9f5d72634377
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.6.14] - 2026-03-23
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Graph API ingest runner and actor for fetching top contacts and their 1:1 chat messages
|
|
7
|
+
- People-based chat matching with email, userId, and displayName fallbacks
|
|
8
|
+
- High-water mark support for incremental message fetching
|
|
9
|
+
- Paginated chat fetching with MAX_CHAT_PAGES cap
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- Replace all silent rescue blocks with log.debug/warn/error entries
|
|
13
|
+
- Use `log.` helper consistently instead of `Legion::Logging.`
|
|
14
|
+
- Fix `IncrementalSync#resolve_token` to use `TokenCache.instance` instead of `.new`
|
|
15
|
+
- Clean up debug logging (remove log.unknown/log.fatal, use log.debug)
|
|
16
|
+
|
|
17
|
+
## [0.6.13] - 2026-03-22
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
- Add legion-data, legion-json, and legion-transport as runtime dependencies
|
|
21
|
+
- Include `Legion::Data::Helper`, `Legion::JSON::Helper`, and `Legion::Transport::Helper` in spec_helper Lex stub
|
|
22
|
+
|
|
23
|
+
## [0.6.12] - 2026-03-22
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
- Add legion-cache and legion-crypt as runtime dependencies
|
|
27
|
+
- Include `Legion::Cache::Helper` and `Legion::Crypt::Helper` in spec_helper Lex stub
|
|
28
|
+
|
|
29
|
+
## [0.6.11] - 2026-03-22
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
- Add legion-logging and legion-settings as runtime dependencies
|
|
33
|
+
- Include `Legion::Settings::Helper` in spec_helper Lex stub for real settings access in tests
|
|
34
|
+
|
|
3
35
|
## [0.6.10] - 2026-03-22
|
|
4
36
|
|
|
5
37
|
### Changed
|
data/Gemfile
CHANGED
data/lex-microsoft_teams.gemspec
CHANGED
|
@@ -28,5 +28,12 @@ Gem::Specification.new do |spec|
|
|
|
28
28
|
|
|
29
29
|
spec.add_dependency 'base64', '>= 0.1'
|
|
30
30
|
spec.add_dependency 'faraday', '>= 2.0'
|
|
31
|
+
spec.add_dependency 'legion-cache', '>= 1.3.11'
|
|
32
|
+
spec.add_dependency 'legion-crypt', '>= 1.4.9'
|
|
33
|
+
spec.add_dependency 'legion-data', '>= 1.4.17'
|
|
34
|
+
spec.add_dependency 'legion-json', '>= 1.2.1'
|
|
35
|
+
spec.add_dependency 'legion-logging', '>= 1.3.2'
|
|
36
|
+
spec.add_dependency 'legion-settings', '>= 1.3.14'
|
|
37
|
+
spec.add_dependency 'legion-transport', '>= 1.3.9'
|
|
31
38
|
spec.add_dependency 'snappy', '>= 0.5'
|
|
32
39
|
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module MicrosoftTeams
|
|
6
|
+
module Actor
|
|
7
|
+
class ApiIngest < Legion::Extensions::Actors::Every
|
|
8
|
+
def runner_class = Legion::Extensions::MicrosoftTeams::Runners::ApiIngest
|
|
9
|
+
|
|
10
|
+
def runner_function = 'ingest_api'
|
|
11
|
+
|
|
12
|
+
def use_runner? = false
|
|
13
|
+
|
|
14
|
+
def check_subtask? = false
|
|
15
|
+
|
|
16
|
+
def generate_task? = false
|
|
17
|
+
|
|
18
|
+
def run_now? = true
|
|
19
|
+
|
|
20
|
+
def delay
|
|
21
|
+
10.0 # let memory + cache ingest initialize first
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def time
|
|
25
|
+
interval = teams_settings.dig(:ingest, :api_interval) || 1800
|
|
26
|
+
interval.to_i
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def enabled?
|
|
30
|
+
defined?(Legion::Extensions::Agentic::Memory::Trace::Runners::Traces)
|
|
31
|
+
rescue StandardError => e
|
|
32
|
+
log.warn("ApiIngest#enabled?: #{e.message}")
|
|
33
|
+
false
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def manual
|
|
37
|
+
token = resolve_token
|
|
38
|
+
unless token
|
|
39
|
+
log.warn('ApiIngest: no delegated token, skipping')
|
|
40
|
+
return
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
ingest = teams_settings[:ingest] || {}
|
|
44
|
+
log.info('ApiIngest: starting Graph API ingest')
|
|
45
|
+
result = runner_class.ingest_api(
|
|
46
|
+
token: token,
|
|
47
|
+
top_people: ingest.fetch(:top_people, 15),
|
|
48
|
+
message_depth: ingest.fetch(:message_depth, 50),
|
|
49
|
+
skip_bots: ingest.fetch(:skip_bots, true),
|
|
50
|
+
imprint_active: imprint_active?
|
|
51
|
+
)
|
|
52
|
+
log.info("ApiIngest: #{result.inspect[0, 200]}")
|
|
53
|
+
result
|
|
54
|
+
rescue StandardError => e
|
|
55
|
+
log.error("ApiIngest: #{e.message}")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def token_available?
|
|
61
|
+
resolve_token != nil
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def resolve_token
|
|
65
|
+
if defined?(Legion::Extensions::MicrosoftTeams::Helpers::TokenCache)
|
|
66
|
+
Legion::Extensions::MicrosoftTeams::Helpers::TokenCache.instance.cached_delegated_token
|
|
67
|
+
end
|
|
68
|
+
rescue StandardError => e
|
|
69
|
+
log.warn("ApiIngest#resolve_token: #{e.message}")
|
|
70
|
+
nil
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def teams_settings
|
|
74
|
+
return {} unless defined?(Legion::Settings)
|
|
75
|
+
|
|
76
|
+
Legion::Settings[:microsoft_teams] || {}
|
|
77
|
+
rescue StandardError => e
|
|
78
|
+
log.warn("ApiIngest#teams_settings: #{e.message}")
|
|
79
|
+
{}
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def imprint_active?
|
|
83
|
+
return false unless defined?(Legion::Extensions::Coldstart::Helpers::Bootstrap)
|
|
84
|
+
|
|
85
|
+
Legion::Extensions::Coldstart::Helpers::Bootstrap.new.imprint_active?
|
|
86
|
+
rescue StandardError => e
|
|
87
|
+
log.debug("ApiIngest#imprint_active?: #{e.message}")
|
|
88
|
+
false
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -17,7 +17,8 @@ module Legion
|
|
|
17
17
|
|
|
18
18
|
def enabled?
|
|
19
19
|
defined?(Legion::Extensions::Agentic::Memory::Trace::Runners::Traces)
|
|
20
|
-
rescue StandardError
|
|
20
|
+
rescue StandardError => e
|
|
21
|
+
log.debug("CacheBulkIngest#enabled?: #{e.message}")
|
|
21
22
|
false
|
|
22
23
|
end
|
|
23
24
|
|
|
@@ -40,7 +41,8 @@ module Legion
|
|
|
40
41
|
return false unless defined?(Legion::Extensions::Coldstart::Helpers::Bootstrap)
|
|
41
42
|
|
|
42
43
|
Legion::Extensions::Coldstart::Helpers::Bootstrap.new.imprint_active?
|
|
43
|
-
rescue StandardError
|
|
44
|
+
rescue StandardError => e
|
|
45
|
+
log.debug("CacheBulkIngest#imprint_active?: #{e.message}")
|
|
44
46
|
false
|
|
45
47
|
end
|
|
46
48
|
end
|
|
@@ -38,7 +38,8 @@ module Legion
|
|
|
38
38
|
return false unless defined?(Legion::Extensions::MicrosoftTeams::Helpers::TokenCache)
|
|
39
39
|
|
|
40
40
|
channel_setting(:enabled, false) == true
|
|
41
|
-
rescue StandardError
|
|
41
|
+
rescue StandardError => e
|
|
42
|
+
log.debug("ChannelPoller#enabled?: #{e.message}")
|
|
42
43
|
false
|
|
43
44
|
end
|
|
44
45
|
|
|
@@ -157,7 +158,8 @@ module Legion
|
|
|
157
158
|
return default unless defined?(Legion::Settings)
|
|
158
159
|
|
|
159
160
|
Legion::Settings.dig(:microsoft_teams, :channels, key) || default
|
|
160
|
-
rescue StandardError
|
|
161
|
+
rescue StandardError => e
|
|
162
|
+
log.debug("ChannelPoller#channel_setting(#{key}): #{e.message}")
|
|
161
163
|
default
|
|
162
164
|
end
|
|
163
165
|
|
|
@@ -15,7 +15,8 @@ module Legion
|
|
|
15
15
|
def delay
|
|
16
16
|
settings = begin
|
|
17
17
|
Legion::Settings[:microsoft_teams] || {}
|
|
18
|
-
rescue StandardError
|
|
18
|
+
rescue StandardError => e
|
|
19
|
+
log.debug("IncrementalSync#delay: #{e.message}")
|
|
19
20
|
{}
|
|
20
21
|
end
|
|
21
22
|
settings.dig(:ingest, :incremental_interval) || 900
|
|
@@ -24,7 +25,8 @@ module Legion
|
|
|
24
25
|
def enabled?
|
|
25
26
|
defined?(Legion::Extensions::Agentic::Memory::Trace::Runners::Traces) &&
|
|
26
27
|
token_available?
|
|
27
|
-
rescue StandardError
|
|
28
|
+
rescue StandardError => e
|
|
29
|
+
log.debug("IncrementalSync#enabled?: #{e.message}")
|
|
28
30
|
false
|
|
29
31
|
end
|
|
30
32
|
|
|
@@ -55,10 +57,10 @@ module Legion
|
|
|
55
57
|
|
|
56
58
|
def resolve_token
|
|
57
59
|
if defined?(Legion::Extensions::MicrosoftTeams::Helpers::TokenCache)
|
|
58
|
-
|
|
59
|
-
cache.cached_delegated_token
|
|
60
|
+
Legion::Extensions::MicrosoftTeams::Helpers::TokenCache.instance.cached_delegated_token
|
|
60
61
|
end
|
|
61
|
-
rescue StandardError
|
|
62
|
+
rescue StandardError => e
|
|
63
|
+
log.warn("IncrementalSync#resolve_token: #{e.message}")
|
|
62
64
|
nil
|
|
63
65
|
end
|
|
64
66
|
end
|
|
@@ -24,7 +24,8 @@ module Legion
|
|
|
24
24
|
def time
|
|
25
25
|
settings = begin
|
|
26
26
|
Legion::Settings[:microsoft_teams] || {}
|
|
27
|
-
rescue StandardError
|
|
27
|
+
rescue StandardError => e
|
|
28
|
+
log.debug("MeetingIngest#time: #{e.message}")
|
|
28
29
|
{}
|
|
29
30
|
end
|
|
30
31
|
settings.dig(:meetings, :ingest_interval) || DEFAULT_INGEST_INTERVAL
|
|
@@ -32,7 +33,8 @@ module Legion
|
|
|
32
33
|
|
|
33
34
|
def enabled?
|
|
34
35
|
defined?(Legion::Extensions::MicrosoftTeams::Helpers::TokenCache)
|
|
35
|
-
rescue StandardError
|
|
36
|
+
rescue StandardError => e
|
|
37
|
+
log.debug("MeetingIngest#enabled?: #{e.message}")
|
|
36
38
|
false
|
|
37
39
|
end
|
|
38
40
|
|
|
@@ -30,7 +30,8 @@ module Legion
|
|
|
30
30
|
return false unless defined?(Legion::Settings)
|
|
31
31
|
|
|
32
32
|
Legion::Settings.dig(:microsoft_teams, :bot, :observe, :enabled) == true
|
|
33
|
-
rescue StandardError
|
|
33
|
+
rescue StandardError => e
|
|
34
|
+
log.debug("ObservedChatPoller#enabled?: #{e.message}")
|
|
34
35
|
false
|
|
35
36
|
end
|
|
36
37
|
|
|
@@ -18,7 +18,8 @@ module Legion
|
|
|
18
18
|
def enabled?
|
|
19
19
|
defined?(Legion::Extensions::Agentic::Memory::Trace::Runners::Traces) &&
|
|
20
20
|
token_available?
|
|
21
|
-
rescue StandardError
|
|
21
|
+
rescue StandardError => e
|
|
22
|
+
log.debug("ProfileIngest#enabled?: #{e.message}")
|
|
22
23
|
false
|
|
23
24
|
end
|
|
24
25
|
|
|
@@ -33,7 +34,8 @@ module Legion
|
|
|
33
34
|
|
|
34
35
|
settings = begin
|
|
35
36
|
Legion::Settings[:microsoft_teams] || {}
|
|
36
|
-
rescue StandardError
|
|
37
|
+
rescue StandardError => e
|
|
38
|
+
log.debug("ProfileIngest#manual settings: #{e.message}")
|
|
37
39
|
{}
|
|
38
40
|
end
|
|
39
41
|
ingest = settings[:ingest] || {}
|
|
@@ -56,7 +58,8 @@ module Legion
|
|
|
56
58
|
if defined?(Legion::Extensions::MicrosoftTeams::Helpers::TokenCache)
|
|
57
59
|
Legion::Extensions::MicrosoftTeams::Helpers::TokenCache.instance.cached_delegated_token
|
|
58
60
|
end
|
|
59
|
-
rescue StandardError
|
|
61
|
+
rescue StandardError => e
|
|
62
|
+
log.warn("ProfileIngest#resolve_token: #{e.message}")
|
|
60
63
|
nil
|
|
61
64
|
end
|
|
62
65
|
end
|
|
@@ -58,7 +58,8 @@ module Legion
|
|
|
58
58
|
return nil unless raw
|
|
59
59
|
|
|
60
60
|
raw.is_a?(Hash) ? raw : ::JSON.parse(raw, symbolize_names: true)
|
|
61
|
-
rescue StandardError
|
|
61
|
+
rescue StandardError => e
|
|
62
|
+
log.debug("HighWaterMark: get_extended_hwm failed to parse cached value: #{e.message}")
|
|
62
63
|
nil
|
|
63
64
|
end
|
|
64
65
|
|
|
@@ -51,7 +51,8 @@ module Legion
|
|
|
51
51
|
|
|
52
52
|
profile = Legion::Extensions::Mesh::Helpers::PreferenceProfile.resolve(owner_id: owner_id)
|
|
53
53
|
Legion::Extensions::Mesh::Helpers::PreferenceProfile.preference_instructions(profile: profile)
|
|
54
|
-
rescue StandardError
|
|
54
|
+
rescue StandardError => e
|
|
55
|
+
log.debug("PromptResolver: preference_instructions_for failed: #{e.message}") if defined?(log)
|
|
55
56
|
nil
|
|
56
57
|
end
|
|
57
58
|
end
|
|
@@ -25,7 +25,7 @@ module Legion
|
|
|
25
25
|
@instance ||= begin
|
|
26
26
|
cache = new
|
|
27
27
|
cache.load_from_vault
|
|
28
|
-
|
|
28
|
+
log.info('[Teams::TokenCache] Shared instance created and loaded')
|
|
29
29
|
cache
|
|
30
30
|
end
|
|
31
31
|
end
|
|
@@ -249,7 +249,8 @@ module Legion
|
|
|
249
249
|
enabled = Legion::Settings.dig(:crypt, :vault, :enabled) == true
|
|
250
250
|
log.debug("vault_available? => #{enabled}")
|
|
251
251
|
enabled
|
|
252
|
-
rescue StandardError
|
|
252
|
+
rescue StandardError => e
|
|
253
|
+
log.debug("TokenCache: vault_available? check failed: #{e.message}")
|
|
253
254
|
false
|
|
254
255
|
end
|
|
255
256
|
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'digest'
|
|
5
|
+
require 'legion/extensions/microsoft_teams/helpers/client'
|
|
6
|
+
require 'legion/extensions/microsoft_teams/helpers/permission_guard'
|
|
7
|
+
require 'legion/extensions/microsoft_teams/helpers/high_water_mark'
|
|
8
|
+
|
|
9
|
+
module Legion
|
|
10
|
+
module Extensions
|
|
11
|
+
module MicrosoftTeams
|
|
12
|
+
module Runners
|
|
13
|
+
module ApiIngest
|
|
14
|
+
include Helpers::Client
|
|
15
|
+
include Helpers::PermissionGuard
|
|
16
|
+
include Helpers::HighWaterMark
|
|
17
|
+
extend self
|
|
18
|
+
|
|
19
|
+
# Fetch top contacts via /me/people, then pull recent messages from
|
|
20
|
+
# their 1:1 chats. Stores each message as an individual memory trace
|
|
21
|
+
# (same format as CacheIngest) with dedup by content hash.
|
|
22
|
+
#
|
|
23
|
+
# Requires a delegated token with Chat.Read and People.Read scopes.
|
|
24
|
+
def ingest_api(token:, top_people: 15, message_depth: 50, skip_bots: true, imprint_active: false, **) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
|
|
25
|
+
return error_result('lex-memory not loaded') unless memory_available?
|
|
26
|
+
return error_result('no token provided') unless token && !token.empty?
|
|
27
|
+
|
|
28
|
+
people = fetch_top_people(token: token, top: top_people)
|
|
29
|
+
log.debug("ApiIngest: fetched #{people.size} top people")
|
|
30
|
+
return error_result('people endpoint denied or empty') if people.empty?
|
|
31
|
+
|
|
32
|
+
chats = fetch_one_on_one_chats(token: token)
|
|
33
|
+
log.debug("ApiIngest: fetched #{chats.size} oneOnOne chats")
|
|
34
|
+
return error_result('no 1:1 chats found') if chats.empty?
|
|
35
|
+
|
|
36
|
+
existing_hashes = load_existing_hashes
|
|
37
|
+
conn = graph_connection(token: token)
|
|
38
|
+
stored = 0
|
|
39
|
+
skipped = 0
|
|
40
|
+
people_ingested = 0
|
|
41
|
+
thread_groups = Hash.new { |h, k| h[k] = [] }
|
|
42
|
+
|
|
43
|
+
people.each do |person|
|
|
44
|
+
chat = match_chat_to_person(chats: chats, person: person, conn: conn)
|
|
45
|
+
unless chat
|
|
46
|
+
log.debug("ApiIngest: no chat match for #{person['displayName']} " \
|
|
47
|
+
"(email=#{person.dig('scoredEmailAddresses', 0, 'address')}, id=#{person['id']})")
|
|
48
|
+
next
|
|
49
|
+
end
|
|
50
|
+
log.info("ApiIngest: matched #{person['displayName']} to chat #{chat['id']}")
|
|
51
|
+
|
|
52
|
+
messages = fetch_chat_messages(conn: conn, chat_id: chat['id'], depth: message_depth)
|
|
53
|
+
next if messages.empty?
|
|
54
|
+
|
|
55
|
+
msg_stored = 0
|
|
56
|
+
messages.each do |msg|
|
|
57
|
+
next if skip_bots && bot_message_graph?(msg)
|
|
58
|
+
|
|
59
|
+
text = extract_body_text(msg)
|
|
60
|
+
next if text.length < 5
|
|
61
|
+
|
|
62
|
+
content_hash = msg['id'] || Digest::SHA256.hexdigest(text)[0, 16]
|
|
63
|
+
if existing_hashes.include?(content_hash)
|
|
64
|
+
skipped += 1
|
|
65
|
+
next
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
trace_result = store_graph_message(msg, text, person, chat['id'],
|
|
69
|
+
content_hash: content_hash,
|
|
70
|
+
imprint_active: imprint_active)
|
|
71
|
+
if trace_result
|
|
72
|
+
stored += 1
|
|
73
|
+
msg_stored += 1
|
|
74
|
+
existing_hashes << content_hash
|
|
75
|
+
thread_groups[chat['id']] << trace_result[:trace_id]
|
|
76
|
+
else
|
|
77
|
+
skipped += 1
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
next unless msg_stored.positive?
|
|
82
|
+
|
|
83
|
+
people_ingested += 1
|
|
84
|
+
update_extended_hwm(chat_id: chat['id'],
|
|
85
|
+
last_message_at: messages.map { |m| m['createdDateTime'] }.compact.max,
|
|
86
|
+
new_message_count: msg_stored, ingested: true)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
coactivate_thread_traces(thread_groups)
|
|
90
|
+
flush_trace_store if stored.positive?
|
|
91
|
+
|
|
92
|
+
{ result: { stored: stored, skipped: skipped, people_ingested: people_ingested,
|
|
93
|
+
people_found: people.length, chats_found: chats.length } }
|
|
94
|
+
rescue StandardError => e
|
|
95
|
+
log_msg = "ApiIngest failed: #{e.class} — #{e.message}"
|
|
96
|
+
log.error(log_msg)
|
|
97
|
+
{ result: { stored: stored || 0, skipped: skipped || 0, error: e.message } }
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
|
|
101
|
+
Legion::Extensions::Helpers.const_defined?(:Lex)
|
|
102
|
+
|
|
103
|
+
MAX_CHAT_PAGES = 10
|
|
104
|
+
|
|
105
|
+
private
|
|
106
|
+
|
|
107
|
+
def fetch_top_people(token:, top:)
|
|
108
|
+
return [] if permission_denied?('/me/people')
|
|
109
|
+
|
|
110
|
+
conn = graph_connection(token: token)
|
|
111
|
+
resp = conn.get('me/people', { '$top' => top })
|
|
112
|
+
|
|
113
|
+
log.debug("ApiIngest: fetch_top_people status=#{resp.status} count=#{(resp.body || {}).fetch('value', []).size}")
|
|
114
|
+
if resp.status == 403
|
|
115
|
+
record_denial('/me/people', resp.body.dig('error', 'message') || 'Forbidden')
|
|
116
|
+
return []
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
people = (resp.body || {}).fetch('value', [])
|
|
120
|
+
people.sort_by { |p| -(p.dig('scoredEmailAddresses', 0, 'relevanceScore') || 0) }
|
|
121
|
+
rescue StandardError => e
|
|
122
|
+
log.warn("ApiIngest: fetch_top_people failed: #{e.message}")
|
|
123
|
+
[]
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def fetch_one_on_one_chats(token:)
|
|
127
|
+
conn = graph_connection(token: token)
|
|
128
|
+
all_chats = []
|
|
129
|
+
url = 'me/chats'
|
|
130
|
+
params = { '$top' => 50 }
|
|
131
|
+
pages = 0
|
|
132
|
+
|
|
133
|
+
loop do
|
|
134
|
+
resp = conn.get(url, params)
|
|
135
|
+
body = resp.body || {}
|
|
136
|
+
chats = body.fetch('value', [])
|
|
137
|
+
all_chats.concat(chats)
|
|
138
|
+
pages += 1
|
|
139
|
+
|
|
140
|
+
next_link = body['@odata.nextLink']
|
|
141
|
+
break unless next_link
|
|
142
|
+
break if pages >= MAX_CHAT_PAGES
|
|
143
|
+
|
|
144
|
+
url = next_link
|
|
145
|
+
params = {}
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
one_on_one = all_chats.select { |c| c['chatType'] == 'oneOnOne' }
|
|
149
|
+
log.info("ApiIngest: fetched #{all_chats.size} chats (#{pages} pages), #{one_on_one.size} oneOnOne")
|
|
150
|
+
one_on_one
|
|
151
|
+
rescue StandardError => e
|
|
152
|
+
log.warn("ApiIngest: fetch_chats failed: #{e.message}")
|
|
153
|
+
[]
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def match_chat_to_person(chats:, person:, conn:)
|
|
157
|
+
email = person.dig('scoredEmailAddresses', 0, 'address')&.downcase
|
|
158
|
+
display_name = person['displayName']&.downcase
|
|
159
|
+
user_id = person['id']
|
|
160
|
+
return nil unless email || user_id || display_name
|
|
161
|
+
|
|
162
|
+
chats.find do |chat|
|
|
163
|
+
members_resp = conn.get("chats/#{chat['id']}/members")
|
|
164
|
+
members = (members_resp.body || {}).fetch('value', [])
|
|
165
|
+
members.any? do |m|
|
|
166
|
+
match_member?(m, email: email, user_id: user_id, display_name: display_name)
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
rescue StandardError => e
|
|
170
|
+
log.debug("ApiIngest: match_chat_to_person failed: #{e.message}")
|
|
171
|
+
nil
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def match_member?(member, email:, user_id:, display_name:)
|
|
175
|
+
return true if email && member['email']&.downcase == email
|
|
176
|
+
return true if user_id && member['userId'] == user_id
|
|
177
|
+
return true if email && member.dig('additionalData', 'email')&.downcase == email
|
|
178
|
+
|
|
179
|
+
member_name = member['displayName']&.downcase
|
|
180
|
+
return true if display_name && member_name && member_name == display_name
|
|
181
|
+
|
|
182
|
+
false
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def fetch_chat_messages(conn:, chat_id:, depth: 50)
|
|
186
|
+
hwm = get_extended_hwm(chat_id: chat_id)
|
|
187
|
+
params = { '$top' => depth, '$orderby' => 'createdDateTime desc' }
|
|
188
|
+
params['$filter'] = "createdDateTime gt #{hwm[:last_message_at]}" if hwm&.dig(:last_message_at)
|
|
189
|
+
|
|
190
|
+
resp = conn.get("chats/#{chat_id}/messages", params)
|
|
191
|
+
log.debug("ApiIngest: fetch_messages chat=#{chat_id} count=#{(resp.body || {}).fetch('value', []).size}")
|
|
192
|
+
(resp.body || {}).fetch('value', [])
|
|
193
|
+
rescue StandardError => e
|
|
194
|
+
log.warn("ApiIngest: fetch_messages failed for #{chat_id}: #{e.message}")
|
|
195
|
+
[]
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def extract_body_text(msg)
|
|
199
|
+
html = msg.dig('body', 'content') || ''
|
|
200
|
+
strip_html(html)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def strip_html(html)
|
|
204
|
+
return '' if html.nil? || html.empty?
|
|
205
|
+
|
|
206
|
+
html.gsub(/<[^>]+>/, ' ').gsub(' ', ' ').gsub('&', '&')
|
|
207
|
+
.gsub('<', '<').gsub('>', '>').gsub('"', '"')
|
|
208
|
+
.gsub(/\s+/, ' ').strip
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def bot_message_graph?(msg)
|
|
212
|
+
app = msg.dig('from', 'application')
|
|
213
|
+
return true if app && app['id']
|
|
214
|
+
|
|
215
|
+
user_type = msg.dig('from', 'user', 'userIdentityType')
|
|
216
|
+
%w[anonymousGuest azureCommunicationServicesUser].include?(user_type)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def store_graph_message(msg, text, person, chat_id, content_hash:, imprint_active: false)
|
|
220
|
+
sender = msg.dig('from', 'user', 'displayName') || person['displayName'] || 'Unknown'
|
|
221
|
+
compose_time = msg['createdDateTime']
|
|
222
|
+
|
|
223
|
+
domain_tags = build_graph_domain_tags(sender: sender, chat_id: chat_id,
|
|
224
|
+
compose_time: compose_time, content_hash: content_hash,
|
|
225
|
+
message_id: msg['id'])
|
|
226
|
+
|
|
227
|
+
memory_runner.store_trace(
|
|
228
|
+
type: :episodic,
|
|
229
|
+
content_payload: text,
|
|
230
|
+
domain_tags: domain_tags,
|
|
231
|
+
origin: :direct_experience,
|
|
232
|
+
confidence: 0.7,
|
|
233
|
+
emotional_valence: 0.1,
|
|
234
|
+
emotional_intensity: 0.2,
|
|
235
|
+
imprint_active: imprint_active
|
|
236
|
+
)
|
|
237
|
+
rescue StandardError => e
|
|
238
|
+
log.warn("ApiIngest: store trace failed: #{e.message}")
|
|
239
|
+
nil
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def build_graph_domain_tags(sender:, chat_id:, compose_time:, content_hash:, message_id:)
|
|
243
|
+
tags = %w[teams graph_api]
|
|
244
|
+
tags << "sender:#{sender}"
|
|
245
|
+
tags << "peer:#{sender}"
|
|
246
|
+
tags << "chat_id:#{chat_id}" if chat_id
|
|
247
|
+
tags << "hash:#{content_hash}" if content_hash
|
|
248
|
+
tags << "time:#{compose_time}" if compose_time
|
|
249
|
+
tags << "msg_id:#{message_id}" if message_id
|
|
250
|
+
tags
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def load_existing_hashes
|
|
254
|
+
store = Legion::Extensions::Agentic::Memory::Trace.shared_store
|
|
255
|
+
hashes = Set.new
|
|
256
|
+
store.all_traces(min_strength: 0.0).each do |trace|
|
|
257
|
+
trace[:domain_tags]&.each do |tag|
|
|
258
|
+
hashes << tag.delete_prefix('hash:') if tag.start_with?('hash:')
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
hashes
|
|
262
|
+
rescue StandardError => e
|
|
263
|
+
log.debug("ApiIngest: load_existing_hashes failed: #{e.message}")
|
|
264
|
+
Set.new
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def memory_available?
|
|
268
|
+
defined?(Legion::Extensions::Agentic::Memory::Trace::Runners::Traces)
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def memory_runner
|
|
272
|
+
@memory_runner ||= Object.new.extend(Legion::Extensions::Agentic::Memory::Trace::Runners::Traces)
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def flush_trace_store
|
|
276
|
+
store = Legion::Extensions::Agentic::Memory::Trace.shared_store
|
|
277
|
+
store.flush if store.respond_to?(:flush)
|
|
278
|
+
rescue StandardError => e
|
|
279
|
+
log.warn("ApiIngest: flush failed: #{e.message}")
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def coactivate_thread_traces(thread_groups)
|
|
283
|
+
return unless defined?(Legion::Extensions::Agentic::Memory::Trace::Helpers::Store)
|
|
284
|
+
|
|
285
|
+
store = Legion::Extensions::Agentic::Memory::Trace.shared_store
|
|
286
|
+
thread_groups.each_value do |trace_ids|
|
|
287
|
+
next if trace_ids.length < 2
|
|
288
|
+
|
|
289
|
+
trace_ids.each_cons(2) do |id_a, id_b|
|
|
290
|
+
store.record_coactivation(id_a, id_b)
|
|
291
|
+
rescue StandardError => e
|
|
292
|
+
log.debug("ApiIngest: coactivation link failed for #{id_a}/#{id_b}: #{e.message}")
|
|
293
|
+
nil
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
rescue StandardError => e
|
|
297
|
+
log.debug("ApiIngest: coactivation skipped: #{e.message}")
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def error_result(message)
|
|
301
|
+
{ result: { stored: 0, skipped: 0, error: message } }
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
end
|
|
@@ -42,6 +42,7 @@ module Legion
|
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
coactivate_thread_traces(thread_groups)
|
|
45
|
+
flush_trace_store if stored.positive?
|
|
45
46
|
|
|
46
47
|
{ result: { stored: stored, skipped: skipped, latest_time: latest_time } }
|
|
47
48
|
end
|
|
@@ -87,7 +88,10 @@ module Legion
|
|
|
87
88
|
|
|
88
89
|
def build_domain_tags(msg)
|
|
89
90
|
tags = ['teams']
|
|
90
|
-
|
|
91
|
+
if msg.sender
|
|
92
|
+
tags << "sender:#{msg.sender}"
|
|
93
|
+
tags << "peer:#{msg.sender}"
|
|
94
|
+
end
|
|
91
95
|
tags << "thread:#{msg.thread_topic}" if msg.thread_topic
|
|
92
96
|
tags << "thread_id:#{msg.thread_id}" if msg.thread_id
|
|
93
97
|
tags << "thread_type:#{msg.thread_type}" if msg.thread_type
|
|
@@ -97,17 +101,25 @@ module Legion
|
|
|
97
101
|
tags
|
|
98
102
|
end
|
|
99
103
|
|
|
104
|
+
def flush_trace_store
|
|
105
|
+
store = Legion::Extensions::Agentic::Memory::Trace.shared_store
|
|
106
|
+
store.flush if store.respond_to?(:flush)
|
|
107
|
+
rescue StandardError => e
|
|
108
|
+
log.warn("CacheIngest: flush failed: #{e.message}")
|
|
109
|
+
end
|
|
110
|
+
|
|
100
111
|
# Seed Hebbian coactivation links between messages in the same thread.
|
|
101
112
|
def coactivate_thread_traces(thread_groups)
|
|
102
113
|
return unless defined?(Legion::Extensions::Agentic::Memory::Trace::Helpers::Store)
|
|
103
114
|
|
|
104
|
-
store = Legion::Extensions::Agentic::Memory::Trace
|
|
115
|
+
store = Legion::Extensions::Agentic::Memory::Trace.shared_store
|
|
105
116
|
thread_groups.each_value do |trace_ids|
|
|
106
117
|
next if trace_ids.length < 2
|
|
107
118
|
|
|
108
119
|
trace_ids.each_cons(2) do |id_a, id_b|
|
|
109
120
|
store.record_coactivation(id_a, id_b)
|
|
110
|
-
rescue StandardError
|
|
121
|
+
rescue StandardError => e
|
|
122
|
+
log.debug("CacheIngest: coactivation link failed for #{id_a}/#{id_b}: #{e.message}")
|
|
111
123
|
nil
|
|
112
124
|
end
|
|
113
125
|
end
|
|
@@ -198,7 +198,8 @@ module Legion
|
|
|
198
198
|
members = (members_resp.body || {}).fetch('value', [])
|
|
199
199
|
members.any? { |m| m['email']&.downcase == email.downcase }
|
|
200
200
|
end
|
|
201
|
-
rescue StandardError
|
|
201
|
+
rescue StandardError => e
|
|
202
|
+
log.debug("ProfileIngest: find_chat_for_person failed: #{e.message}")
|
|
202
203
|
nil
|
|
203
204
|
end
|
|
204
205
|
|
|
@@ -209,7 +210,8 @@ module Legion
|
|
|
209
210
|
|
|
210
211
|
resp = conn.get("chats/#{chat_id}/messages", params)
|
|
211
212
|
(resp.body || {}).fetch('value', [])
|
|
212
|
-
rescue StandardError
|
|
213
|
+
rescue StandardError => e
|
|
214
|
+
log.warn("ProfileIngest: fetch_new_messages failed for #{chat_id}: #{e.message}")
|
|
213
215
|
[]
|
|
214
216
|
end
|
|
215
217
|
|
|
@@ -229,7 +231,8 @@ module Legion
|
|
|
229
231
|
elsif defined?(Legion::LLM)
|
|
230
232
|
Legion::LLM.ask(prompt: "#{definition[:prompt]}\n\nConversation with #{peer_name}:\n#{text}")
|
|
231
233
|
end
|
|
232
|
-
rescue StandardError
|
|
234
|
+
rescue StandardError => e
|
|
235
|
+
log.debug("ProfileIngest: extract_conversation failed: #{e.message}")
|
|
233
236
|
nil
|
|
234
237
|
end
|
|
235
238
|
|
|
@@ -20,6 +20,7 @@ require 'legion/extensions/microsoft_teams/runners/cache_ingest'
|
|
|
20
20
|
require 'legion/extensions/microsoft_teams/runners/people'
|
|
21
21
|
require 'legion/extensions/microsoft_teams/runners/profile_ingest'
|
|
22
22
|
require 'legion/extensions/microsoft_teams/runners/ownership'
|
|
23
|
+
require 'legion/extensions/microsoft_teams/runners/api_ingest'
|
|
23
24
|
|
|
24
25
|
# Helpers (bot)
|
|
25
26
|
require 'legion/extensions/microsoft_teams/helpers/high_water_mark'
|
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.14
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -37,6 +37,104 @@ dependencies:
|
|
|
37
37
|
- - ">="
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
39
|
version: '2.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: legion-cache
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: 1.3.11
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: 1.3.11
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: legion-crypt
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: 1.4.9
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: 1.4.9
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: legion-data
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - ">="
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: 1.4.17
|
|
75
|
+
type: :runtime
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - ">="
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: 1.4.17
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: legion-json
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - ">="
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: 1.2.1
|
|
89
|
+
type: :runtime
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - ">="
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: 1.2.1
|
|
96
|
+
- !ruby/object:Gem::Dependency
|
|
97
|
+
name: legion-logging
|
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
|
99
|
+
requirements:
|
|
100
|
+
- - ">="
|
|
101
|
+
- !ruby/object:Gem::Version
|
|
102
|
+
version: 1.3.2
|
|
103
|
+
type: :runtime
|
|
104
|
+
prerelease: false
|
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
106
|
+
requirements:
|
|
107
|
+
- - ">="
|
|
108
|
+
- !ruby/object:Gem::Version
|
|
109
|
+
version: 1.3.2
|
|
110
|
+
- !ruby/object:Gem::Dependency
|
|
111
|
+
name: legion-settings
|
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
|
113
|
+
requirements:
|
|
114
|
+
- - ">="
|
|
115
|
+
- !ruby/object:Gem::Version
|
|
116
|
+
version: 1.3.14
|
|
117
|
+
type: :runtime
|
|
118
|
+
prerelease: false
|
|
119
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
120
|
+
requirements:
|
|
121
|
+
- - ">="
|
|
122
|
+
- !ruby/object:Gem::Version
|
|
123
|
+
version: 1.3.14
|
|
124
|
+
- !ruby/object:Gem::Dependency
|
|
125
|
+
name: legion-transport
|
|
126
|
+
requirement: !ruby/object:Gem::Requirement
|
|
127
|
+
requirements:
|
|
128
|
+
- - ">="
|
|
129
|
+
- !ruby/object:Gem::Version
|
|
130
|
+
version: 1.3.9
|
|
131
|
+
type: :runtime
|
|
132
|
+
prerelease: false
|
|
133
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
134
|
+
requirements:
|
|
135
|
+
- - ">="
|
|
136
|
+
- !ruby/object:Gem::Version
|
|
137
|
+
version: 1.3.9
|
|
40
138
|
- !ruby/object:Gem::Dependency
|
|
41
139
|
name: snappy
|
|
42
140
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -75,6 +173,7 @@ files:
|
|
|
75
173
|
- docs/plans/2026-03-19-teams-token-lifecycle-implementation.md
|
|
76
174
|
- lex-microsoft_teams.gemspec
|
|
77
175
|
- lib/legion/extensions/microsoft_teams.rb
|
|
176
|
+
- lib/legion/extensions/microsoft_teams/actors/api_ingest.rb
|
|
78
177
|
- lib/legion/extensions/microsoft_teams/actors/auth_validator.rb
|
|
79
178
|
- lib/legion/extensions/microsoft_teams/actors/cache_bulk_ingest.rb
|
|
80
179
|
- lib/legion/extensions/microsoft_teams/actors/cache_sync.rb
|
|
@@ -105,6 +204,7 @@ files:
|
|
|
105
204
|
- lib/legion/extensions/microsoft_teams/local_cache/sstable_reader.rb
|
|
106
205
|
- lib/legion/extensions/microsoft_teams/runners/adaptive_cards.rb
|
|
107
206
|
- lib/legion/extensions/microsoft_teams/runners/ai_insights.rb
|
|
207
|
+
- lib/legion/extensions/microsoft_teams/runners/api_ingest.rb
|
|
108
208
|
- lib/legion/extensions/microsoft_teams/runners/auth.rb
|
|
109
209
|
- lib/legion/extensions/microsoft_teams/runners/bot.rb
|
|
110
210
|
- lib/legion/extensions/microsoft_teams/runners/cache_ingest.rb
|