@memberjunction/ai-bridge-teams 0.0.1 → 5.41.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.
- package/README.md +109 -43
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/teams-bridge.d.ts +155 -0
- package/dist/teams-bridge.d.ts.map +1 -0
- package/dist/teams-bridge.js +356 -0
- package/dist/teams-bridge.js.map +1 -0
- package/dist/teams-meeting-controls.d.ts +80 -0
- package/dist/teams-meeting-controls.d.ts.map +1 -0
- package/dist/teams-meeting-controls.js +130 -0
- package/dist/teams-meeting-controls.js.map +1 -0
- package/dist/teams-sdk.d.ts +173 -0
- package/dist/teams-sdk.d.ts.map +1 -0
- package/dist/teams-sdk.js +35 -0
- package/dist/teams-sdk.js.map +1 -0
- package/package.json +32 -7
package/README.md
CHANGED
|
@@ -1,45 +1,111 @@
|
|
|
1
1
|
# @memberjunction/ai-bridge-teams
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
##
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
3
|
+
The **Microsoft Teams** Realtime Bridge driver in MemberJunction's Realtime Bridges program. It connects
|
|
4
|
+
the one realtime agent engine to a **Teams meeting**: bidirectional audio, a diarized participant roster,
|
|
5
|
+
participant mute, Teams meeting chat, and a **Meeting Controls** facilitator channel — all behind an
|
|
6
|
+
injectable Teams calling-bot SDK seam so the driver builds and unit-tests with **no network and no real
|
|
7
|
+
Teams / Azure Communication Services SDK**. It is a structural mirror of the reference
|
|
8
|
+
[`@memberjunction/ai-bridge-zoom`](../BridgeZoom/README.md) driver.
|
|
9
|
+
|
|
10
|
+
See the [Realtime Bridges Guide](../../../../guides/REALTIME_BRIDGES_GUIDE.md) and
|
|
11
|
+
[`/plans/realtime/realtime-bridges-architecture.md`](../../../../plans/realtime/realtime-bridges-architecture.md)
|
|
12
|
+
(§3 provider abstraction, §4b channels, §8 Microsoft Teams capability row) for the full architecture.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @memberjunction/ai-bridge-teams
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## What it provides
|
|
21
|
+
|
|
22
|
+
- **`TeamsBridge`** — `@RegisterClass(BaseRealtimeBridge, 'TeamsBridge')`. The `MJ: AI Bridge Providers`
|
|
23
|
+
row with `DriverClass = 'TeamsBridge'` resolves to this driver via the `ClassFactory`. Implements the
|
|
24
|
+
four `BaseRealtimeBridge` abstracts (`Connect` / `Disconnect` / `SendMedia` / `OnMedia`) and the
|
|
25
|
+
capability-gated virtuals Teams supports (`GetParticipants`, `OnParticipantChange`), plus
|
|
26
|
+
`GetMeetingControlsEventSource` for the facilitator channel and a `PostChatMessage` helper.
|
|
27
|
+
- **`ITeamsMeetingSdk`** — the **injectable seam** the driver depends on instead of the real SDK.
|
|
28
|
+
- **`TeamsMeetingControlsEventSource`** — adapts the seam's roster / hand-raise / speaking / mute into
|
|
29
|
+
the bridge's `IBridgeMeetingControlsEventSource`, so the engine wires the Meeting Controls channel.
|
|
30
|
+
|
|
31
|
+
## Capability coverage (the Microsoft Teams seed row)
|
|
32
|
+
|
|
33
|
+
| Capability | Status |
|
|
34
|
+
|---|---|
|
|
35
|
+
| On-demand + scheduled + invite + **native invite** join | ✅ |
|
|
36
|
+
| Inbound routing | ✅ |
|
|
37
|
+
| Audio in / out | ✅ |
|
|
38
|
+
| Video in/out, Screen in/out (directional flags) | ✅ (transport carries them; models light audio first) |
|
|
39
|
+
| Speaker diarization (roster + per-speaker labels) | ✅ |
|
|
40
|
+
| Participant mute (Meeting Controls) | ✅ |
|
|
41
|
+
| Teams meeting chat | ✅ |
|
|
42
|
+
| Native raised-hand | ⚠️ Partial — wired where the calling-bot API surfaces it; tolerant of it never firing |
|
|
43
|
+
| DTMF / call transfer / recording | ➖ not Teams-meeting features here — the gated base methods throw `BridgeCapabilityNotSupportedError` |
|
|
44
|
+
|
|
45
|
+
Capability gating is two-layer (defense-in-depth): the engine checks the provider's `SupportedFeatures`
|
|
46
|
+
first, and the driver re-asserts each flag with `RequireFeature` at the top of its overrides.
|
|
47
|
+
|
|
48
|
+
### Teams vs. Zoom capability differences
|
|
49
|
+
|
|
50
|
+
Teams advertises everything Zoom does, **plus** `InviteJoin`, `NativeInvite`, and `InboundRouting` (the
|
|
51
|
+
calendar-invite / marketplace-native-invite / inbound-routing UX), and its native raised-hand is **partial**
|
|
52
|
+
rather than fully reliable. Everything else (audio in/out, directional video/screen, diarized roster,
|
|
53
|
+
mute, meeting chat) maps one-to-one onto the Zoom reference driver.
|
|
54
|
+
|
|
55
|
+
## The Teams calling-bot SDK seam (`ITeamsMeetingSdk`)
|
|
56
|
+
|
|
57
|
+
The driver never imports the real Teams / ACS SDK. It depends only on this minimal interface, named after
|
|
58
|
+
Microsoft Teams / Azure Communication Services calling-bot concepts:
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
export interface ITeamsMeetingSdk {
|
|
62
|
+
join(args: TeamsJoinArgs): Promise<TeamsJoinResult>;
|
|
63
|
+
leave(): Promise<void>;
|
|
64
|
+
sendAudioFrame(pcm: ArrayBuffer): void; // agent's voice out (outbound audio socket)
|
|
65
|
+
onAudioFrame(cb: (frame: TeamsAudioFrame) => void): void; // raw per-participant audio in (diarization)
|
|
66
|
+
onParticipantJoin(cb: (p: TeamsParticipant) => void): void;
|
|
67
|
+
onParticipantLeave(cb: (id: string) => void): void;
|
|
68
|
+
onHandRaise(cb: (id: string, raised: boolean) => void): void; // ⚠️ partial on Teams
|
|
69
|
+
getParticipants(): Promise<TeamsParticipant[]>;
|
|
70
|
+
postChatMessage(text: string): Promise<void>; // Teams meeting chat thread
|
|
71
|
+
muteParticipant(participantId: string): Promise<void>;
|
|
72
|
+
onMeetingEnded(cb: () => void): void;
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Production binding (deployment TODO)
|
|
77
|
+
|
|
78
|
+
In production this is bound to the **Azure Communication Services (ACS) calling-bot** SDK plus the
|
|
79
|
+
**Microsoft Graph cloud-communications API** (`/communications/calls`, application-hosted media) for
|
|
80
|
+
per-participant PCM audio, roster events, mute, and Teams meeting-chat posting. Supply a factory via the
|
|
81
|
+
creation seam:
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
import { TeamsBridge } from '@memberjunction/ai-bridge-teams';
|
|
85
|
+
|
|
86
|
+
// Once, where bridge drivers are configured:
|
|
87
|
+
// bridge.SetSdkFactory((config) => new RealTeamsSdkAdapter(config));
|
|
88
|
+
// The adapter implements ITeamsMeetingSdk over the real ACS/Graph SDK. The driver + its tests do not change.
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Out of the box, `TeamsBridge` ships **without** the real SDK adapter — `Connect` throws an explicit
|
|
92
|
+
"bind the real Microsoft Teams SDK" error until `SetSdkFactory` is called. Tests inject a `FakeTeamsSdk`.
|
|
93
|
+
|
|
94
|
+
## Usage (engine-driven)
|
|
95
|
+
|
|
96
|
+
The bridge is not used directly — `AIBridgeEngine.StartBridgeSession` (`@memberjunction/ai-bridge-server`)
|
|
97
|
+
resolves it from the provider's `DriverClass`, wires the transport seam to the injected
|
|
98
|
+
`IRealtimeSession`, and (when a channel host is supplied) wires the Meeting Controls channel from
|
|
99
|
+
`GetMeetingControlsEventSource`. See the bridge-server package and the guide's "Channel plane" section.
|
|
100
|
+
|
|
101
|
+
## Testing
|
|
102
|
+
|
|
103
|
+
`FakeTeamsSdk` (in `src/__tests__/`) is an in-memory `ITeamsMeetingSdk` with drive helpers and capture
|
|
104
|
+
sinks. The suite covers connect/disconnect (incl. parsing the meeting thread id out of the join URL),
|
|
105
|
+
audio in→`OnMedia` (speaker labels) and out→seam, participant join/leave → roster + event source, native
|
|
106
|
+
hand-raise → Meeting Controls perception, capability gating (a feature Teams lacks throws), and chat — all
|
|
107
|
+
with no network.
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
cd packages/AI/Providers/BridgeTeams && npm run test
|
|
111
|
+
```
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,0BAA0B,CAAC;AACzC,cAAc,gBAAgB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from './teams-sdk.js';
|
|
2
|
+
export * from './teams-meeting-controls.js';
|
|
3
|
+
export * from './teams-bridge.js';
|
|
4
|
+
import { LoadTeamsBridge } from './teams-bridge.js';
|
|
5
|
+
// Static reference so bundlers cannot tree-shake the @RegisterClass(BaseRealtimeBridge, 'TeamsBridge')
|
|
6
|
+
// registration. Calling the no-op here keeps the driver resolvable by the engine's ClassFactory.
|
|
7
|
+
LoadTeamsBridge();
|
|
8
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,0BAA0B,CAAC;AACzC,cAAc,gBAAgB,CAAC;AAE/B,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAEjD,uGAAuG;AACvG,iGAAiG;AACjG,eAAe,EAAE,CAAC"}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview `TeamsBridge` — the **Microsoft Teams** platform Realtime Bridge driver, connecting the
|
|
3
|
+
* one realtime agent engine to a **Teams meeting**. Implements the {@link BaseRealtimeBridge} contract
|
|
4
|
+
* against an injectable {@link ITeamsMeetingSdk} seam so it builds + unit-tests with NO network and NO
|
|
5
|
+
* real Teams / Azure Communication Services SDK (the Gemini `connectLiveSession` testability pattern).
|
|
6
|
+
* Structurally mirrors `ZoomBridge` (the reference driver) — only the platform names + join-coordinate
|
|
7
|
+
* parsing differ.
|
|
8
|
+
*
|
|
9
|
+
* Teams capability coverage (per the §8 seed row): on-demand + scheduled + invite + native-invite join,
|
|
10
|
+
* inbound routing, audio in/out (video/screen directional flags carried by transport), a diarized roster
|
|
11
|
+
* (`SpeakerDiarization`), participant mute + Teams meeting chat (via the Meeting Controls channel). Native
|
|
12
|
+
* raised-hand is ⚠️ partial over the calling-bot API — wired where the platform surfaces it, tolerant of
|
|
13
|
+
* it never firing. Telephony features (DTMF / transfer / recording) are NOT Teams-meeting features here,
|
|
14
|
+
* so those virtual base methods keep throwing `BridgeCapabilityNotSupportedError`.
|
|
15
|
+
*
|
|
16
|
+
* @module @memberjunction/ai-bridge-teams
|
|
17
|
+
* @author MemberJunction.com
|
|
18
|
+
*/
|
|
19
|
+
import { BaseRealtimeBridge, BridgeConnectResult, BridgeDisconnectReason, BridgeMediaFrame, BridgeMediaTrackKind, BridgeParticipantInfo, RealtimeBridgeContext, IBridgeMeetingControlsEventSource } from '@memberjunction/ai-bridge-base';
|
|
20
|
+
import { TeamsMeetingSdkFactory } from './teams-sdk.js';
|
|
21
|
+
/**
|
|
22
|
+
* The `DriverClass` key {@link TeamsBridge} registers under. A `MJ: AI Bridge Providers` row with
|
|
23
|
+
* `DriverClass = 'TeamsBridge'` resolves to this driver via the `ClassFactory`.
|
|
24
|
+
*/
|
|
25
|
+
export declare const TEAMS_BRIDGE_DRIVER_CLASS = "TeamsBridge";
|
|
26
|
+
/**
|
|
27
|
+
* Realtime Bridge driver for **Microsoft Teams meetings**.
|
|
28
|
+
*
|
|
29
|
+
* Construct the driver with the default constructor (the engine's `ClassFactory` path) — it lazily
|
|
30
|
+
* builds the Teams SDK from the {@link sdkFactory} at {@link Connect} time. Tests inject a `FakeTeamsSdk`
|
|
31
|
+
* by overriding the factory via {@link SetSdkFactory} (the creation seam) before connecting.
|
|
32
|
+
*
|
|
33
|
+
* Registered via `@RegisterClass(BaseRealtimeBridge, 'TeamsBridge')`.
|
|
34
|
+
*/
|
|
35
|
+
export declare class TeamsBridge extends BaseRealtimeBridge {
|
|
36
|
+
/** The live Teams SDK seam for this session, created at {@link Connect}. */
|
|
37
|
+
private sdk;
|
|
38
|
+
/** The bot's own participant id in the joined meeting (set at {@link Connect}). */
|
|
39
|
+
private botParticipantId;
|
|
40
|
+
/** The inbound-media handler registered via {@link OnMedia}; raw audio frames are forwarded to it. */
|
|
41
|
+
private mediaHandler?;
|
|
42
|
+
/** The roster-change handler registered via {@link OnParticipantChange}. */
|
|
43
|
+
private participantHandler?;
|
|
44
|
+
/** The Meeting Controls event source for this session (only when diarization is supported). */
|
|
45
|
+
private meetingControls;
|
|
46
|
+
/**
|
|
47
|
+
* The SDK creation seam. Defaults to a factory that throws an explicit "bind the real Teams SDK"
|
|
48
|
+
* error, since this package ships WITHOUT the real SDK adapter (a deployment concern). Production
|
|
49
|
+
* sets a real factory via {@link SetSdkFactory}; tests inject a `FakeTeamsSdk`.
|
|
50
|
+
*/
|
|
51
|
+
private sdkFactory;
|
|
52
|
+
/**
|
|
53
|
+
* Sets the {@link TeamsMeetingSdkFactory} this driver uses to construct its SDK seam at connect —
|
|
54
|
+
* the creation seam (mirroring Gemini's overridable `connectLiveSession`). Production binds the
|
|
55
|
+
* real Teams ACS calling-bot adapter here; tests inject a `FakeTeamsSdk`.
|
|
56
|
+
*
|
|
57
|
+
* @param factory The factory that builds the {@link ITeamsMeetingSdk} for a session.
|
|
58
|
+
*/
|
|
59
|
+
SetSdkFactory(factory: TeamsMeetingSdkFactory): void;
|
|
60
|
+
/**
|
|
61
|
+
* Joins the Teams meeting and brings the bot online. Captures the capability context, builds the SDK
|
|
62
|
+
* from the {@link sdkFactory}, wires the inbound raw-audio path and meeting-ended callback, joins,
|
|
63
|
+
* and (when the provider diarizes) constructs + seeds the Meeting Controls event source.
|
|
64
|
+
*
|
|
65
|
+
* @param ctx The bridge context (features, provider name, the join URL as `Address`, config).
|
|
66
|
+
* @returns The bot participant + call (external connection) identifiers, persisted by the engine.
|
|
67
|
+
*/
|
|
68
|
+
Connect(ctx: RealtimeBridgeContext): Promise<BridgeConnectResult>;
|
|
69
|
+
/**
|
|
70
|
+
* Leaves the Teams meeting and releases all SDK resources. Tolerant of teardown errors.
|
|
71
|
+
*
|
|
72
|
+
* @param _reason Why the disconnect happened (Teams teardown is uniform; the bot simply leaves).
|
|
73
|
+
*/
|
|
74
|
+
Disconnect(_reason: BridgeDisconnectReason): Promise<void>;
|
|
75
|
+
/**
|
|
76
|
+
* Sends an outbound media frame into the meeting. Audio is fed to the SDK's outbound audio-socket
|
|
77
|
+
* path; video/screen frames are gated by the directional capability flags (the transport carries
|
|
78
|
+
* them, and the SDK send is a deployment-time binding TODO — today the models emit audio).
|
|
79
|
+
*
|
|
80
|
+
* @param track The outbound track the frame targets.
|
|
81
|
+
* @param frame The media frame to send.
|
|
82
|
+
*/
|
|
83
|
+
SendMedia(track: BridgeMediaTrackKind, frame: BridgeMediaFrame): void;
|
|
84
|
+
/**
|
|
85
|
+
* Registers the inbound-media handler. The driver forwards each raw per-participant audio frame
|
|
86
|
+
* (with its speaker label) to this handler; the engine routes it to `IRealtimeSession.SendInput`.
|
|
87
|
+
*
|
|
88
|
+
* @param handler Invoked with each inbound media frame.
|
|
89
|
+
*/
|
|
90
|
+
OnMedia(handler: (frame: BridgeMediaFrame) => void): void;
|
|
91
|
+
/**
|
|
92
|
+
* Returns the current Teams participant roster (gated by `SpeakerDiarization`). Re-asserts the flag
|
|
93
|
+
* (defense-in-depth) so even an engine-bypassing caller cannot pull a roster a disabled provider
|
|
94
|
+
* forbids.
|
|
95
|
+
*
|
|
96
|
+
* @returns The current participants.
|
|
97
|
+
* @throws {BridgeCapabilityNotSupportedError} when diarization is not enabled for the provider.
|
|
98
|
+
*/
|
|
99
|
+
GetParticipants(): Promise<BridgeParticipantInfo[]>;
|
|
100
|
+
/**
|
|
101
|
+
* Registers a roster-change handler (gated by `SpeakerDiarization`). The driver fires it from the
|
|
102
|
+
* SDK's participant join/leave stream with the full current roster.
|
|
103
|
+
*
|
|
104
|
+
* @param handler Invoked with the updated participant list on each change.
|
|
105
|
+
* @throws {BridgeCapabilityNotSupportedError} when diarization is not enabled.
|
|
106
|
+
*/
|
|
107
|
+
OnParticipantChange(handler: (participants: BridgeParticipantInfo[]) => void): void;
|
|
108
|
+
/**
|
|
109
|
+
* Returns the Teams Meeting Controls event source for this session (roster · hand-raise · speaking ·
|
|
110
|
+
* mute), or `null` when diarization is off (no roster to facilitate). The engine wires the Meeting
|
|
111
|
+
* Controls channel from this.
|
|
112
|
+
*/
|
|
113
|
+
GetMeetingControlsEventSource(): IBridgeMeetingControlsEventSource | null;
|
|
114
|
+
/**
|
|
115
|
+
* Posts a message to the Teams meeting chat. Exposed for the channel plane / turn-taking hybrid
|
|
116
|
+
* mode (the social-cost-free "raise hand"). Best-effort — a chat failure is logged, never fatal.
|
|
117
|
+
*
|
|
118
|
+
* @param text The chat message to post.
|
|
119
|
+
*/
|
|
120
|
+
PostChatMessage(text: string): Promise<void>;
|
|
121
|
+
/** The bot's participant id in the joined meeting (or `null` before {@link Connect}). */
|
|
122
|
+
get BotParticipantId(): string | null;
|
|
123
|
+
/** Wires the SDK's inbound raw-audio callback to a diarized inbound {@link BridgeMediaFrame}. */
|
|
124
|
+
private wireInboundAudio;
|
|
125
|
+
/**
|
|
126
|
+
* Wires the SDK's participant join/leave + hand-raise streams ONCE (the driver is the single owner;
|
|
127
|
+
* the SDK seam is latest-handler-wins). Each roster change fans out to both the driver's own
|
|
128
|
+
* roster-change handler and the Meeting Controls source; hand-raise signals (⚠️ partial on Teams)
|
|
129
|
+
* feed the source.
|
|
130
|
+
*/
|
|
131
|
+
private wireRoster;
|
|
132
|
+
/** Pulls the current roster from the SDK and fans it out to both the participant handler + Meeting Controls. */
|
|
133
|
+
private refreshRoster;
|
|
134
|
+
/** Handles the SDK's meeting-ended signal: surface an empty roster so the engine sees everyone gone. */
|
|
135
|
+
private handleMeetingEnded;
|
|
136
|
+
/** Builds the SDK join args from the bridge context (Address is the meeting join URL). */
|
|
137
|
+
private buildJoinArgs;
|
|
138
|
+
/**
|
|
139
|
+
* Extracts the meeting thread id from a Teams `meetup-join` URL when present, else `undefined`. The
|
|
140
|
+
* thread id is the `19:meeting_...@thread.v2` segment embedded (URL-encoded) in the join link.
|
|
141
|
+
* Tolerant — a non-matching address yields `undefined` so the SDK can resolve coordinates itself.
|
|
142
|
+
*/
|
|
143
|
+
private parseThreadId;
|
|
144
|
+
/** Returns the PCM bytes of an outbound audio frame, preferring the binary payload. */
|
|
145
|
+
private framePcm;
|
|
146
|
+
/** Decodes a base64 payload to an `ArrayBuffer` (Node `Buffer` fast path, `atob` fallback). */
|
|
147
|
+
private decodeBase64;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Tree-shaking-prevention loader. Modern bundlers cannot see the `@RegisterClass` dynamic registration
|
|
151
|
+
* of {@link TeamsBridge} and may eliminate it. Import and call this no-op from a static code path
|
|
152
|
+
* (the package entry point does) so the `ClassFactory` can resolve `'TeamsBridge'`.
|
|
153
|
+
*/
|
|
154
|
+
export declare function LoadTeamsBridge(): void;
|
|
155
|
+
//# sourceMappingURL=teams-bridge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"teams-bridge.d.ts","sourceRoot":"","sources":["../src/teams-bridge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAIH,OAAO,EACH,kBAAkB,EAClB,mBAAmB,EACnB,sBAAsB,EACtB,gBAAgB,EAChB,oBAAoB,EACpB,qBAAqB,EAErB,qBAAqB,EACrB,iCAAiC,EACpC,MAAM,gCAAgC,CAAC;AACxC,OAAO,EAEH,sBAAsB,EAKzB,MAAM,aAAa,CAAC;AAGrB;;;GAGG;AACH,eAAO,MAAM,yBAAyB,gBAAgB,CAAC;AA8BvD;;;;;;;;GAQG;AACH,qBACa,WAAY,SAAQ,kBAAkB;IAC/C,4EAA4E;IAC5E,OAAO,CAAC,GAAG,CAAiC;IAE5C,mFAAmF;IACnF,OAAO,CAAC,gBAAgB,CAAuB;IAE/C,sGAAsG;IACtG,OAAO,CAAC,YAAY,CAAC,CAAoC;IAEzD,4EAA4E;IAC5E,OAAO,CAAC,kBAAkB,CAAC,CAAkD;IAE7E,+FAA+F;IAC/F,OAAO,CAAC,eAAe,CAAgD;IAEvE;;;;OAIG;IACH,OAAO,CAAC,UAAU,CAMhB;IAEF;;;;;;OAMG;IACI,aAAa,CAAC,OAAO,EAAE,sBAAsB,GAAG,IAAI;IAM3D;;;;;;;OAOG;IACU,OAAO,CAAC,GAAG,EAAE,qBAAqB,GAAG,OAAO,CAAC,mBAAmB,CAAC;IA2B9E;;;;OAIG;IACU,UAAU,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,IAAI,CAAC;IAgBvE;;;;;;;OAOG;IACI,SAAS,CAAC,KAAK,EAAE,oBAAoB,EAAE,KAAK,EAAE,gBAAgB,GAAG,IAAI;IAuB5E;;;;;OAKG;IACI,OAAO,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,gBAAgB,KAAK,IAAI,GAAG,IAAI;IAMhE;;;;;;;OAOG;IACmB,eAAe,IAAI,OAAO,CAAC,qBAAqB,EAAE,CAAC;IASzE;;;;;;OAMG;IACa,mBAAmB,CAAC,OAAO,EAAE,CAAC,YAAY,EAAE,qBAAqB,EAAE,KAAK,IAAI,GAAG,IAAI;IAKnG;;;;OAIG;IACa,6BAA6B,IAAI,iCAAiC,GAAG,IAAI;IAMzF;;;;;OAKG;IACU,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAWzD,yFAAyF;IACzF,IAAW,gBAAgB,IAAI,MAAM,GAAG,IAAI,CAE3C;IAID,iGAAiG;IACjG,OAAO,CAAC,gBAAgB;IAWxB;;;;;OAKG;IACH,OAAO,CAAC,UAAU;IAMlB,gHAAgH;YAClG,aAAa;IAU3B,wGAAwG;IACxG,OAAO,CAAC,kBAAkB;IAK1B,0FAA0F;IAC1F,OAAO,CAAC,aAAa;IAWrB;;;;OAIG;IACH,OAAO,CAAC,aAAa;IAerB,uFAAuF;IACvF,OAAO,CAAC,QAAQ;IAUhB,+FAA+F;IAC/F,OAAO,CAAC,YAAY;CAevB;AAED;;;;GAIG;AACH,wBAAgB,eAAe,IAAI,IAAI,CAEtC"}
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview `TeamsBridge` — the **Microsoft Teams** platform Realtime Bridge driver, connecting the
|
|
3
|
+
* one realtime agent engine to a **Teams meeting**. Implements the {@link BaseRealtimeBridge} contract
|
|
4
|
+
* against an injectable {@link ITeamsMeetingSdk} seam so it builds + unit-tests with NO network and NO
|
|
5
|
+
* real Teams / Azure Communication Services SDK (the Gemini `connectLiveSession` testability pattern).
|
|
6
|
+
* Structurally mirrors `ZoomBridge` (the reference driver) — only the platform names + join-coordinate
|
|
7
|
+
* parsing differ.
|
|
8
|
+
*
|
|
9
|
+
* Teams capability coverage (per the §8 seed row): on-demand + scheduled + invite + native-invite join,
|
|
10
|
+
* inbound routing, audio in/out (video/screen directional flags carried by transport), a diarized roster
|
|
11
|
+
* (`SpeakerDiarization`), participant mute + Teams meeting chat (via the Meeting Controls channel). Native
|
|
12
|
+
* raised-hand is ⚠️ partial over the calling-bot API — wired where the platform surfaces it, tolerant of
|
|
13
|
+
* it never firing. Telephony features (DTMF / transfer / recording) are NOT Teams-meeting features here,
|
|
14
|
+
* so those virtual base methods keep throwing `BridgeCapabilityNotSupportedError`.
|
|
15
|
+
*
|
|
16
|
+
* @module @memberjunction/ai-bridge-teams
|
|
17
|
+
* @author MemberJunction.com
|
|
18
|
+
*/
|
|
19
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
20
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
21
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
22
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
23
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
24
|
+
};
|
|
25
|
+
import { RegisterClass } from '@memberjunction/global';
|
|
26
|
+
import { LogError } from '@memberjunction/core';
|
|
27
|
+
import { BaseRealtimeBridge, } from '@memberjunction/ai-bridge-base';
|
|
28
|
+
import { TeamsMeetingControlsEventSource } from './teams-meeting-controls.js';
|
|
29
|
+
/**
|
|
30
|
+
* The `DriverClass` key {@link TeamsBridge} registers under. A `MJ: AI Bridge Providers` row with
|
|
31
|
+
* `DriverClass = 'TeamsBridge'` resolves to this driver via the `ClassFactory`.
|
|
32
|
+
*/
|
|
33
|
+
export const TEAMS_BRIDGE_DRIVER_CLASS = 'TeamsBridge';
|
|
34
|
+
/**
|
|
35
|
+
* Maps a Teams participant role to the bridge's {@link BridgeParticipantRole}. The bot (`IsSelf`) is
|
|
36
|
+
* surfaced as `'Agent'`; Teams organizers/presenters map to host/co-host.
|
|
37
|
+
*/
|
|
38
|
+
function mapParticipantRole(role, isSelf) {
|
|
39
|
+
if (isSelf) {
|
|
40
|
+
return 'Agent';
|
|
41
|
+
}
|
|
42
|
+
switch (role) {
|
|
43
|
+
case 'Organizer':
|
|
44
|
+
return 'Host';
|
|
45
|
+
case 'Presenter':
|
|
46
|
+
return 'CoHost';
|
|
47
|
+
default:
|
|
48
|
+
return 'Participant';
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** Maps a Teams participant onto the bridge's {@link BridgeParticipantInfo}. */
|
|
52
|
+
function toBridgeParticipant(p) {
|
|
53
|
+
return {
|
|
54
|
+
ExternalId: p.ParticipantId,
|
|
55
|
+
DisplayName: p.DisplayName,
|
|
56
|
+
Role: mapParticipantRole(p.Role, p.IsSelf),
|
|
57
|
+
IsAgent: p.IsSelf === true,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Realtime Bridge driver for **Microsoft Teams meetings**.
|
|
62
|
+
*
|
|
63
|
+
* Construct the driver with the default constructor (the engine's `ClassFactory` path) — it lazily
|
|
64
|
+
* builds the Teams SDK from the {@link sdkFactory} at {@link Connect} time. Tests inject a `FakeTeamsSdk`
|
|
65
|
+
* by overriding the factory via {@link SetSdkFactory} (the creation seam) before connecting.
|
|
66
|
+
*
|
|
67
|
+
* Registered via `@RegisterClass(BaseRealtimeBridge, 'TeamsBridge')`.
|
|
68
|
+
*/
|
|
69
|
+
let TeamsBridge = class TeamsBridge extends BaseRealtimeBridge {
|
|
70
|
+
constructor() {
|
|
71
|
+
super(...arguments);
|
|
72
|
+
/** The live Teams SDK seam for this session, created at {@link Connect}. */
|
|
73
|
+
this.sdk = null;
|
|
74
|
+
/** The bot's own participant id in the joined meeting (set at {@link Connect}). */
|
|
75
|
+
this.botParticipantId = null;
|
|
76
|
+
/** The Meeting Controls event source for this session (only when diarization is supported). */
|
|
77
|
+
this.meetingControls = null;
|
|
78
|
+
/**
|
|
79
|
+
* The SDK creation seam. Defaults to a factory that throws an explicit "bind the real Teams SDK"
|
|
80
|
+
* error, since this package ships WITHOUT the real SDK adapter (a deployment concern). Production
|
|
81
|
+
* sets a real factory via {@link SetSdkFactory}; tests inject a `FakeTeamsSdk`.
|
|
82
|
+
*/
|
|
83
|
+
this.sdkFactory = () => {
|
|
84
|
+
throw new Error('TeamsBridge has no Microsoft Teams SDK bound. Call TeamsBridge.SetSdkFactory(...) with a factory ' +
|
|
85
|
+
'that builds an ITeamsMeetingSdk over the real Azure Communication Services calling-bot / Microsoft Graph ' +
|
|
86
|
+
'cloud-communications API, or inject a fake in tests.');
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Sets the {@link TeamsMeetingSdkFactory} this driver uses to construct its SDK seam at connect —
|
|
91
|
+
* the creation seam (mirroring Gemini's overridable `connectLiveSession`). Production binds the
|
|
92
|
+
* real Teams ACS calling-bot adapter here; tests inject a `FakeTeamsSdk`.
|
|
93
|
+
*
|
|
94
|
+
* @param factory The factory that builds the {@link ITeamsMeetingSdk} for a session.
|
|
95
|
+
*/
|
|
96
|
+
SetSdkFactory(factory) {
|
|
97
|
+
this.sdkFactory = factory;
|
|
98
|
+
}
|
|
99
|
+
// ── Abstract — every bridge MUST implement ───────────────────────────────────────
|
|
100
|
+
/**
|
|
101
|
+
* Joins the Teams meeting and brings the bot online. Captures the capability context, builds the SDK
|
|
102
|
+
* from the {@link sdkFactory}, wires the inbound raw-audio path and meeting-ended callback, joins,
|
|
103
|
+
* and (when the provider diarizes) constructs + seeds the Meeting Controls event source.
|
|
104
|
+
*
|
|
105
|
+
* @param ctx The bridge context (features, provider name, the join URL as `Address`, config).
|
|
106
|
+
* @returns The bot participant + call (external connection) identifiers, persisted by the engine.
|
|
107
|
+
*/
|
|
108
|
+
async Connect(ctx) {
|
|
109
|
+
this.applyContext(ctx);
|
|
110
|
+
this.RequireFeature('AudioIn'); // a Teams meeting bridge requires bidirectional audio at minimum
|
|
111
|
+
this.RequireFeature('AudioOut');
|
|
112
|
+
this.sdk = this.sdkFactory(ctx.Configuration);
|
|
113
|
+
this.wireInboundAudio(this.sdk);
|
|
114
|
+
this.sdk.onMeetingEnded(() => this.handleMeetingEnded());
|
|
115
|
+
// Roster diarization is optional per provider; only stand up the Meeting Controls source when
|
|
116
|
+
// the platform advertises it (the engine also gates participant tracking on this flag).
|
|
117
|
+
if (this.features.SpeakerDiarization === true) {
|
|
118
|
+
this.meetingControls = new TeamsMeetingControlsEventSource(this.sdk);
|
|
119
|
+
}
|
|
120
|
+
// The driver owns the single SDK subscription set and fans out to BOTH its own roster handler
|
|
121
|
+
// and the Meeting Controls source (the SDK seam is latest-handler-wins, so there is one owner).
|
|
122
|
+
this.wireRoster(this.sdk);
|
|
123
|
+
const result = await this.sdk.join(this.buildJoinArgs(ctx));
|
|
124
|
+
this.botParticipantId = result.BotParticipantId;
|
|
125
|
+
// Seed the initial roster now that the bot has joined.
|
|
126
|
+
await this.refreshRoster(this.sdk);
|
|
127
|
+
return { BotParticipantId: result.BotParticipantId, ExternalConnectionId: result.CallId };
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Leaves the Teams meeting and releases all SDK resources. Tolerant of teardown errors.
|
|
131
|
+
*
|
|
132
|
+
* @param _reason Why the disconnect happened (Teams teardown is uniform; the bot simply leaves).
|
|
133
|
+
*/
|
|
134
|
+
async Disconnect(_reason) {
|
|
135
|
+
const sdk = this.sdk;
|
|
136
|
+
this.sdk = null;
|
|
137
|
+
this.botParticipantId = null;
|
|
138
|
+
this.meetingControls = null;
|
|
139
|
+
this.mediaHandler = undefined;
|
|
140
|
+
this.participantHandler = undefined;
|
|
141
|
+
if (sdk) {
|
|
142
|
+
try {
|
|
143
|
+
await sdk.leave();
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
LogError(`[TeamsBridge] leave() failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Sends an outbound media frame into the meeting. Audio is fed to the SDK's outbound audio-socket
|
|
152
|
+
* path; video/screen frames are gated by the directional capability flags (the transport carries
|
|
153
|
+
* them, and the SDK send is a deployment-time binding TODO — today the models emit audio).
|
|
154
|
+
*
|
|
155
|
+
* @param track The outbound track the frame targets.
|
|
156
|
+
* @param frame The media frame to send.
|
|
157
|
+
*/
|
|
158
|
+
SendMedia(track, frame) {
|
|
159
|
+
if (!this.sdk) {
|
|
160
|
+
return; // not connected — drop
|
|
161
|
+
}
|
|
162
|
+
switch (track) {
|
|
163
|
+
case 'audio-out': {
|
|
164
|
+
const pcm = this.framePcm(frame);
|
|
165
|
+
if (pcm) {
|
|
166
|
+
this.sdk.sendAudioFrame(pcm);
|
|
167
|
+
}
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
case 'video-out':
|
|
171
|
+
case 'screen-out':
|
|
172
|
+
// Directional video/screen capability is declared; the SDK send binding lands with the
|
|
173
|
+
// first realtime model that emits these tracks (TODO at deployment). No-op for now.
|
|
174
|
+
break;
|
|
175
|
+
default:
|
|
176
|
+
// An inbound track was passed to SendMedia — ignore (defensive).
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Registers the inbound-media handler. The driver forwards each raw per-participant audio frame
|
|
182
|
+
* (with its speaker label) to this handler; the engine routes it to `IRealtimeSession.SendInput`.
|
|
183
|
+
*
|
|
184
|
+
* @param handler Invoked with each inbound media frame.
|
|
185
|
+
*/
|
|
186
|
+
OnMedia(handler) {
|
|
187
|
+
this.mediaHandler = handler;
|
|
188
|
+
}
|
|
189
|
+
// ── Capability-gated virtuals Teams supports (gated by SupportedFeatures) ──────────
|
|
190
|
+
/**
|
|
191
|
+
* Returns the current Teams participant roster (gated by `SpeakerDiarization`). Re-asserts the flag
|
|
192
|
+
* (defense-in-depth) so even an engine-bypassing caller cannot pull a roster a disabled provider
|
|
193
|
+
* forbids.
|
|
194
|
+
*
|
|
195
|
+
* @returns The current participants.
|
|
196
|
+
* @throws {BridgeCapabilityNotSupportedError} when diarization is not enabled for the provider.
|
|
197
|
+
*/
|
|
198
|
+
async GetParticipants() {
|
|
199
|
+
this.RequireFeature('SpeakerDiarization');
|
|
200
|
+
if (!this.sdk) {
|
|
201
|
+
return [];
|
|
202
|
+
}
|
|
203
|
+
const participants = await this.sdk.getParticipants();
|
|
204
|
+
return participants.map(toBridgeParticipant);
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Registers a roster-change handler (gated by `SpeakerDiarization`). The driver fires it from the
|
|
208
|
+
* SDK's participant join/leave stream with the full current roster.
|
|
209
|
+
*
|
|
210
|
+
* @param handler Invoked with the updated participant list on each change.
|
|
211
|
+
* @throws {BridgeCapabilityNotSupportedError} when diarization is not enabled.
|
|
212
|
+
*/
|
|
213
|
+
OnParticipantChange(handler) {
|
|
214
|
+
this.RequireFeature('SpeakerDiarization');
|
|
215
|
+
this.participantHandler = handler;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Returns the Teams Meeting Controls event source for this session (roster · hand-raise · speaking ·
|
|
219
|
+
* mute), or `null` when diarization is off (no roster to facilitate). The engine wires the Meeting
|
|
220
|
+
* Controls channel from this.
|
|
221
|
+
*/
|
|
222
|
+
GetMeetingControlsEventSource() {
|
|
223
|
+
return this.meetingControls;
|
|
224
|
+
}
|
|
225
|
+
// ── Teams-native surfaces (used by the channel plane / facilitator) ──────────────
|
|
226
|
+
/**
|
|
227
|
+
* Posts a message to the Teams meeting chat. Exposed for the channel plane / turn-taking hybrid
|
|
228
|
+
* mode (the social-cost-free "raise hand"). Best-effort — a chat failure is logged, never fatal.
|
|
229
|
+
*
|
|
230
|
+
* @param text The chat message to post.
|
|
231
|
+
*/
|
|
232
|
+
async PostChatMessage(text) {
|
|
233
|
+
if (!this.sdk) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
try {
|
|
237
|
+
await this.sdk.postChatMessage(text);
|
|
238
|
+
}
|
|
239
|
+
catch (err) {
|
|
240
|
+
LogError(`[TeamsBridge] postChatMessage failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
/** The bot's participant id in the joined meeting (or `null` before {@link Connect}). */
|
|
244
|
+
get BotParticipantId() {
|
|
245
|
+
return this.botParticipantId;
|
|
246
|
+
}
|
|
247
|
+
// ── internals ────────────────────────────────────────────────────────────────────
|
|
248
|
+
/** Wires the SDK's inbound raw-audio callback to a diarized inbound {@link BridgeMediaFrame}. */
|
|
249
|
+
wireInboundAudio(sdk) {
|
|
250
|
+
sdk.onAudioFrame((frame) => {
|
|
251
|
+
this.mediaHandler?.({
|
|
252
|
+
Track: 'audio-in',
|
|
253
|
+
Bytes: frame.Pcm,
|
|
254
|
+
SpeakerLabel: frame.ParticipantId,
|
|
255
|
+
TimestampMs: frame.TimestampMs ?? Date.now(),
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Wires the SDK's participant join/leave + hand-raise streams ONCE (the driver is the single owner;
|
|
261
|
+
* the SDK seam is latest-handler-wins). Each roster change fans out to both the driver's own
|
|
262
|
+
* roster-change handler and the Meeting Controls source; hand-raise signals (⚠️ partial on Teams)
|
|
263
|
+
* feed the source.
|
|
264
|
+
*/
|
|
265
|
+
wireRoster(sdk) {
|
|
266
|
+
sdk.onParticipantJoin(() => void this.refreshRoster(sdk));
|
|
267
|
+
sdk.onParticipantLeave(() => void this.refreshRoster(sdk));
|
|
268
|
+
sdk.onHandRaise((id, raised) => this.meetingControls?.IngestHandRaise(id, raised));
|
|
269
|
+
}
|
|
270
|
+
/** Pulls the current roster from the SDK and fans it out to both the participant handler + Meeting Controls. */
|
|
271
|
+
async refreshRoster(sdk) {
|
|
272
|
+
try {
|
|
273
|
+
const participants = await sdk.getParticipants();
|
|
274
|
+
this.participantHandler?.(participants.map(toBridgeParticipant));
|
|
275
|
+
this.meetingControls?.IngestRoster(participants);
|
|
276
|
+
}
|
|
277
|
+
catch (err) {
|
|
278
|
+
LogError(`[TeamsBridge] roster refresh failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
/** Handles the SDK's meeting-ended signal: surface an empty roster so the engine sees everyone gone. */
|
|
282
|
+
handleMeetingEnded() {
|
|
283
|
+
this.participantHandler?.([]);
|
|
284
|
+
this.meetingControls?.IngestRoster([]);
|
|
285
|
+
}
|
|
286
|
+
/** Builds the SDK join args from the bridge context (Address is the meeting join URL). */
|
|
287
|
+
buildJoinArgs(ctx) {
|
|
288
|
+
const config = ctx.Configuration ?? {};
|
|
289
|
+
return {
|
|
290
|
+
JoinUrl: ctx.Address,
|
|
291
|
+
ThreadId: typeof config.ThreadId === 'string' ? config.ThreadId : this.parseThreadId(ctx.Address),
|
|
292
|
+
BotDisplayName: typeof config.BotDisplayName === 'string' ? config.BotDisplayName : `${ctx.ProviderName} Agent`,
|
|
293
|
+
AccessToken: typeof config.AccessToken === 'string' ? config.AccessToken : undefined,
|
|
294
|
+
TenantId: typeof config.TenantId === 'string' ? config.TenantId : undefined,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Extracts the meeting thread id from a Teams `meetup-join` URL when present, else `undefined`. The
|
|
299
|
+
* thread id is the `19:meeting_...@thread.v2` segment embedded (URL-encoded) in the join link.
|
|
300
|
+
* Tolerant — a non-matching address yields `undefined` so the SDK can resolve coordinates itself.
|
|
301
|
+
*/
|
|
302
|
+
parseThreadId(address) {
|
|
303
|
+
if (!address) {
|
|
304
|
+
return undefined;
|
|
305
|
+
}
|
|
306
|
+
const decoded = (() => {
|
|
307
|
+
try {
|
|
308
|
+
return decodeURIComponent(address);
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
return address;
|
|
312
|
+
}
|
|
313
|
+
})();
|
|
314
|
+
const match = decoded.match(/(19:meeting_[^/@]+@thread\.v2)/i);
|
|
315
|
+
return match ? match[1] : undefined;
|
|
316
|
+
}
|
|
317
|
+
/** Returns the PCM bytes of an outbound audio frame, preferring the binary payload. */
|
|
318
|
+
framePcm(frame) {
|
|
319
|
+
if (frame.Bytes) {
|
|
320
|
+
return frame.Bytes;
|
|
321
|
+
}
|
|
322
|
+
if (frame.Base64) {
|
|
323
|
+
return this.decodeBase64(frame.Base64);
|
|
324
|
+
}
|
|
325
|
+
return undefined;
|
|
326
|
+
}
|
|
327
|
+
/** Decodes a base64 payload to an `ArrayBuffer` (Node `Buffer` fast path, `atob` fallback). */
|
|
328
|
+
decodeBase64(base64) {
|
|
329
|
+
if (typeof Buffer !== 'undefined') {
|
|
330
|
+
const buf = Buffer.from(base64, 'base64');
|
|
331
|
+
// Copy into a fresh ArrayBuffer (Buffer.buffer is ArrayBufferLike under newer @types/node).
|
|
332
|
+
const out = new Uint8Array(buf.byteLength);
|
|
333
|
+
out.set(buf);
|
|
334
|
+
return out.buffer;
|
|
335
|
+
}
|
|
336
|
+
const binary = atob(base64);
|
|
337
|
+
const bytes = new Uint8Array(binary.length);
|
|
338
|
+
for (let i = 0; i < binary.length; i++) {
|
|
339
|
+
bytes[i] = binary.charCodeAt(i);
|
|
340
|
+
}
|
|
341
|
+
return bytes.buffer;
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
TeamsBridge = __decorate([
|
|
345
|
+
RegisterClass(BaseRealtimeBridge, TEAMS_BRIDGE_DRIVER_CLASS)
|
|
346
|
+
], TeamsBridge);
|
|
347
|
+
export { TeamsBridge };
|
|
348
|
+
/**
|
|
349
|
+
* Tree-shaking-prevention loader. Modern bundlers cannot see the `@RegisterClass` dynamic registration
|
|
350
|
+
* of {@link TeamsBridge} and may eliminate it. Import and call this no-op from a static code path
|
|
351
|
+
* (the package entry point does) so the `ClassFactory` can resolve `'TeamsBridge'`.
|
|
352
|
+
*/
|
|
353
|
+
export function LoadTeamsBridge() {
|
|
354
|
+
// Intentionally empty — referencing the module is what prevents tree-shaking.
|
|
355
|
+
}
|
|
356
|
+
//# sourceMappingURL=teams-bridge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"teams-bridge.js","sourceRoot":"","sources":["../src/teams-bridge.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapts the {@link ITeamsMeetingSdk} participant / hand-raise / speaking stream into an
|
|
3
|
+
* {@link IBridgeMeetingControlsEventSource} so the bridge engine can wire the **Meeting Controls**
|
|
4
|
+
* facilitator channel (roster · hand-raise queue · who's-speaking · mute) for a Teams session. This is
|
|
5
|
+
* the §4b "the bridge contributes a channel" pattern realized for Teams — no channel logic lives here,
|
|
6
|
+
* only the platform→channel signal mapping.
|
|
7
|
+
*/
|
|
8
|
+
import { IBridgeMeetingControlsEventSource, BridgeMeetingParticipant, BridgeMeetingControlsCapability } from '@memberjunction/ai-bridge-base';
|
|
9
|
+
import { ITeamsMeetingSdk, TeamsParticipant } from './teams-sdk.js';
|
|
10
|
+
/** Maps one Teams participant onto the channel's {@link BridgeMeetingParticipant} shape. */
|
|
11
|
+
export declare function toMeetingParticipant(p: TeamsParticipant): BridgeMeetingParticipant;
|
|
12
|
+
/**
|
|
13
|
+
* Teams's adapter to the Meeting Controls channel's event source. It maintains a live roster and
|
|
14
|
+
* mirrors native hand-raise + diarized speaking signals, and actuates mute through the SDK.
|
|
15
|
+
*
|
|
16
|
+
* **The driver owns the SDK subscriptions** (the SDK seam is "latest handler wins", so it can have
|
|
17
|
+
* only one subscriber per event) and **feeds this source imperatively** via {@link IngestRoster} /
|
|
18
|
+
* {@link IngestHandRaise} / {@link IngestSpeaking}. This source only needs the SDK for the one *action*
|
|
19
|
+
* it actuates — {@link MuteParticipant}. The Meeting Controls channel subscribes the three `On*`
|
|
20
|
+
* streams in turn.
|
|
21
|
+
*
|
|
22
|
+
* The driver constructs ONE of these per session (only when the provider has `SpeakerDiarization`, i.e.
|
|
23
|
+
* there is a roster to facilitate) and returns it from `TeamsBridge.GetMeetingControlsEventSource`.
|
|
24
|
+
*/
|
|
25
|
+
export declare class TeamsMeetingControlsEventSource implements IBridgeMeetingControlsEventSource {
|
|
26
|
+
private readonly sdk;
|
|
27
|
+
/** Live roster keyed by participant id (lowercased), kept current by the driver's {@link IngestRoster}. */
|
|
28
|
+
private readonly roster;
|
|
29
|
+
private rosterHandler?;
|
|
30
|
+
private speakingHandler?;
|
|
31
|
+
private handRaiseHandler?;
|
|
32
|
+
/** Teams supports organizer/presenter-driven mute, so the facilitator `MuteParticipant` tool is offered. */
|
|
33
|
+
readonly Capabilities: ReadonlyArray<BridgeMeetingControlsCapability>;
|
|
34
|
+
/**
|
|
35
|
+
* @param sdk The Teams SDK seam used only to actuate {@link MuteParticipant} — all *perception* is
|
|
36
|
+
* fed in by the driver via the `Ingest*` methods so the SDK keeps a single subscriber per event.
|
|
37
|
+
*/
|
|
38
|
+
constructor(sdk: ITeamsMeetingSdk);
|
|
39
|
+
/**
|
|
40
|
+
* Replaces the roster with a fresh snapshot from the driver and emits it to the channel. Called by
|
|
41
|
+
* the driver after the initial join and on every participant join/leave.
|
|
42
|
+
*
|
|
43
|
+
* @param participants The full current Teams roster.
|
|
44
|
+
*/
|
|
45
|
+
IngestRoster(participants: TeamsParticipant[]): void;
|
|
46
|
+
/**
|
|
47
|
+
* Forwards a native hand-raise/lower signal from the driver to the channel.
|
|
48
|
+
*
|
|
49
|
+
* ⚠️ Teams raised-hand is partial; this may never fire on tenants/builds that don't surface the
|
|
50
|
+
* event over the calling-bot API. The channel degrades gracefully (its software hand-raise still works).
|
|
51
|
+
*
|
|
52
|
+
* @param participantId The participant whose hand changed.
|
|
53
|
+
* @param raised Whether the hand is now raised.
|
|
54
|
+
*/
|
|
55
|
+
IngestHandRaise(participantId: string, raised: boolean): void;
|
|
56
|
+
/**
|
|
57
|
+
* Forwards a diarized speaking-set change from the driver to the channel.
|
|
58
|
+
*
|
|
59
|
+
* @param participantIds The ids currently speaking.
|
|
60
|
+
*/
|
|
61
|
+
IngestSpeaking(participantIds: string[]): void;
|
|
62
|
+
/**
|
|
63
|
+
* Convenience alias for {@link IngestSpeaking}, kept because "notify" reads naturally at the
|
|
64
|
+
* driver's inbound-audio call site.
|
|
65
|
+
*
|
|
66
|
+
* @param participantIds The ids currently speaking.
|
|
67
|
+
*/
|
|
68
|
+
NotifySpeaking(participantIds: string[]): void;
|
|
69
|
+
/** @inheritdoc */
|
|
70
|
+
OnRosterChange(handler: (participants: BridgeMeetingParticipant[]) => void): void;
|
|
71
|
+
/** @inheritdoc */
|
|
72
|
+
OnSpeakingChange(handler: (participantIds: string[]) => void): void;
|
|
73
|
+
/** @inheritdoc */
|
|
74
|
+
OnHandRaiseChange(handler: (participantId: string, raised: boolean) => void): void;
|
|
75
|
+
/** @inheritdoc */
|
|
76
|
+
MuteParticipant(participantId: string): Promise<void>;
|
|
77
|
+
private emitRoster;
|
|
78
|
+
private key;
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=teams-meeting-controls.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"teams-meeting-controls.d.ts","sourceRoot":"","sources":["../src/teams-meeting-controls.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EACH,iCAAiC,EACjC,wBAAwB,EACxB,+BAA+B,EAElC,MAAM,gCAAgC,CAAC;AACxC,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAwB,MAAM,aAAa,CAAC;AAqBvF,4FAA4F;AAC5F,wBAAgB,oBAAoB,CAAC,CAAC,EAAE,gBAAgB,GAAG,wBAAwB,CAOlF;AAED;;;;;;;;;;;;GAYG;AACH,qBAAa,+BAAgC,YAAW,iCAAiC;IAezE,OAAO,CAAC,QAAQ,CAAC,GAAG;IAdhC,2GAA2G;IAC3G,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA+C;IAEtE,OAAO,CAAC,aAAa,CAAC,CAAqD;IAC3E,OAAO,CAAC,eAAe,CAAC,CAAqC;IAC7D,OAAO,CAAC,gBAAgB,CAAC,CAAmD;IAE5E,4GAA4G;IAC5G,SAAgB,YAAY,EAAE,aAAa,CAAC,+BAA+B,CAAC,CAAY;IAExF;;;OAGG;gBAC0B,GAAG,EAAE,gBAAgB;IAIlD;;;;;OAKG;IACI,YAAY,CAAC,YAAY,EAAE,gBAAgB,EAAE,GAAG,IAAI;IAQ3D;;;;;;;;OAQG;IACI,eAAe,CAAC,aAAa,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,IAAI;IAIpE;;;;OAIG;IACI,cAAc,CAAC,cAAc,EAAE,MAAM,EAAE,GAAG,IAAI;IAIrD;;;;;OAKG;IACI,cAAc,CAAC,cAAc,EAAE,MAAM,EAAE,GAAG,IAAI;IAMrD,kBAAkB;IACX,cAAc,CAAC,OAAO,EAAE,CAAC,YAAY,EAAE,wBAAwB,EAAE,KAAK,IAAI,GAAG,IAAI;IAMxF,kBAAkB;IACX,gBAAgB,CAAC,OAAO,EAAE,CAAC,cAAc,EAAE,MAAM,EAAE,KAAK,IAAI,GAAG,IAAI;IAI1E,kBAAkB;IACX,iBAAiB,CAAC,OAAO,EAAE,CAAC,aAAa,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,KAAK,IAAI,GAAG,IAAI;IAIzF,kBAAkB;IACL,eAAe,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAMlE,OAAO,CAAC,UAAU;IAIlB,OAAO,CAAC,GAAG;CAGd"}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapts the {@link ITeamsMeetingSdk} participant / hand-raise / speaking stream into an
|
|
3
|
+
* {@link IBridgeMeetingControlsEventSource} so the bridge engine can wire the **Meeting Controls**
|
|
4
|
+
* facilitator channel (roster · hand-raise queue · who's-speaking · mute) for a Teams session. This is
|
|
5
|
+
* the §4b "the bridge contributes a channel" pattern realized for Teams — no channel logic lives here,
|
|
6
|
+
* only the platform→channel signal mapping.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Maps a Teams participant role to the bridge's meeting-participant role. Teams has no distinct "Agent"
|
|
10
|
+
* role; the bot is flagged via `IsSelf` and surfaced as `'Agent'`. Teams organizers/presenters map to
|
|
11
|
+
* the bridge's host/co-host roles (the closest facilitator-authority equivalents).
|
|
12
|
+
*/
|
|
13
|
+
function mapRole(role, isSelf) {
|
|
14
|
+
if (isSelf) {
|
|
15
|
+
return 'Agent';
|
|
16
|
+
}
|
|
17
|
+
switch (role) {
|
|
18
|
+
case 'Organizer':
|
|
19
|
+
return 'Host';
|
|
20
|
+
case 'Presenter':
|
|
21
|
+
return 'CoHost';
|
|
22
|
+
default:
|
|
23
|
+
return 'Participant';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** Maps one Teams participant onto the channel's {@link BridgeMeetingParticipant} shape. */
|
|
27
|
+
export function toMeetingParticipant(p) {
|
|
28
|
+
return {
|
|
29
|
+
ParticipantId: p.ParticipantId,
|
|
30
|
+
DisplayName: p.DisplayName,
|
|
31
|
+
Role: mapRole(p.Role, p.IsSelf),
|
|
32
|
+
IsAgent: p.IsSelf === true,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Teams's adapter to the Meeting Controls channel's event source. It maintains a live roster and
|
|
37
|
+
* mirrors native hand-raise + diarized speaking signals, and actuates mute through the SDK.
|
|
38
|
+
*
|
|
39
|
+
* **The driver owns the SDK subscriptions** (the SDK seam is "latest handler wins", so it can have
|
|
40
|
+
* only one subscriber per event) and **feeds this source imperatively** via {@link IngestRoster} /
|
|
41
|
+
* {@link IngestHandRaise} / {@link IngestSpeaking}. This source only needs the SDK for the one *action*
|
|
42
|
+
* it actuates — {@link MuteParticipant}. The Meeting Controls channel subscribes the three `On*`
|
|
43
|
+
* streams in turn.
|
|
44
|
+
*
|
|
45
|
+
* The driver constructs ONE of these per session (only when the provider has `SpeakerDiarization`, i.e.
|
|
46
|
+
* there is a roster to facilitate) and returns it from `TeamsBridge.GetMeetingControlsEventSource`.
|
|
47
|
+
*/
|
|
48
|
+
export class TeamsMeetingControlsEventSource {
|
|
49
|
+
/**
|
|
50
|
+
* @param sdk The Teams SDK seam used only to actuate {@link MuteParticipant} — all *perception* is
|
|
51
|
+
* fed in by the driver via the `Ingest*` methods so the SDK keeps a single subscriber per event.
|
|
52
|
+
*/
|
|
53
|
+
constructor(sdk) {
|
|
54
|
+
this.sdk = sdk;
|
|
55
|
+
/** Live roster keyed by participant id (lowercased), kept current by the driver's {@link IngestRoster}. */
|
|
56
|
+
this.roster = new Map();
|
|
57
|
+
/** Teams supports organizer/presenter-driven mute, so the facilitator `MuteParticipant` tool is offered. */
|
|
58
|
+
this.Capabilities = ['Mute'];
|
|
59
|
+
}
|
|
60
|
+
// ── Driver-fed perception (the driver owns the SDK subscriptions) ─────────────────
|
|
61
|
+
/**
|
|
62
|
+
* Replaces the roster with a fresh snapshot from the driver and emits it to the channel. Called by
|
|
63
|
+
* the driver after the initial join and on every participant join/leave.
|
|
64
|
+
*
|
|
65
|
+
* @param participants The full current Teams roster.
|
|
66
|
+
*/
|
|
67
|
+
IngestRoster(participants) {
|
|
68
|
+
this.roster.clear();
|
|
69
|
+
for (const p of participants) {
|
|
70
|
+
this.roster.set(this.key(p.ParticipantId), toMeetingParticipant(p));
|
|
71
|
+
}
|
|
72
|
+
this.emitRoster();
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Forwards a native hand-raise/lower signal from the driver to the channel.
|
|
76
|
+
*
|
|
77
|
+
* ⚠️ Teams raised-hand is partial; this may never fire on tenants/builds that don't surface the
|
|
78
|
+
* event over the calling-bot API. The channel degrades gracefully (its software hand-raise still works).
|
|
79
|
+
*
|
|
80
|
+
* @param participantId The participant whose hand changed.
|
|
81
|
+
* @param raised Whether the hand is now raised.
|
|
82
|
+
*/
|
|
83
|
+
IngestHandRaise(participantId, raised) {
|
|
84
|
+
this.handRaiseHandler?.(participantId, raised);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Forwards a diarized speaking-set change from the driver to the channel.
|
|
88
|
+
*
|
|
89
|
+
* @param participantIds The ids currently speaking.
|
|
90
|
+
*/
|
|
91
|
+
IngestSpeaking(participantIds) {
|
|
92
|
+
this.speakingHandler?.(participantIds);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Convenience alias for {@link IngestSpeaking}, kept because "notify" reads naturally at the
|
|
96
|
+
* driver's inbound-audio call site.
|
|
97
|
+
*
|
|
98
|
+
* @param participantIds The ids currently speaking.
|
|
99
|
+
*/
|
|
100
|
+
NotifySpeaking(participantIds) {
|
|
101
|
+
this.IngestSpeaking(participantIds);
|
|
102
|
+
}
|
|
103
|
+
// ── IBridgeMeetingControlsEventSource ────────────────────────────────────────────
|
|
104
|
+
/** @inheritdoc */
|
|
105
|
+
OnRosterChange(handler) {
|
|
106
|
+
this.rosterHandler = handler;
|
|
107
|
+
// Emit the current roster immediately so a late subscriber is not blank.
|
|
108
|
+
this.emitRoster();
|
|
109
|
+
}
|
|
110
|
+
/** @inheritdoc */
|
|
111
|
+
OnSpeakingChange(handler) {
|
|
112
|
+
this.speakingHandler = handler;
|
|
113
|
+
}
|
|
114
|
+
/** @inheritdoc */
|
|
115
|
+
OnHandRaiseChange(handler) {
|
|
116
|
+
this.handRaiseHandler = handler;
|
|
117
|
+
}
|
|
118
|
+
/** @inheritdoc */
|
|
119
|
+
async MuteParticipant(participantId) {
|
|
120
|
+
await this.sdk.muteParticipant(participantId);
|
|
121
|
+
}
|
|
122
|
+
// ── internals ────────────────────────────────────────────────────────────────────
|
|
123
|
+
emitRoster() {
|
|
124
|
+
this.rosterHandler?.(Array.from(this.roster.values()));
|
|
125
|
+
}
|
|
126
|
+
key(participantId) {
|
|
127
|
+
return participantId.trim().toLowerCase();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=teams-meeting-controls.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"teams-meeting-controls.js","sourceRoot":"","sources":["../src/teams-meeting-controls.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAUH;;;;GAIG;AACH,SAAS,OAAO,CAAC,IAA0B,EAAE,MAA2B;IACpE,IAAI,MAAM,EAAE,CAAC;QACT,OAAO,OAAO,CAAC;IACnB,CAAC;IACD,QAAQ,IAAI,EAAE,CAAC;QACX,KAAK,WAAW;YACZ,OAAO,MAAM,CAAC;QAClB,KAAK,WAAW;YACZ,OAAO,QAAQ,CAAC;QACpB;YACI,OAAO,aAAa,CAAC;IAC7B,CAAC;AACL,CAAC;AAED,4FAA4F;AAC5F,MAAM,UAAU,oBAAoB,CAAC,CAAmB;IACpD,OAAO;QACH,aAAa,EAAE,CAAC,CAAC,aAAa;QAC9B,WAAW,EAAE,CAAC,CAAC,WAAW;QAC1B,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC;QAC/B,OAAO,EAAE,CAAC,CAAC,MAAM,KAAK,IAAI;KAC7B,CAAC;AACN,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,OAAO,+BAA+B;IAWxC;;;OAGG;IACH,YAA6B,GAAqB;QAArB,QAAG,GAAH,GAAG,CAAkB;QAdlD,2GAA2G;QAC1F,WAAM,GAAG,IAAI,GAAG,EAAoC,CAAC;QAMtE,4GAA4G;QAC5F,iBAAY,GAAmD,CAAC,MAAM,CAAC,CAAC;IAMnC,CAAC;IAEtD,qFAAqF;IAErF;;;;;OAKG;IACI,YAAY,CAAC,YAAgC;QAChD,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;QACpB,KAAK,MAAM,CAAC,IAAI,YAAY,EAAE,CAAC;YAC3B,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,aAAa,CAAC,EAAE,oBAAoB,CAAC,CAAC,CAAC,CAAC,CAAC;QACxE,CAAC;QACD,IAAI,CAAC,UAAU,EAAE,CAAC;IACtB,CAAC;IAED;;;;;;;;OAQG;IACI,eAAe,CAAC,aAAqB,EAAE,MAAe;QACzD,IAAI,CAAC,gBAAgB,EAAE,CAAC,aAAa,EAAE,MAAM,CAAC,CAAC;IACnD,CAAC;IAED;;;;OAIG;IACI,cAAc,CAAC,cAAwB;QAC1C,IAAI,CAAC,eAAe,EAAE,CAAC,cAAc,CAAC,CAAC;IAC3C,CAAC;IAED;;;;;OAKG;IACI,cAAc,CAAC,cAAwB;QAC1C,IAAI,CAAC,cAAc,CAAC,cAAc,CAAC,CAAC;IACxC,CAAC;IAED,oFAAoF;IAEpF,kBAAkB;IACX,cAAc,CAAC,OAA2D;QAC7E,IAAI,CAAC,aAAa,GAAG,OAAO,CAAC;QAC7B,yEAAyE;QACzE,IAAI,CAAC,UAAU,EAAE,CAAC;IACtB,CAAC;IAED,kBAAkB;IACX,gBAAgB,CAAC,OAA2C;QAC/D,IAAI,CAAC,eAAe,GAAG,OAAO,CAAC;IACnC,CAAC;IAED,kBAAkB;IACX,iBAAiB,CAAC,OAAyD;QAC9E,IAAI,CAAC,gBAAgB,GAAG,OAAO,CAAC;IACpC,CAAC;IAED,kBAAkB;IACX,KAAK,CAAC,eAAe,CAAC,aAAqB;QAC9C,MAAM,IAAI,CAAC,GAAG,CAAC,eAAe,CAAC,aAAa,CAAC,CAAC;IAClD,CAAC;IAED,oFAAoF;IAE5E,UAAU;QACd,IAAI,CAAC,aAAa,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;IAC3D,CAAC;IAEO,GAAG,CAAC,aAAqB;QAC7B,OAAO,aAAa,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAC9C,CAAC;CACJ"}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The injectable **Microsoft Teams calling-bot SDK seam** — the minimal set of operations
|
|
3
|
+
* {@link import('./teams-bridge.js').TeamsBridge} needs from Teams, declared as an interface so the driver
|
|
4
|
+
* builds and unit-tests against an in-memory fake with **no network and no real Teams / Azure
|
|
5
|
+
* Communication Services SDK**.
|
|
6
|
+
*
|
|
7
|
+
* ## Production binding (TODO at deployment)
|
|
8
|
+
* In production this interface is bound to the **Azure Communication Services (ACS) calling-bot** SDK
|
|
9
|
+
* plus the **Microsoft Graph cloud-communications API** (the `/communications/calls` "application-hosted
|
|
10
|
+
* media" bot model) so the bot can join a Teams meeting, pull per-participant PCM audio for diarization,
|
|
11
|
+
* push synthesized audio back, observe roster + raised-hand events, and post to the Teams meeting chat.
|
|
12
|
+
* The named operations map to the SDK as follows:
|
|
13
|
+
* - {@link join} / {@link leave} → the Graph `POST /communications/calls` (join meeting via the join
|
|
14
|
+
* URL / meeting coordinates) and the call's `DELETE` (leave) lifecycle.
|
|
15
|
+
* - {@link sendAudioFrame} → the ACS application-hosted-media outbound audio-socket (the bot's voice in).
|
|
16
|
+
* - {@link onAudioFrame} → the ACS per-participant inbound audio-socket callback (the source of speaker
|
|
17
|
+
* labels for diarization — Teams provides participant attribution on the media stream).
|
|
18
|
+
* - {@link onParticipantJoin} / {@link onParticipantLeave} / {@link getParticipants} → the call's
|
|
19
|
+
* `participants` collection + the `participantsUpdated` change notifications.
|
|
20
|
+
* - {@link onHandRaise} → the meeting's raised-hand signal. ⚠️ Partial: Teams surfaces raised hands in
|
|
21
|
+
* the meeting UI, but a clean per-participant *event* over the calling-bot API is not guaranteed on
|
|
22
|
+
* every tenant/build; the adapter wires it where the platform exposes it and otherwise no-ops.
|
|
23
|
+
* - {@link postChatMessage} → the Graph `POST /chats/{id}/messages` against the meeting's chat thread.
|
|
24
|
+
* - {@link muteParticipant} → the call's `participant:mute` action (requires the bot be an organizer /
|
|
25
|
+
* presenter with the relevant policy).
|
|
26
|
+
* - {@link onMeetingEnded} → the call's `callEnded` / `terminated` status notification.
|
|
27
|
+
*
|
|
28
|
+
* Binding the real SDK is a thin adapter that implements this interface; the driver and its tests do
|
|
29
|
+
* not change. **None of the ACS/Graph SDK types leak into this package.**
|
|
30
|
+
*
|
|
31
|
+
* See `/plans/realtime/realtime-bridges-architecture.md` §3, §8 (Microsoft Teams row) and
|
|
32
|
+
* `/guides/REALTIME_BRIDGES_GUIDE.md`.
|
|
33
|
+
*/
|
|
34
|
+
/** The role a Teams participant holds in the meeting, normalized to the bridge's participant roles. */
|
|
35
|
+
export type TeamsParticipantRole = 'Organizer' | 'Presenter' | 'Attendee';
|
|
36
|
+
/**
|
|
37
|
+
* One Teams meeting participant as the seam reports it. Platform-native and minimal — the driver maps
|
|
38
|
+
* this onto `BridgeParticipantInfo` / `BridgeMeetingParticipant`.
|
|
39
|
+
*/
|
|
40
|
+
export interface TeamsParticipant {
|
|
41
|
+
/** The Teams participant id (the call's participant identity / `id`, stable for their presence). */
|
|
42
|
+
ParticipantId: string;
|
|
43
|
+
/** The participant's display name as Teams reports it. */
|
|
44
|
+
DisplayName?: string;
|
|
45
|
+
/** The participant's meeting role. */
|
|
46
|
+
Role: TeamsParticipantRole;
|
|
47
|
+
/** Whether this participant is the bridge's own bot (so the driver can exclude it from diarization addressing). */
|
|
48
|
+
IsSelf?: boolean;
|
|
49
|
+
}
|
|
50
|
+
/** One frame of raw per-participant audio the seam surfaces for diarization + the agent's "hearing". */
|
|
51
|
+
export interface TeamsAudioFrame {
|
|
52
|
+
/** Raw PCM audio bytes for this frame. */
|
|
53
|
+
Pcm: ArrayBuffer;
|
|
54
|
+
/** The Teams participant id this audio came from (the diarization speaker label). */
|
|
55
|
+
ParticipantId: string;
|
|
56
|
+
/** The participant's display name at capture time, when known. */
|
|
57
|
+
DisplayName?: string;
|
|
58
|
+
/** Optional epoch-ms capture timestamp. */
|
|
59
|
+
TimestampMs?: number;
|
|
60
|
+
}
|
|
61
|
+
/** Arguments to {@link ITeamsMeetingSdk.join} — what the bot needs to get into a Teams meeting. */
|
|
62
|
+
export interface TeamsJoinArgs {
|
|
63
|
+
/** The Teams meeting join URL (the `https://teams.microsoft.com/l/meetup-join/...` link). */
|
|
64
|
+
JoinUrl: string;
|
|
65
|
+
/**
|
|
66
|
+
* The parsed meeting thread id (the chat/conversation id behind the join URL), when resolved
|
|
67
|
+
* upstream. Used for posting to the meeting chat and as a stable meeting coordinate.
|
|
68
|
+
*/
|
|
69
|
+
ThreadId?: string;
|
|
70
|
+
/** The display name the bot appears as in the participant list. */
|
|
71
|
+
BotDisplayName: string;
|
|
72
|
+
/**
|
|
73
|
+
* The OAuth bearer / application token authorizing the bot's Graph + ACS calls. Resolved upstream
|
|
74
|
+
* via MJ's credential system; never inline secrets.
|
|
75
|
+
*/
|
|
76
|
+
AccessToken?: string;
|
|
77
|
+
/** The Azure tenant id the meeting belongs to, when joining cross-tenant. */
|
|
78
|
+
TenantId?: string;
|
|
79
|
+
}
|
|
80
|
+
/** The handles the seam returns after a successful {@link ITeamsMeetingSdk.join}. */
|
|
81
|
+
export interface TeamsJoinResult {
|
|
82
|
+
/** The bot's own participant id in the joined meeting. */
|
|
83
|
+
BotParticipantId: string;
|
|
84
|
+
/** The Teams call id the bot joined (the durable external connection id). */
|
|
85
|
+
CallId: string;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The minimal Microsoft Teams calling-bot SDK surface the {@link import('./teams-bridge.js').TeamsBridge}
|
|
89
|
+
* depends on. Production binds this to the real ACS calling-bot / Graph cloud-communications API; tests
|
|
90
|
+
* inject a `FakeTeamsSdk`.
|
|
91
|
+
*/
|
|
92
|
+
export interface ITeamsMeetingSdk {
|
|
93
|
+
/**
|
|
94
|
+
* Joins the Teams meeting and brings the bot online. Returns the bot's participant id + the call id.
|
|
95
|
+
*
|
|
96
|
+
* @param args Join parameters (join URL, thread id, bot name, auth).
|
|
97
|
+
* @returns The bot participant + call handles.
|
|
98
|
+
*/
|
|
99
|
+
join(args: TeamsJoinArgs): Promise<TeamsJoinResult>;
|
|
100
|
+
/** Leaves the meeting (terminates the bot's call leg) and releases SDK resources. */
|
|
101
|
+
leave(): Promise<void>;
|
|
102
|
+
/**
|
|
103
|
+
* Sends one raw PCM audio frame as the bot's outbound audio (the agent's voice into the meeting),
|
|
104
|
+
* over the ACS application-hosted-media outbound audio socket.
|
|
105
|
+
*
|
|
106
|
+
* @param pcm The PCM audio bytes to send.
|
|
107
|
+
*/
|
|
108
|
+
sendAudioFrame(pcm: ArrayBuffer): void;
|
|
109
|
+
/**
|
|
110
|
+
* Registers a callback for inbound raw per-participant audio frames (what the agent hears, carrying
|
|
111
|
+
* the speaker label for diarization). "Latest handler wins."
|
|
112
|
+
*
|
|
113
|
+
* @param cb Invoked with each inbound audio frame.
|
|
114
|
+
*/
|
|
115
|
+
onAudioFrame(cb: (frame: TeamsAudioFrame) => void): void;
|
|
116
|
+
/**
|
|
117
|
+
* Registers a callback fired when a participant joins. "Latest handler wins."
|
|
118
|
+
*
|
|
119
|
+
* @param cb Invoked with the participant who joined.
|
|
120
|
+
*/
|
|
121
|
+
onParticipantJoin(cb: (participant: TeamsParticipant) => void): void;
|
|
122
|
+
/**
|
|
123
|
+
* Registers a callback fired when a participant leaves. "Latest handler wins."
|
|
124
|
+
*
|
|
125
|
+
* @param cb Invoked with the participant id that left.
|
|
126
|
+
*/
|
|
127
|
+
onParticipantLeave(cb: (participantId: string) => void): void;
|
|
128
|
+
/**
|
|
129
|
+
* Registers a callback for native raised-hand signals. "Latest handler wins."
|
|
130
|
+
*
|
|
131
|
+
* ⚠️ Teams raised-hand is partial over the calling-bot API: a clean per-participant event is not
|
|
132
|
+
* guaranteed on every tenant/build. The adapter wires this where the platform exposes it and the
|
|
133
|
+
* driver tolerates the signal never firing.
|
|
134
|
+
*
|
|
135
|
+
* @param cb Invoked with the participant id and whether the hand is now raised.
|
|
136
|
+
*/
|
|
137
|
+
onHandRaise(cb: (participantId: string, raised: boolean) => void): void;
|
|
138
|
+
/**
|
|
139
|
+
* Returns the current participant roster (including the bot).
|
|
140
|
+
*
|
|
141
|
+
* @returns The current participants.
|
|
142
|
+
*/
|
|
143
|
+
getParticipants(): Promise<TeamsParticipant[]>;
|
|
144
|
+
/**
|
|
145
|
+
* Posts a message to the Teams meeting chat thread (everyone).
|
|
146
|
+
*
|
|
147
|
+
* @param text The chat message text.
|
|
148
|
+
*/
|
|
149
|
+
postChatMessage(text: string): Promise<void>;
|
|
150
|
+
/**
|
|
151
|
+
* Mutes a participant (requires the bot be an organizer/presenter with the relevant policy).
|
|
152
|
+
*
|
|
153
|
+
* @param participantId The participant to mute.
|
|
154
|
+
*/
|
|
155
|
+
muteParticipant(participantId: string): Promise<void>;
|
|
156
|
+
/**
|
|
157
|
+
* Registers a callback fired when the meeting/call ends (organizer ended / call terminated).
|
|
158
|
+
* "Latest handler wins."
|
|
159
|
+
*
|
|
160
|
+
* @param cb Invoked when the meeting has ended.
|
|
161
|
+
*/
|
|
162
|
+
onMeetingEnded(cb: () => void): void;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* A factory that constructs an {@link ITeamsMeetingSdk} for a session — the creation seam (mirroring
|
|
166
|
+
* Gemini's `connectLiveSession`). Production supplies a factory that builds the real ACS calling-bot /
|
|
167
|
+
* Graph adapter from resolved config; tests supply one that returns a `FakeTeamsSdk`.
|
|
168
|
+
*
|
|
169
|
+
* @param config The resolved provider/session configuration (tenant, credential refs already resolved upstream).
|
|
170
|
+
* @returns The Teams SDK instance to drive the meeting with.
|
|
171
|
+
*/
|
|
172
|
+
export type TeamsMeetingSdkFactory = (config?: Record<string, unknown>) => ITeamsMeetingSdk;
|
|
173
|
+
//# sourceMappingURL=teams-sdk.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"teams-sdk.d.ts","sourceRoot":"","sources":["../src/teams-sdk.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,uGAAuG;AACvG,MAAM,MAAM,oBAAoB,GAAG,WAAW,GAAG,WAAW,GAAG,UAAU,CAAC;AAE1E;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC7B,oGAAoG;IACpG,aAAa,EAAE,MAAM,CAAC;IACtB,0DAA0D;IAC1D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,sCAAsC;IACtC,IAAI,EAAE,oBAAoB,CAAC;IAC3B,mHAAmH;IACnH,MAAM,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,wGAAwG;AACxG,MAAM,WAAW,eAAe;IAC5B,0CAA0C;IAC1C,GAAG,EAAE,WAAW,CAAC;IACjB,qFAAqF;IACrF,aAAa,EAAE,MAAM,CAAC;IACtB,kEAAkE;IAClE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,2CAA2C;IAC3C,WAAW,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,mGAAmG;AACnG,MAAM,WAAW,aAAa;IAC1B,6FAA6F;IAC7F,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mEAAmE;IACnE,cAAc,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,qFAAqF;AACrF,MAAM,WAAW,eAAe;IAC5B,0DAA0D;IAC1D,gBAAgB,EAAE,MAAM,CAAC;IACzB,6EAA6E;IAC7E,MAAM,EAAE,MAAM,CAAC;CAClB;AAED;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC7B;;;;;OAKG;IACH,IAAI,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;IAEpD,qFAAqF;IACrF,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAEvB;;;;;OAKG;IACH,cAAc,CAAC,GAAG,EAAE,WAAW,GAAG,IAAI,CAAC;IAEvC;;;;;OAKG;IACH,YAAY,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,GAAG,IAAI,CAAC;IAEzD;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,EAAE,CAAC,WAAW,EAAE,gBAAgB,KAAK,IAAI,GAAG,IAAI,CAAC;IAErE;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,EAAE,CAAC,aAAa,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,CAAC;IAE9D;;;;;;;;OAQG;IACH,WAAW,CAAC,EAAE,EAAE,CAAC,aAAa,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,KAAK,IAAI,GAAG,IAAI,CAAC;IAExE;;;;OAIG;IACH,eAAe,IAAI,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAAC;IAE/C;;;;OAIG;IACH,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE7C;;;;OAIG;IACH,eAAe,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEtD;;;;;OAKG;IACH,cAAc,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,IAAI,CAAC;CACxC;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,sBAAsB,GAAG,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,gBAAgB,CAAC"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The injectable **Microsoft Teams calling-bot SDK seam** — the minimal set of operations
|
|
3
|
+
* {@link import('./teams-bridge.js').TeamsBridge} needs from Teams, declared as an interface so the driver
|
|
4
|
+
* builds and unit-tests against an in-memory fake with **no network and no real Teams / Azure
|
|
5
|
+
* Communication Services SDK**.
|
|
6
|
+
*
|
|
7
|
+
* ## Production binding (TODO at deployment)
|
|
8
|
+
* In production this interface is bound to the **Azure Communication Services (ACS) calling-bot** SDK
|
|
9
|
+
* plus the **Microsoft Graph cloud-communications API** (the `/communications/calls` "application-hosted
|
|
10
|
+
* media" bot model) so the bot can join a Teams meeting, pull per-participant PCM audio for diarization,
|
|
11
|
+
* push synthesized audio back, observe roster + raised-hand events, and post to the Teams meeting chat.
|
|
12
|
+
* The named operations map to the SDK as follows:
|
|
13
|
+
* - {@link join} / {@link leave} → the Graph `POST /communications/calls` (join meeting via the join
|
|
14
|
+
* URL / meeting coordinates) and the call's `DELETE` (leave) lifecycle.
|
|
15
|
+
* - {@link sendAudioFrame} → the ACS application-hosted-media outbound audio-socket (the bot's voice in).
|
|
16
|
+
* - {@link onAudioFrame} → the ACS per-participant inbound audio-socket callback (the source of speaker
|
|
17
|
+
* labels for diarization — Teams provides participant attribution on the media stream).
|
|
18
|
+
* - {@link onParticipantJoin} / {@link onParticipantLeave} / {@link getParticipants} → the call's
|
|
19
|
+
* `participants` collection + the `participantsUpdated` change notifications.
|
|
20
|
+
* - {@link onHandRaise} → the meeting's raised-hand signal. ⚠️ Partial: Teams surfaces raised hands in
|
|
21
|
+
* the meeting UI, but a clean per-participant *event* over the calling-bot API is not guaranteed on
|
|
22
|
+
* every tenant/build; the adapter wires it where the platform exposes it and otherwise no-ops.
|
|
23
|
+
* - {@link postChatMessage} → the Graph `POST /chats/{id}/messages` against the meeting's chat thread.
|
|
24
|
+
* - {@link muteParticipant} → the call's `participant:mute` action (requires the bot be an organizer /
|
|
25
|
+
* presenter with the relevant policy).
|
|
26
|
+
* - {@link onMeetingEnded} → the call's `callEnded` / `terminated` status notification.
|
|
27
|
+
*
|
|
28
|
+
* Binding the real SDK is a thin adapter that implements this interface; the driver and its tests do
|
|
29
|
+
* not change. **None of the ACS/Graph SDK types leak into this package.**
|
|
30
|
+
*
|
|
31
|
+
* See `/plans/realtime/realtime-bridges-architecture.md` §3, §8 (Microsoft Teams row) and
|
|
32
|
+
* `/guides/REALTIME_BRIDGES_GUIDE.md`.
|
|
33
|
+
*/
|
|
34
|
+
export {};
|
|
35
|
+
//# sourceMappingURL=teams-sdk.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"teams-sdk.js","sourceRoot":"","sources":["../src/teams-sdk.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG"}
|
package/package.json
CHANGED
|
@@ -1,10 +1,35 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@memberjunction/ai-bridge-teams",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "5.41.0",
|
|
5
|
+
"description": "MemberJunction: Microsoft Teams Realtime Bridge driver. Connects the realtime agent engine to a Teams meeting (audio in/out, diarized roster, participant mute, Teams meeting chat) via an injectable Teams calling-bot SDK seam (Azure Communication Services / Microsoft Graph cloud-communications), and contributes a Meeting Controls facilitator channel.",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"/dist"
|
|
10
|
+
],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"start": "ts-node-dev src/index.ts",
|
|
13
|
+
"build": "tsc && tsc-alias -f",
|
|
14
|
+
"test": "vitest run",
|
|
15
|
+
"test:watch": "vitest"
|
|
16
|
+
},
|
|
17
|
+
"author": "MemberJunction.com",
|
|
18
|
+
"license": "ISC",
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@memberjunction/core": "5.41.0",
|
|
21
|
+
"@memberjunction/global": "5.41.0",
|
|
22
|
+
"@memberjunction/core-entities": "5.41.0",
|
|
23
|
+
"@memberjunction/ai-bridge-base": "5.41.0"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/node": "24.10.11",
|
|
27
|
+
"ts-node-dev": "^2.0.0",
|
|
28
|
+
"typescript": "^5.9.3",
|
|
29
|
+
"vitest": "^4.0.18"
|
|
30
|
+
},
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "https://github.com/MemberJunction/MJ"
|
|
34
|
+
}
|
|
10
35
|
}
|