lex-microsoft_teams 0.5.6 → 0.6.0
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 +16 -0
- data/CLAUDE.md +44 -8
- data/lib/legion/extensions/microsoft_teams/actors/incremental_sync.rb +64 -0
- data/lib/legion/extensions/microsoft_teams/actors/profile_ingest.rb +58 -0
- data/lib/legion/extensions/microsoft_teams/cli/auth.rb +75 -0
- data/lib/legion/extensions/microsoft_teams/client.rb +2 -0
- data/lib/legion/extensions/microsoft_teams/helpers/high_water_mark.rb +73 -0
- data/lib/legion/extensions/microsoft_teams/helpers/permission_guard.rb +63 -0
- data/lib/legion/extensions/microsoft_teams/helpers/transform_definitions.rb +52 -0
- data/lib/legion/extensions/microsoft_teams/runners/people.rb +33 -0
- data/lib/legion/extensions/microsoft_teams/runners/profile_ingest.rb +246 -0
- data/lib/legion/extensions/microsoft_teams/version.rb +1 -1
- data/lib/legion/extensions/microsoft_teams.rb +4 -0
- metadata +8 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 01446eb9c4949be87da72534f700b4df999d654c479807e10afe77d2c8d5f584
|
|
4
|
+
data.tar.gz: 6d749b6ee7d420fedfa517dbeb73ac321c2fef90333a6b4aaca65f1df6195aed
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8ed0d0cf1be02569570bcf7d415f6bd490358466f88f2b908166f4a3756c116073ce24959c01d43608a3d9bb3783a1fd9002394410692b07b70e574ae34b9967
|
|
7
|
+
data.tar.gz: 18ba3b8cbcdc980e871fae2824a5c24ce5752d6aa1b7c836c3c94351d68c05e94927ae9d995f3f0f3465e601b711f563fd243df743b1c15a1045332150725527
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.6.0] - 2026-03-20
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- `Runners::People` with `get_profile` and `list_people` (Graph API `/me` and `/me/people`)
|
|
7
|
+
- `Runners::ProfileIngest` four-phase pipeline (self, people, conversations, teams/meetings)
|
|
8
|
+
- `Helpers::PermissionGuard` circuit breaker for 403 errors with exponential backoff
|
|
9
|
+
- `Helpers::TransformDefinitions` for lex-transformer conversation extraction and person summary
|
|
10
|
+
- `Actors::ProfileIngest` (Once): four-phase data pipeline at boot after auth
|
|
11
|
+
- `Actors::IncrementalSync` (Every, 15min): periodic re-sync with HWM dedup
|
|
12
|
+
- `CLI::Auth` module for `legion lex teams auth login/status`
|
|
13
|
+
- Extended high-water mark with dual timestamps and procedural trace persistence
|
|
14
|
+
- `People.Read` delegated permission scope
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- `Helpers::HighWaterMark` extended with `get/set/update_extended_hwm`, trace persistence, restore
|
|
18
|
+
|
|
3
19
|
## [0.5.6] - 2026-03-19
|
|
4
20
|
|
|
5
21
|
### Added
|
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.
|
|
13
|
+
**Version**: 0.6.0
|
|
14
14
|
|
|
15
15
|
## Architecture
|
|
16
16
|
|
|
@@ -30,7 +30,9 @@ Legion::Extensions::MicrosoftTeams
|
|
|
30
30
|
│ ├── Meetings # Online meeting CRUD, join URL lookup, attendance reports
|
|
31
31
|
│ ├── Transcripts # Meeting transcript list/get/content (VTT/DOCX)
|
|
32
32
|
│ ├── LocalCache # Offline message extraction from local LevelDB cache
|
|
33
|
-
│
|
|
33
|
+
│ ├── CacheIngest # Ingest cached messages into lex-memory as episodic traces
|
|
34
|
+
│ ├── People # Graph API /me and /me/people (profile + relevant contacts)
|
|
35
|
+
│ └── ProfileIngest # Four-phase cognitive pipeline (self, people, conversations, teams/meetings)
|
|
34
36
|
├── Actors/
|
|
35
37
|
│ ├── CacheBulkIngest # Once: full cache ingest at startup (imprint window support)
|
|
36
38
|
│ ├── CacheSync # Every 5min: incremental ingest of new messages
|
|
@@ -38,7 +40,9 @@ Legion::Extensions::MicrosoftTeams
|
|
|
38
40
|
│ ├── ObservedChatPoller # Every 30s: polls subscribed human conversations (compliance-gated)
|
|
39
41
|
│ ├── MessageProcessor # Subscription: consumes AMQP queue, routes by mode
|
|
40
42
|
│ ├── AuthValidator # Once: validates/restores delegated tokens on boot (2s delay)
|
|
41
|
-
│
|
|
43
|
+
│ ├── TokenRefresher # Every 15min (configurable): keeps delegated tokens fresh
|
|
44
|
+
│ ├── ProfileIngest # Once (5s delay): four-phase data pipeline after auth
|
|
45
|
+
│ └── IncrementalSync # Every 15min: periodic re-sync with HWM dedup
|
|
42
46
|
├── Transport/
|
|
43
47
|
│ ├── Exchanges/Messages # teams.messages topic exchange
|
|
44
48
|
│ ├── Queues/MessagesProcess # teams.messages.process durable queue
|
|
@@ -54,10 +58,14 @@ Legion::Extensions::MicrosoftTeams
|
|
|
54
58
|
│ ├── SessionManager # Multi-turn LLM session lifecycle with lex-memory persistence
|
|
55
59
|
│ ├── TokenCache # In-memory OAuth token cache with pre-expiry refresh (app + delegated slots, authenticated?/previously_authenticated? predicates)
|
|
56
60
|
│ ├── SubscriptionRegistry # Conversation observation subscriptions (in-memory + lex-memory)
|
|
57
|
-
│ ├── BrowserAuth # Delegated OAuth orchestrator (PKCE, headless detection, browser launch)
|
|
58
|
-
│
|
|
61
|
+
│ ├── BrowserAuth # Delegated OAuth orchestrator (PKCE, headless detection, browser launch, API hook detection)
|
|
62
|
+
│ ├── CallbackServer # Ephemeral TCP server for OAuth redirect callback
|
|
63
|
+
│ ├── PermissionGuard # Circuit breaker for 403 errors with exponential backoff
|
|
64
|
+
│ └── TransformDefinitions # lex-transformer definitions for conversation extraction and person summary
|
|
59
65
|
├── Hooks/
|
|
60
66
|
│ └── Auth # OAuth callback hook (mount '/callback') → /api/hooks/lex/microsoft_teams/auth/callback
|
|
67
|
+
├── CLI/
|
|
68
|
+
│ └── Auth # CLI module for `legion lex teams auth login/status`
|
|
61
69
|
└── Client # Standalone client (includes all runners)
|
|
62
70
|
```
|
|
63
71
|
|
|
@@ -65,7 +73,7 @@ Legion::Extensions::MicrosoftTeams
|
|
|
65
73
|
|
|
66
74
|
Opt-in browser-based OAuth for delegated Microsoft Graph permissions. Two flows:
|
|
67
75
|
|
|
68
|
-
- **Authorization Code + PKCE** (primary): Opens browser for Entra ID login,
|
|
76
|
+
- **Authorization Code + PKCE** (primary): Opens browser for Entra ID login. When the Legion API is running, uses the hook URL (`/api/hooks/lex/microsoft_teams/auth/callback`) with `Legion::Events` for callback notification; otherwise falls back to an ephemeral local port via `CallbackServer`
|
|
69
77
|
- **Device Code** (fallback): Auto-selected in headless/SSH environments (no `DISPLAY`/`WAYLAND_DISPLAY`)
|
|
70
78
|
|
|
71
79
|
Tokens stored in Vault (`legionio/microsoft_teams/delegated_token`) with configurable pre-expiry silent refresh. CLI command: `legion auth teams`. Hook route: `GET|POST /api/hooks/lex/microsoft_teams/auth/callback` for daemon re-auth (routed through Ingress for RBAC/audit).
|
|
@@ -84,6 +92,33 @@ Configuration: `settings[:microsoft_teams][:auth][:delegated][:refresh_interval]
|
|
|
84
92
|
|
|
85
93
|
Design doc: `docs/plans/2026-03-19-teams-token-lifecycle-design.md`
|
|
86
94
|
|
|
95
|
+
## Cognitive Pipeline (v0.6.0)
|
|
96
|
+
|
|
97
|
+
Four-phase data ingestion that runs after delegated auth to build the agent's social context:
|
|
98
|
+
|
|
99
|
+
1. **Self** (`ingest_self`): Fetches `/me` profile and `/me/presence`, stores as identity trace
|
|
100
|
+
2. **People** (`ingest_people`): Fetches `/me/people` (top 25), stores each as semantic trace
|
|
101
|
+
3. **Conversations** (`ingest_conversations`): For top N people, fetches recent chat messages, stores as episodic traces
|
|
102
|
+
4. **Teams & Meetings** (`ingest_teams_and_meetings`): Fetches joined teams and recent meetings, stores as semantic + episodic traces
|
|
103
|
+
|
|
104
|
+
### Actors
|
|
105
|
+
|
|
106
|
+
- **ProfileIngest** (Once, 5s delay): Fires `full_ingest` after boot. Only enabled when lex-memory is available and a delegated token exists.
|
|
107
|
+
- **IncrementalSync** (Every, 15min): Fires `incremental_sync` using extended high-water marks for dedup. Configurable via `settings[:microsoft_teams][:ingest][:incremental_interval]`.
|
|
108
|
+
|
|
109
|
+
### Supporting Components
|
|
110
|
+
|
|
111
|
+
- **Runners::People**: Graph API `/me` and `/me/people` endpoints with `user_id:` flexibility
|
|
112
|
+
- **Helpers::PermissionGuard**: Circuit breaker for Graph API 403 errors with exponential backoff (60s → 5min → 30min → 2hr → 8hr cap). Wraps API calls via `guarded_request(endpoint) { block }`.
|
|
113
|
+
- **Helpers::TransformDefinitions**: Structured extraction schemas for lex-transformer (`conversation_extract`, `person_summary`)
|
|
114
|
+
- **Extended HWM**: `get/set/update_extended_hwm` with dual timestamps (last_message_at + last_ingested_at) and `persist_hwm_as_trace` / `restore_hwm_from_traces` for cross-boot memory
|
|
115
|
+
|
|
116
|
+
### CLI
|
|
117
|
+
|
|
118
|
+
`CLI::Auth` provides `legion lex teams auth login` and `legion lex teams auth status` via the LEX CLI manifest system. Uses `cli_alias: 'teams'` for short-form dispatch.
|
|
119
|
+
|
|
120
|
+
Design doc: `docs/plans/2026-03-20-teams-cognitive-pipeline-implementation.md`
|
|
121
|
+
|
|
87
122
|
## AI Bot (v0.2.0)
|
|
88
123
|
|
|
89
124
|
Two operating modes, both using polling (Graph API) with AMQP-based message routing:
|
|
@@ -153,7 +188,7 @@ Per-conversation overrides stored in lex-memory (system_prompt_append, llm model
|
|
|
153
188
|
- **Session persistence**: Multi-turn sessions flush to lex-memory on threshold (20 msgs), idle timeout (15 min), or shutdown. Restored on restart via summary + recent messages.
|
|
154
189
|
- **Token caching**: In-memory OAuth token cache refreshes 60 seconds before expiry. Both pollers share a `TokenCache` instance instead of hitting OAuth every cycle.
|
|
155
190
|
- **Subscription registry**: In-memory working set of observed conversations, persisted to lex-memory on change. No legion-data migration needed.
|
|
156
|
-
- **Design docs**: `docs/
|
|
191
|
+
- **Design docs**: `docs/work/completed/2026-03-15-teams-ai-bot-design.md`, `docs/work/completed/2026-03-15-teams-bot-commands-design.md`
|
|
157
192
|
|
|
158
193
|
### Bot Commands (v0.3.0)
|
|
159
194
|
|
|
@@ -193,6 +228,7 @@ Four distinct APIs accessed via Faraday + one local data source:
|
|
|
193
228
|
| `OnlineMeetings.Read` | Delegated | Read online meetings (user context) |
|
|
194
229
|
| `OnlineMeetings.Read.All` | Application | Read online meetings |
|
|
195
230
|
| `OnlineMeetingTranscript.Read.All` | Application/Delegated | Read meeting transcripts |
|
|
231
|
+
| `People.Read` | Delegated | Read relevant people for cognitive pipeline |
|
|
196
232
|
|
|
197
233
|
For bot scenarios, register the Entra app as a Teams Bot via Bot Framework portal.
|
|
198
234
|
|
|
@@ -215,7 +251,7 @@ Optional framework dependencies (guarded with `defined?`, not in gemspec):
|
|
|
215
251
|
|
|
216
252
|
```bash
|
|
217
253
|
bundle install
|
|
218
|
-
bundle exec rspec #
|
|
254
|
+
bundle exec rspec # 268 specs across 38 spec files (as of v0.6.0)
|
|
219
255
|
bundle exec rubocop # Clean
|
|
220
256
|
```
|
|
221
257
|
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module MicrosoftTeams
|
|
6
|
+
module Actor
|
|
7
|
+
class IncrementalSync < Legion::Extensions::Actors::Every
|
|
8
|
+
def runner_class = Legion::Extensions::MicrosoftTeams::Runners::ProfileIngest
|
|
9
|
+
def runner_function = 'incremental_sync'
|
|
10
|
+
def use_runner? = false
|
|
11
|
+
def check_subtask? = false
|
|
12
|
+
def generate_task? = false
|
|
13
|
+
def run_now? = false
|
|
14
|
+
|
|
15
|
+
def delay
|
|
16
|
+
settings = begin
|
|
17
|
+
Legion::Settings[:microsoft_teams]
|
|
18
|
+
rescue StandardError
|
|
19
|
+
{}
|
|
20
|
+
end
|
|
21
|
+
settings.dig(:ingest, :incremental_interval) || 900
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def enabled?
|
|
25
|
+
defined?(Legion::Extensions::Agentic::Memory::Trace::Runners::Traces) &&
|
|
26
|
+
token_available?
|
|
27
|
+
rescue StandardError
|
|
28
|
+
false
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def args
|
|
32
|
+
token = resolve_token
|
|
33
|
+
settings = begin
|
|
34
|
+
Legion::Settings[:microsoft_teams]
|
|
35
|
+
rescue StandardError
|
|
36
|
+
{}
|
|
37
|
+
end
|
|
38
|
+
ingest = settings[:ingest] || {}
|
|
39
|
+
{
|
|
40
|
+
token: token,
|
|
41
|
+
top_people: ingest.fetch(:top_people, 10),
|
|
42
|
+
message_depth: ingest.fetch(:message_depth, 50)
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def token_available?
|
|
49
|
+
resolve_token != nil
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def resolve_token
|
|
53
|
+
if defined?(Legion::Extensions::MicrosoftTeams::Helpers::TokenCache)
|
|
54
|
+
cache = Legion::Extensions::MicrosoftTeams::Helpers::TokenCache.new
|
|
55
|
+
cache.cached_delegated_token
|
|
56
|
+
end
|
|
57
|
+
rescue StandardError
|
|
58
|
+
nil
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module MicrosoftTeams
|
|
6
|
+
module Actor
|
|
7
|
+
class ProfileIngest < Legion::Extensions::Actors::Once
|
|
8
|
+
def runner_class = Legion::Extensions::MicrosoftTeams::Runners::ProfileIngest
|
|
9
|
+
def runner_function = 'full_ingest'
|
|
10
|
+
def use_runner? = false
|
|
11
|
+
def check_subtask? = false
|
|
12
|
+
def generate_task? = false
|
|
13
|
+
|
|
14
|
+
def delay
|
|
15
|
+
5.0
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def enabled?
|
|
19
|
+
defined?(Legion::Extensions::Agentic::Memory::Trace::Runners::Traces) &&
|
|
20
|
+
token_available?
|
|
21
|
+
rescue StandardError
|
|
22
|
+
false
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def args
|
|
26
|
+
token = resolve_token
|
|
27
|
+
settings = begin
|
|
28
|
+
Legion::Settings[:microsoft_teams]
|
|
29
|
+
rescue StandardError
|
|
30
|
+
{}
|
|
31
|
+
end
|
|
32
|
+
ingest = settings[:ingest] || {}
|
|
33
|
+
{
|
|
34
|
+
token: token,
|
|
35
|
+
top_people: ingest.fetch(:top_people, 10),
|
|
36
|
+
message_depth: ingest.fetch(:message_depth, 50)
|
|
37
|
+
}
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def token_available?
|
|
43
|
+
resolve_token != nil
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def resolve_token
|
|
47
|
+
if defined?(Legion::Extensions::MicrosoftTeams::Helpers::TokenCache)
|
|
48
|
+
cache = Legion::Extensions::MicrosoftTeams::Helpers::TokenCache.new
|
|
49
|
+
cache.cached_delegated_token
|
|
50
|
+
end
|
|
51
|
+
rescue StandardError
|
|
52
|
+
nil
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/microsoft_teams/helpers/browser_auth'
|
|
4
|
+
require 'legion/extensions/microsoft_teams/helpers/token_cache'
|
|
5
|
+
|
|
6
|
+
module Legion
|
|
7
|
+
module Extensions
|
|
8
|
+
module MicrosoftTeams
|
|
9
|
+
module CLI
|
|
10
|
+
class Auth
|
|
11
|
+
def self.cli_alias
|
|
12
|
+
'teams'
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.descriptions
|
|
16
|
+
{
|
|
17
|
+
login: 'Authenticate with Microsoft Teams via browser OAuth',
|
|
18
|
+
status: 'Show current Teams authentication state'
|
|
19
|
+
}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def login(tenant_id: nil, client_id: nil)
|
|
23
|
+
settings = resolve_settings
|
|
24
|
+
tid = tenant_id || settings[:tenant_id]
|
|
25
|
+
cid = client_id || settings[:client_id]
|
|
26
|
+
|
|
27
|
+
unless tid && cid
|
|
28
|
+
puts 'Error: tenant_id and client_id required (set in settings or pass as args)'
|
|
29
|
+
return
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
browser_auth = Helpers::BrowserAuth.new(tenant_id: tid, client_id: cid)
|
|
33
|
+
result = browser_auth.authenticate
|
|
34
|
+
|
|
35
|
+
if result&.dig(:access_token)
|
|
36
|
+
store_token(result)
|
|
37
|
+
puts 'Teams authenticated successfully.'
|
|
38
|
+
else
|
|
39
|
+
puts 'Teams authentication failed or was cancelled.'
|
|
40
|
+
end
|
|
41
|
+
rescue StandardError => e
|
|
42
|
+
puts "Error: #{e.message}"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def status
|
|
46
|
+
token_file = File.expand_path('~/.legionio/tokens/microsoft_teams.json')
|
|
47
|
+
if File.exist?(token_file)
|
|
48
|
+
puts 'Teams: authenticated (token file present)'
|
|
49
|
+
else
|
|
50
|
+
puts 'Teams: not authenticated'
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def resolve_settings
|
|
57
|
+
return {} unless defined?(Legion::Settings)
|
|
58
|
+
|
|
59
|
+
Legion::Settings[:microsoft_teams]&.dig(:auth) || {}
|
|
60
|
+
rescue StandardError
|
|
61
|
+
{}
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def store_token(result)
|
|
65
|
+
cache = Helpers::TokenCache.new
|
|
66
|
+
cache.store_delegated_token(result)
|
|
67
|
+
cache.save_to_vault
|
|
68
|
+
rescue StandardError
|
|
69
|
+
nil
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -13,6 +13,7 @@ require 'legion/extensions/microsoft_teams/runners/bot'
|
|
|
13
13
|
require 'legion/extensions/microsoft_teams/runners/presence'
|
|
14
14
|
require 'legion/extensions/microsoft_teams/runners/meetings'
|
|
15
15
|
require 'legion/extensions/microsoft_teams/runners/transcripts'
|
|
16
|
+
require 'legion/extensions/microsoft_teams/runners/people'
|
|
16
17
|
|
|
17
18
|
module Legion
|
|
18
19
|
module Extensions
|
|
@@ -33,6 +34,7 @@ module Legion
|
|
|
33
34
|
include Runners::Transcripts
|
|
34
35
|
include Runners::LocalCache
|
|
35
36
|
include Runners::CacheIngest
|
|
37
|
+
include Runners::People
|
|
36
38
|
|
|
37
39
|
attr_reader :opts
|
|
38
40
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
3
5
|
module Legion
|
|
4
6
|
module Extensions
|
|
5
7
|
module MicrosoftTeams
|
|
@@ -45,6 +47,77 @@ module Legion
|
|
|
45
47
|
set_hwm(chat_id: chat_id, timestamp: latest)
|
|
46
48
|
end
|
|
47
49
|
|
|
50
|
+
def get_extended_hwm(chat_id:)
|
|
51
|
+
key = "teams:ehwm:#{chat_id}"
|
|
52
|
+
raw = if cache_available?
|
|
53
|
+
Legion::Cache.get(key)
|
|
54
|
+
else
|
|
55
|
+
@ehwm_fallback ||= {}
|
|
56
|
+
@ehwm_fallback[key]
|
|
57
|
+
end
|
|
58
|
+
return nil unless raw
|
|
59
|
+
|
|
60
|
+
raw.is_a?(Hash) ? raw : ::JSON.parse(raw, symbolize_names: true)
|
|
61
|
+
rescue StandardError
|
|
62
|
+
nil
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def set_extended_hwm(chat_id:, last_message_at:, last_ingested_at:, message_count: 0)
|
|
66
|
+
key = "teams:ehwm:#{chat_id}"
|
|
67
|
+
value = { last_message_at: last_message_at, last_ingested_at: last_ingested_at,
|
|
68
|
+
message_count: message_count }
|
|
69
|
+
if cache_available?
|
|
70
|
+
Legion::Cache.set(key, ::JSON.dump(value), HWM_TTL)
|
|
71
|
+
else
|
|
72
|
+
@ehwm_fallback ||= {}
|
|
73
|
+
@ehwm_fallback[key] = value
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def update_extended_hwm(chat_id:, last_message_at:, new_message_count: 0, ingested: false)
|
|
78
|
+
existing = get_extended_hwm(chat_id: chat_id) || { last_message_at: nil, last_ingested_at: nil, message_count: 0 }
|
|
79
|
+
existing[:last_message_at] = last_message_at
|
|
80
|
+
existing[:message_count] = (existing[:message_count] || 0) + new_message_count
|
|
81
|
+
existing[:last_ingested_at] = Time.now.utc.iso8601 if ingested
|
|
82
|
+
set_extended_hwm(chat_id: chat_id, **existing)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def persist_hwm_as_trace(chat_id:)
|
|
86
|
+
hwm = get_extended_hwm(chat_id: chat_id)
|
|
87
|
+
return unless hwm
|
|
88
|
+
|
|
89
|
+
memory_runner.store_trace(
|
|
90
|
+
type: :procedural,
|
|
91
|
+
content_payload: ::JSON.dump({ chat_id: chat_id }.merge(hwm)),
|
|
92
|
+
domain_tags: ['teams', 'hwm', "chat:#{chat_id}"],
|
|
93
|
+
confidence: 1.0,
|
|
94
|
+
origin: :direct_experience
|
|
95
|
+
)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def restore_hwm_from_traces
|
|
99
|
+
traces = memory_runner.retrieve_by_domain(domain_tag: 'teams', min_strength: 0.0, limit: 500)
|
|
100
|
+
return unless traces.is_a?(Array)
|
|
101
|
+
|
|
102
|
+
traces.select { |t| t[:trace_type] == :procedural && t[:domain_tags]&.include?('hwm') }.each do |trace|
|
|
103
|
+
data = ::JSON.parse(trace[:content_payload], symbolize_names: true)
|
|
104
|
+
next unless data[:chat_id]
|
|
105
|
+
|
|
106
|
+
set_extended_hwm(chat_id: data[:chat_id], last_message_at: data[:last_message_at],
|
|
107
|
+
last_ingested_at: data[:last_ingested_at], message_count: data[:message_count] || 0)
|
|
108
|
+
end
|
|
109
|
+
rescue StandardError => e
|
|
110
|
+
log_warn("Failed to restore HWM from traces: #{e.message}") if respond_to?(:log_warn, true)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def memory_runner
|
|
114
|
+
@memory_runner ||= begin
|
|
115
|
+
runner = Object.new
|
|
116
|
+
runner.extend(Legion::Extensions::Agentic::Memory::Trace::Runners::Traces)
|
|
117
|
+
runner
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
48
121
|
private
|
|
49
122
|
|
|
50
123
|
def cache_available?
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module MicrosoftTeams
|
|
6
|
+
module Helpers
|
|
7
|
+
module PermissionGuard
|
|
8
|
+
BACKOFF_SCHEDULE = [60, 300, 1800, 7200, 28_800].freeze
|
|
9
|
+
|
|
10
|
+
def permission_denied?(endpoint)
|
|
11
|
+
denial = permission_denials[endpoint]
|
|
12
|
+
return false unless denial
|
|
13
|
+
|
|
14
|
+
Time.now.utc < denial[:retry_after]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def record_denial(endpoint, error_message)
|
|
18
|
+
denial = permission_denials[endpoint] || { count: 0 }
|
|
19
|
+
denial[:count] += 1
|
|
20
|
+
backoff = BACKOFF_SCHEDULE.fetch(denial[:count] - 1, BACKOFF_SCHEDULE.last)
|
|
21
|
+
denial[:retry_after] = Time.now.utc + backoff
|
|
22
|
+
permission_denials[endpoint] = denial
|
|
23
|
+
log_warn("Graph API permission denied for #{endpoint}: #{error_message}. " \
|
|
24
|
+
"Retry in #{backoff}s (attempt #{denial[:count]})")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def denial_info(endpoint)
|
|
28
|
+
permission_denials[endpoint]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def reset_denials!
|
|
32
|
+
@permission_denials = {}
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def guarded_request(endpoint)
|
|
36
|
+
return { skipped: true, endpoint: endpoint, reason: :permission_denied } if permission_denied?(endpoint)
|
|
37
|
+
|
|
38
|
+
result = yield
|
|
39
|
+
if result.is_a?(Hash) && result[:status] == 403
|
|
40
|
+
msg = result.dig(:result, 'error', 'message') || 'Unknown'
|
|
41
|
+
record_denial(endpoint, msg)
|
|
42
|
+
end
|
|
43
|
+
result
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def permission_denials
|
|
49
|
+
@permission_denials ||= {}
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def log_warn(msg)
|
|
53
|
+
if defined?(Legion::Logging)
|
|
54
|
+
Legion::Logging.warn(msg)
|
|
55
|
+
else
|
|
56
|
+
warn(msg)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module MicrosoftTeams
|
|
6
|
+
module Helpers
|
|
7
|
+
module TransformDefinitions
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def conversation_extract
|
|
11
|
+
{
|
|
12
|
+
name: 'teams.conversation.extract',
|
|
13
|
+
structured: true,
|
|
14
|
+
prompt: 'Analyze this conversation between two people. Extract their communication ' \
|
|
15
|
+
'style, recurring topics, the nature of their working relationship, and any ' \
|
|
16
|
+
'pending action items. Be concise.',
|
|
17
|
+
schema: {
|
|
18
|
+
type: :object,
|
|
19
|
+
properties: {
|
|
20
|
+
communication_style: { type: :string, description: 'How this person communicates (formal, casual, terse, detailed, etc.)' },
|
|
21
|
+
topics: { type: :array, items: { type: :string }, description: 'Recurring discussion topics' },
|
|
22
|
+
relationship_context: { type: :string, description: 'Nature of working relationship (manager, peer, cross-team, mentor, etc.)' },
|
|
23
|
+
action_items: { type: :array, items: { type: :string }, description: 'Pending tasks or follow-ups' }
|
|
24
|
+
},
|
|
25
|
+
required: %i[communication_style topics relationship_context action_items]
|
|
26
|
+
},
|
|
27
|
+
engine_options: { max_retries: 2 }
|
|
28
|
+
}
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def person_summary
|
|
32
|
+
{
|
|
33
|
+
name: 'teams.person.summary',
|
|
34
|
+
structured: true,
|
|
35
|
+
prompt: 'Given this person profile data from Microsoft Teams, write a brief summary ' \
|
|
36
|
+
'of who this person is and their role. Keep it factual and concise.',
|
|
37
|
+
schema: {
|
|
38
|
+
type: :object,
|
|
39
|
+
properties: {
|
|
40
|
+
summary: { type: :string, description: 'One-sentence summary of this person' },
|
|
41
|
+
role_category: { type: :string, description: 'Role category: engineering, management, design, product, support, other' }
|
|
42
|
+
},
|
|
43
|
+
required: %i[summary role_category]
|
|
44
|
+
},
|
|
45
|
+
engine_options: { max_retries: 1 }
|
|
46
|
+
}
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
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 People
|
|
10
|
+
include Legion::Extensions::MicrosoftTeams::Helpers::Client
|
|
11
|
+
|
|
12
|
+
def get_profile(user_id: 'me', **)
|
|
13
|
+
response = graph_connection(**).get(user_path(user_id).to_s)
|
|
14
|
+
{ result: response.body }
|
|
15
|
+
rescue StandardError => e
|
|
16
|
+
{ error: e.message }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def list_people(user_id: 'me', top: 25, **)
|
|
20
|
+
params = { '$top' => top }
|
|
21
|
+
response = graph_connection(**).get("#{user_path(user_id)}/people", params)
|
|
22
|
+
{ result: response.body }
|
|
23
|
+
rescue StandardError => e
|
|
24
|
+
{ error: e.message }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
|
|
28
|
+
Legion::Extensions::Helpers.const_defined?(:Lex)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'legion/extensions/microsoft_teams/helpers/client'
|
|
5
|
+
require 'legion/extensions/microsoft_teams/helpers/permission_guard'
|
|
6
|
+
require 'legion/extensions/microsoft_teams/helpers/high_water_mark'
|
|
7
|
+
require 'legion/extensions/microsoft_teams/helpers/transform_definitions'
|
|
8
|
+
|
|
9
|
+
module Legion
|
|
10
|
+
module Extensions
|
|
11
|
+
module MicrosoftTeams
|
|
12
|
+
module Runners
|
|
13
|
+
module ProfileIngest
|
|
14
|
+
include Helpers::Client
|
|
15
|
+
include Helpers::PermissionGuard
|
|
16
|
+
include Helpers::HighWaterMark
|
|
17
|
+
|
|
18
|
+
def full_ingest(token:, top_people: 10, message_depth: 50, **)
|
|
19
|
+
self_result = ingest_self(token: token)
|
|
20
|
+
people_result = ingest_people(token: token, top: 25)
|
|
21
|
+
people = people_result[:skipped] ? [] : (people_result[:people] || [])
|
|
22
|
+
conv_result = ingest_conversations(token: token, people: people,
|
|
23
|
+
top_people: top_people, message_depth: message_depth)
|
|
24
|
+
teams_result = ingest_teams_and_meetings(token: token)
|
|
25
|
+
|
|
26
|
+
{ self: self_result, people: people_result, conversations: conv_result, teams: teams_result }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def ingest_self(token:, **)
|
|
30
|
+
conn = graph_connection(token: token)
|
|
31
|
+
profile = conn.get('me').body
|
|
32
|
+
|
|
33
|
+
memory_runner.store_trace(
|
|
34
|
+
type: :identity,
|
|
35
|
+
content_payload: ::JSON.dump(profile),
|
|
36
|
+
domain_tags: %w[teams self owner],
|
|
37
|
+
confidence: 1.0,
|
|
38
|
+
origin: :direct_experience
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
presence = begin
|
|
42
|
+
conn.get('me/presence').body
|
|
43
|
+
rescue StandardError
|
|
44
|
+
{}
|
|
45
|
+
end
|
|
46
|
+
unless presence.empty?
|
|
47
|
+
memory_runner.store_trace(
|
|
48
|
+
type: :sensory,
|
|
49
|
+
content_payload: ::JSON.dump(presence),
|
|
50
|
+
domain_tags: %w[teams presence self],
|
|
51
|
+
confidence: 0.8,
|
|
52
|
+
origin: :direct_experience
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
{ profile: profile, presence: presence }
|
|
57
|
+
rescue StandardError => e
|
|
58
|
+
{ error: e.message }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def ingest_people(token:, top: 25, **)
|
|
62
|
+
return { skipped: true, reason: :permission_denied } if permission_denied?('/me/people')
|
|
63
|
+
|
|
64
|
+
conn = graph_connection(token: token)
|
|
65
|
+
resp = conn.get('me/people', { '$top' => top })
|
|
66
|
+
|
|
67
|
+
if resp.respond_to?(:status) && resp.status == 403
|
|
68
|
+
record_denial('/me/people', resp.body.dig('error', 'message') || 'Forbidden')
|
|
69
|
+
return { skipped: true, reason: :permission_denied }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
people = (resp.body || {}).fetch('value', [])
|
|
73
|
+
people.sort_by! { |p| -(p.dig('scoredEmailAddresses', 0, 'relevanceScore') || 0) }
|
|
74
|
+
|
|
75
|
+
people.each do |person|
|
|
76
|
+
name = person['displayName'] || 'Unknown'
|
|
77
|
+
memory_runner.store_trace(
|
|
78
|
+
type: :semantic,
|
|
79
|
+
content_payload: ::JSON.dump(person.slice('displayName', 'jobTitle', 'department',
|
|
80
|
+
'officeLocation', 'scoredEmailAddresses')),
|
|
81
|
+
domain_tags: ['teams', 'peer', "peer:#{name}"],
|
|
82
|
+
confidence: 0.7,
|
|
83
|
+
origin: :direct_experience
|
|
84
|
+
)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
{ people: people, count: people.length }
|
|
88
|
+
rescue StandardError => e
|
|
89
|
+
{ error: e.message, skipped: false }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def ingest_conversations(token:, people:, top_people: 10, message_depth: 50, **)
|
|
93
|
+
return { ingested: 0 } if people.empty?
|
|
94
|
+
|
|
95
|
+
conn = graph_connection(token: token)
|
|
96
|
+
chats_resp = conn.get('me/chats', { '$top' => 50 })
|
|
97
|
+
chats = (chats_resp.body || {}).fetch('value', [])
|
|
98
|
+
ingested = 0
|
|
99
|
+
|
|
100
|
+
people.first(top_people).each do |person|
|
|
101
|
+
email = person.dig('scoredEmailAddresses', 0, 'address')
|
|
102
|
+
next unless email
|
|
103
|
+
|
|
104
|
+
chat = find_chat_for_person(chats: chats, email: email, conn: conn)
|
|
105
|
+
next unless chat
|
|
106
|
+
|
|
107
|
+
messages = fetch_new_messages(conn: conn, chat_id: chat['id'], depth: message_depth)
|
|
108
|
+
next if messages.empty?
|
|
109
|
+
|
|
110
|
+
extraction = extract_conversation(messages: messages, peer_name: person['displayName'])
|
|
111
|
+
next unless extraction
|
|
112
|
+
|
|
113
|
+
memory_runner.store_trace(
|
|
114
|
+
type: :episodic,
|
|
115
|
+
content_payload: ::JSON.dump({
|
|
116
|
+
peer: person['displayName'], chat_id: chat['id'],
|
|
117
|
+
summary: extraction, last_active: messages.first&.dig('createdDateTime')
|
|
118
|
+
}),
|
|
119
|
+
domain_tags: ['teams', 'conversation', "peer:#{person['displayName']}"],
|
|
120
|
+
confidence: 0.6,
|
|
121
|
+
origin: :direct_experience
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
update_extended_hwm(chat_id: chat['id'],
|
|
125
|
+
last_message_at: messages.map { |m| m['createdDateTime'] }.max,
|
|
126
|
+
new_message_count: messages.length, ingested: true)
|
|
127
|
+
persist_hwm_as_trace(chat_id: chat['id'])
|
|
128
|
+
ingested += 1
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
{ ingested: ingested }
|
|
132
|
+
rescue StandardError => e
|
|
133
|
+
{ error: e.message, ingested: ingested || 0 }
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def ingest_teams_and_meetings(token:, **)
|
|
137
|
+
conn = graph_connection(token: token)
|
|
138
|
+
teams_count = 0
|
|
139
|
+
|
|
140
|
+
unless permission_denied?('/me/joinedTeams')
|
|
141
|
+
teams_resp = conn.get('me/joinedTeams')
|
|
142
|
+
teams = (teams_resp.body || {}).fetch('value', [])
|
|
143
|
+
|
|
144
|
+
teams.each do |team|
|
|
145
|
+
members_resp = conn.get("teams/#{team['id']}/members")
|
|
146
|
+
members = (members_resp.body || {}).fetch('value', [])
|
|
147
|
+
memory_runner.store_trace(
|
|
148
|
+
type: :semantic,
|
|
149
|
+
content_payload: ::JSON.dump({ team: team['displayName'], member_count: members.length,
|
|
150
|
+
members: members.map { |m| m['displayName'] } }),
|
|
151
|
+
domain_tags: ['teams', 'org', "team:#{team['displayName']}"],
|
|
152
|
+
confidence: 0.8,
|
|
153
|
+
origin: :direct_experience
|
|
154
|
+
)
|
|
155
|
+
teams_count += 1
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
meetings_count = 0
|
|
160
|
+
unless permission_denied?('/me/onlineMeetings')
|
|
161
|
+
meetings_resp = conn.get('me/onlineMeetings')
|
|
162
|
+
meetings = (meetings_resp.body || {}).fetch('value', [])
|
|
163
|
+
meetings.each do |meeting|
|
|
164
|
+
memory_runner.store_trace(
|
|
165
|
+
type: :episodic,
|
|
166
|
+
content_payload: ::JSON.dump(meeting.slice('subject', 'startDateTime', 'endDateTime',
|
|
167
|
+
'participants')),
|
|
168
|
+
domain_tags: %w[teams meeting],
|
|
169
|
+
confidence: 0.5,
|
|
170
|
+
origin: :direct_experience
|
|
171
|
+
)
|
|
172
|
+
meetings_count += 1
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
{ teams: teams_count, meetings: meetings_count }
|
|
177
|
+
rescue StandardError => e
|
|
178
|
+
{ error: e.message }
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def incremental_sync(token:, top_people: 10, message_depth: 50, **)
|
|
182
|
+
ingest_self(token: token)
|
|
183
|
+
people_result = ingest_people(token: token, top: 25)
|
|
184
|
+
people = people_result[:skipped] ? [] : (people_result[:people] || [])
|
|
185
|
+
|
|
186
|
+
return { refreshed: true, conversations: 0 } if people.empty?
|
|
187
|
+
|
|
188
|
+
ingest_conversations(token: token, people: people,
|
|
189
|
+
top_people: top_people, message_depth: message_depth)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
private
|
|
193
|
+
|
|
194
|
+
def find_chat_for_person(chats:, email:, conn:)
|
|
195
|
+
chats.select { |c| c['chatType'] == 'oneOnOne' }.find do |chat|
|
|
196
|
+
members_resp = conn.get("chats/#{chat['id']}/members")
|
|
197
|
+
members = (members_resp.body || {}).fetch('value', [])
|
|
198
|
+
members.any? { |m| m['email']&.downcase == email.downcase }
|
|
199
|
+
end
|
|
200
|
+
rescue StandardError
|
|
201
|
+
nil
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def fetch_new_messages(conn:, chat_id:, depth: 50)
|
|
205
|
+
hwm = get_extended_hwm(chat_id: chat_id)
|
|
206
|
+
params = { '$top' => depth, '$orderby' => 'createdDateTime desc' }
|
|
207
|
+
params['$filter'] = "createdDateTime gt #{hwm[:last_message_at]}" if hwm&.dig(:last_message_at)
|
|
208
|
+
|
|
209
|
+
resp = conn.get("chats/#{chat_id}/messages", params)
|
|
210
|
+
(resp.body || {}).fetch('value', [])
|
|
211
|
+
rescue StandardError
|
|
212
|
+
[]
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def extract_conversation(messages:, peer_name:)
|
|
216
|
+
return nil if messages.empty?
|
|
217
|
+
|
|
218
|
+
definition = Helpers::TransformDefinitions.conversation_extract
|
|
219
|
+
text = messages.map do |m|
|
|
220
|
+
from = m.dig('from', 'user', 'displayName') || 'Unknown'
|
|
221
|
+
"#{from}: #{m['body']&.dig('content') || ''}"
|
|
222
|
+
end.join("\n")
|
|
223
|
+
|
|
224
|
+
if defined?(Legion::Extensions::Transformer::Client)
|
|
225
|
+
client = Legion::Extensions::Transformer::Client.new
|
|
226
|
+
result = client.transform(text: text, **definition)
|
|
227
|
+
result[:result] || result[:error] ? nil : result
|
|
228
|
+
elsif defined?(Legion::LLM)
|
|
229
|
+
Legion::LLM.ask(prompt: "#{definition[:prompt]}\n\nConversation with #{peer_name}:\n#{text}")
|
|
230
|
+
end
|
|
231
|
+
rescue StandardError
|
|
232
|
+
nil
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def memory_runner
|
|
236
|
+
@memory_runner ||= begin
|
|
237
|
+
runner = Object.new
|
|
238
|
+
runner.extend(Legion::Extensions::Agentic::Memory::Trace::Runners::Traces)
|
|
239
|
+
runner
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|
|
@@ -16,6 +16,8 @@ require 'legion/extensions/microsoft_teams/runners/meetings'
|
|
|
16
16
|
require 'legion/extensions/microsoft_teams/runners/transcripts'
|
|
17
17
|
require 'legion/extensions/microsoft_teams/runners/local_cache'
|
|
18
18
|
require 'legion/extensions/microsoft_teams/runners/cache_ingest'
|
|
19
|
+
require 'legion/extensions/microsoft_teams/runners/people'
|
|
20
|
+
require 'legion/extensions/microsoft_teams/runners/profile_ingest'
|
|
19
21
|
|
|
20
22
|
# Helpers (bot)
|
|
21
23
|
require 'legion/extensions/microsoft_teams/helpers/high_water_mark'
|
|
@@ -25,6 +27,8 @@ require 'legion/extensions/microsoft_teams/helpers/subscription_registry'
|
|
|
25
27
|
require 'legion/extensions/microsoft_teams/helpers/token_cache'
|
|
26
28
|
require 'legion/extensions/microsoft_teams/helpers/callback_server'
|
|
27
29
|
require 'legion/extensions/microsoft_teams/helpers/browser_auth'
|
|
30
|
+
require 'legion/extensions/microsoft_teams/helpers/permission_guard'
|
|
31
|
+
require 'legion/extensions/microsoft_teams/helpers/transform_definitions'
|
|
28
32
|
|
|
29
33
|
# Transport
|
|
30
34
|
if defined?(Legion::Transport)
|
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.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -79,18 +79,23 @@ files:
|
|
|
79
79
|
- lib/legion/extensions/microsoft_teams/actors/cache_bulk_ingest.rb
|
|
80
80
|
- lib/legion/extensions/microsoft_teams/actors/cache_sync.rb
|
|
81
81
|
- lib/legion/extensions/microsoft_teams/actors/direct_chat_poller.rb
|
|
82
|
+
- lib/legion/extensions/microsoft_teams/actors/incremental_sync.rb
|
|
82
83
|
- lib/legion/extensions/microsoft_teams/actors/message_processor.rb
|
|
83
84
|
- lib/legion/extensions/microsoft_teams/actors/observed_chat_poller.rb
|
|
85
|
+
- lib/legion/extensions/microsoft_teams/actors/profile_ingest.rb
|
|
84
86
|
- lib/legion/extensions/microsoft_teams/actors/token_refresher.rb
|
|
87
|
+
- lib/legion/extensions/microsoft_teams/cli/auth.rb
|
|
85
88
|
- lib/legion/extensions/microsoft_teams/client.rb
|
|
86
89
|
- lib/legion/extensions/microsoft_teams/helpers/browser_auth.rb
|
|
87
90
|
- lib/legion/extensions/microsoft_teams/helpers/callback_server.rb
|
|
88
91
|
- lib/legion/extensions/microsoft_teams/helpers/client.rb
|
|
89
92
|
- lib/legion/extensions/microsoft_teams/helpers/high_water_mark.rb
|
|
93
|
+
- lib/legion/extensions/microsoft_teams/helpers/permission_guard.rb
|
|
90
94
|
- lib/legion/extensions/microsoft_teams/helpers/prompt_resolver.rb
|
|
91
95
|
- lib/legion/extensions/microsoft_teams/helpers/session_manager.rb
|
|
92
96
|
- lib/legion/extensions/microsoft_teams/helpers/subscription_registry.rb
|
|
93
97
|
- lib/legion/extensions/microsoft_teams/helpers/token_cache.rb
|
|
98
|
+
- lib/legion/extensions/microsoft_teams/helpers/transform_definitions.rb
|
|
94
99
|
- lib/legion/extensions/microsoft_teams/hooks/auth.rb
|
|
95
100
|
- lib/legion/extensions/microsoft_teams/local_cache/extractor.rb
|
|
96
101
|
- lib/legion/extensions/microsoft_teams/local_cache/record_parser.rb
|
|
@@ -105,7 +110,9 @@ files:
|
|
|
105
110
|
- lib/legion/extensions/microsoft_teams/runners/local_cache.rb
|
|
106
111
|
- lib/legion/extensions/microsoft_teams/runners/meetings.rb
|
|
107
112
|
- lib/legion/extensions/microsoft_teams/runners/messages.rb
|
|
113
|
+
- lib/legion/extensions/microsoft_teams/runners/people.rb
|
|
108
114
|
- lib/legion/extensions/microsoft_teams/runners/presence.rb
|
|
115
|
+
- lib/legion/extensions/microsoft_teams/runners/profile_ingest.rb
|
|
109
116
|
- lib/legion/extensions/microsoft_teams/runners/subscriptions.rb
|
|
110
117
|
- lib/legion/extensions/microsoft_teams/runners/teams.rb
|
|
111
118
|
- lib/legion/extensions/microsoft_teams/runners/transcripts.rb
|