Put a LiveKit Agent on Microsoft Teams voice/video calls. Terminates the StandIn media bridge wire protocol on one side and a LiveKit room on the other: per-call rooms, explicit agent dispatch, 16 kHz PCM relay, data-topic context, call governors.
Project description
livekit-msteams-bridge (Python)
Put a LiveKit Agent - including avatar agents - on Microsoft Teams voice/video calls.
PyPI package:
livekit-msteams-bridge- the-pysuffix is only in this repository's name, to distinguish it from the Node.js sibling repo.
This is the Python sibling of @komaa/livekit-msteams-bridge
(Node.js) - same wire contract, same environment variables, drop-in interchangeable behind the same
.env file. It terminates the StandIn media bridge wire protocol on one side and a LiveKit room on
the other:
- One room per call: the bridge creates a fresh room, joins as a participant, and dispatches
your agent into it (explicit dispatch by
agent_name- LiveKit's recommended model). - No transcoding on our side: the worker speaks 16 kHz mono PCM16 natively; the room side uses
the SDK's resampling
AudioSource/AudioStream, so the bridge itself never transcodes. - Agent integration without Teams code: per-call metadata (
ctx.job.metadata- caller name, tenant, direction, AAD id when known) plus two data topics:teams.context(participants, DTMF, recording state) andteams.goodbye(the governor's goodbye line to speak). - Call governors: a bridge-side hard time cap (the agent speaks the goodbye), plus the StandIn-side governor.
- Hardened: HMAC-signed upgrades with replay guard, connection caps, dead-peer detection,
graceful SIGTERM drain, Prometheus
/metrics.
StandIn is the hosted media bridge that joins the Teams call and dials this bridge - you run no Teams media stack yourself.
Documentation: komaa-com.github.io/livekit-msteams-bridge-py (getting started, example walkthrough, agents and dispatch, configuration and library reference, wire protocol). Teams/StandIn setup lives at docs.komaa.com.
Install
pip install livekit-msteams-bridge
Requires Python 3.10+.
Run
LIVEKIT_URL=wss://your-project.livekit.cloud \
LIVEKIT_API_KEY=API... \
LIVEKIT_API_SECRET=... \
LIVEKIT_AGENT_NAME=teams-voice-agent \
WORKER_SHARED_SECRET=... \
livekit-msteams-bridge
A .env file in the working directory is loaded automatically (existing environment wins). The
bridge listens on ws://0.0.0.0:8080/voice/msteams/stream by default; StandIn appends /{callId}
per call. Expose the port with a tunnel and register the wss:// URL as your identity's
Agent voice URL in the StandIn dashboard.
LIVEKIT_AGENT_NAME must equal the agent_name your worker registers with
(WorkerOptions(entrypoint_fnc=..., agent_name="teams-voice-agent")). A mismatch is the classic
silent failure: the room is created, the caller hears nothing, and the worker never gets a job.
Embed
import asyncio
from livekit_msteams_bridge import load_config, start_server
async def main():
server = await start_server(load_config())
await asyncio.Event().wait() # run until cancelled
asyncio.run(main())
Tests can inject a fake room with the connect_room argument - see the test suite's
FakeRoomPort for the shape.
Configuration
Everything is environment variables; names are identical to the Node package.
| Variable | Required | Default | Meaning |
|---|---|---|---|
WORKER_SHARED_SECRET |
yes | - | Must equal the shared secret from StandIn pairing (HMAC upgrade check). |
LIVEKIT_URL |
yes | - | LiveKit server URL (wss://<project>.livekit.cloud or self-hosted). |
LIVEKIT_API_KEY / LIVEKIT_API_SECRET |
yes | - | Mint join tokens, dispatch agents, delete rooms. Server-side only. |
LIVEKIT_AGENT_NAME |
no | - | Named agent for explicit dispatch (recommended). Unset = automatic dispatch (prototype-only). |
LIVEKIT_ROOM_PREFIX |
no | msteams- |
Room name prefix; the room is {prefix}{callId} (sanitized). |
LIVEKIT_DELETE_ROOM_ON_END |
no | true |
Delete the room at teardown so the agent job ends immediately (billing hygiene). |
MAX_CALL_MINUTES |
no | 0 (off) |
Bridge-side hard cap per call; on expiry the agent is asked to say goodbye, then the call ends. |
GOODBYE_TEXT / GOODBYE_GRACE_MS |
no | (default line) / 8000 |
Goodbye wording (sent on teams.goodbye) and playout grace. The call ends GOODBYE_GRACE_MS + a fixed 500 ms scheduling buffer after the goodbye request. |
PORT / BIND |
no | 8080 / 0.0.0.0 |
Listen port / bind address. |
HMAC_FRESHNESS_MS |
no | 60000 |
Two-sided freshness window: a timestamp up to 60 s in the past OR the future is accepted, and the replay guard holds a used handshake until the timestamp ages out. |
MAX_CONNECTIONS / MAX_CONNECTIONS_PER_IP |
no | 64 / = total |
Connection caps. |
PRE_START_TIMEOUT_MS |
no | 10000 |
Drop a worker that authenticates but never sends session.start. |
WORKER_IDLE_TIMEOUT_MS |
no | 90000 |
Dead-peer window (the worker heartbeats every 30 s). |
TRUST_PROXY_XFF |
no | false |
Trust the first X-Forwarded-For hop for the per-IP cap. |
TLS_CERT_PATH / TLS_KEY_PATH |
no | - | Serve native TLS (wss). Otherwise front the plain WS with a TLS terminator. |
LOG_LEVEL |
no | info |
debug / info / warn / error. |
Endpoints
GET /healthz- liveness.GET /metrics- Prometheus counters (calls, rejections, relayed/dropped frames).GET /{...}/{callId}+ WebSocket upgrade - the worker wire, HMAC-signed withX-OpenClawTeamsBridge-Timestamp/X-OpenClawTeamsBridge-Signatureover"{timestampMs}.{callId}".
Notes for operators:
/healthzand/metricsare unauthenticated (only the WebSocket upgrade is HMAC-gated). They expose no call content - just liveness and counters - but if you would rather not leak call volumes, keep the port behind your ingress/tunnel rules.- Barge-in: interruption handling lives inside your LiveKit agent session (VAD, turn-taking), exactly as for WebRTC callers; the room transport gives the bridge no interruption event to relay, so the worker's own flush-on-silence smooths the tail end.
- The bridge participant's join token has a fixed 6 h TTL. Calls that should be allowed to run
longer than that need a re-join strategy; in practice set
MAX_CALL_MINUTESwell below it. - Inbound Teams video (
video.frame) is not forwarded to the room in this version - the bot's tile is rendered by the worker's own avatar. Publishing caller video as a room track is on the roadmap. - The Docker image exposes port 8080 and does not remap
PORT/BINDat the Docker layer; use-e PORT=... -p <host>:<port>together if you change them.
Agent integration points
Your agent needs no Teams-specific code, but three integration points are available:
agent_nameinWorkerOptions- must matchLIVEKIT_AGENT_NAMEfor explicit dispatch.ctx.job.metadata(JSON) - per-call context:source,caller_name,tenant_id,call_direction, anduser_id(AAD id when Teams provides one).- Data topics -
teams.context(participant count, DTMF, recording state) andteams.goodbye(the governor's goodbye line; have your handler speak it and interrupt the current turn).
License
MIT (c) Alaaeldin Elhenawy
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file livekit_msteams_bridge-0.1.0.tar.gz.
File metadata
- Download URL: livekit_msteams_bridge-0.1.0.tar.gz
- Upload date:
- Size: 42.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4edba7c2e21f1f016c7dbf42606b4e51b681589a5e9f3e5da88ec6d03f6ebac6
|
|
| MD5 |
ed29d73266e12753f95f98ddbacf6d3d
|
|
| BLAKE2b-256 |
6f4423de4355a416de29dfebe631ac1a1ace9ec4969445cd8f2982ba10c315b4
|
Provenance
The following attestation bundles were made for livekit_msteams_bridge-0.1.0.tar.gz:
Publisher:
publish.yml on komaa-com/livekit-msteams-bridge-py
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
livekit_msteams_bridge-0.1.0.tar.gz -
Subject digest:
4edba7c2e21f1f016c7dbf42606b4e51b681589a5e9f3e5da88ec6d03f6ebac6 - Sigstore transparency entry: 2148309472
- Sigstore integration time:
-
Permalink:
komaa-com/livekit-msteams-bridge-py@980e9677eb320dae90de0655bdfd22076d2e3891 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/komaa-com
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@980e9677eb320dae90de0655bdfd22076d2e3891 -
Trigger Event:
push
-
Statement type:
File details
Details for the file livekit_msteams_bridge-0.1.0-py3-none-any.whl.
File metadata
- Download URL: livekit_msteams_bridge-0.1.0-py3-none-any.whl
- Upload date:
- Size: 31.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4531ebfb3c415249127a985b7a0bb7a7fb6809271acca1469b017908f6e4ca4e
|
|
| MD5 |
8e1c398a0928536815f4c4a517a34a80
|
|
| BLAKE2b-256 |
922fb03ba023b0ab5b53e6356554e9608ae2ca9fcb3fbea6deebe04196e62415
|
Provenance
The following attestation bundles were made for livekit_msteams_bridge-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on komaa-com/livekit-msteams-bridge-py
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
livekit_msteams_bridge-0.1.0-py3-none-any.whl -
Subject digest:
4531ebfb3c415249127a985b7a0bb7a7fb6809271acca1469b017908f6e4ca4e - Sigstore transparency entry: 2148309473
- Sigstore integration time:
-
Permalink:
komaa-com/livekit-msteams-bridge-py@980e9677eb320dae90de0655bdfd22076d2e3891 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/komaa-com
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@980e9677eb320dae90de0655bdfd22076d2e3891 -
Trigger Event:
push
-
Statement type: