Run the Example
The repository ships one example, examples/basic-bridge - a complete, working embedding in about 30 lines. This page walks through it so you understand every moving part before writing your own.
What the example is
Section titled “What the example is”A single main.py that:
- loads a
.envfile, - starts the bridge with
start_server(), - plugs a custom vision hook into the agent’s
looktool, - shuts down gracefully on Ctrl-C / SIGTERM.
Run it
Section titled “Run it”pip install elevenlabs-msteams-bridgegit clone https://github.com/komaa-com/elevenlabs-msteams-bridge-pycd elevenlabs-msteams-bridge-py/examples/basic-bridgecp .env.example .env # fill in the three required valuespython main.pyIt prints the WebSocket URL to give StandIn:
Point your StandIn identity's agent WebSocket URL at ws://<this-host>:8080/voice/msteams/streamExpose port 8080 with a tunnel (see Getting Started), set your StandIn identity’s agent WebSocket URL to the wss:// form, and place a Teams call - your ElevenLabs agent answers.
The three values in .env:
| Variable | What to put there |
|---|---|
ELEVENLABS_API_KEY | Your ElevenLabs API key (server-side only; never sent to the Teams side). |
ELEVENLABS_AGENT_ID | The agent that should answer calls, from the Agents dashboard. |
WORKER_SHARED_SECRET | The shared secret from StandIn pairing - both sides must match exactly. |
The code, line by line
Section titled “The code, line by line”from elevenlabs_msteams_bridge import load_config, load_dotenv, start_server
async def describe(frame: dict, question: str) -> str: # frame["dataBase64"] is the JPEG/PNG frame, frame["mime"] its type, # frame["source"] is "camera" or "screenshare". return f"(stub) I received a {frame.get('mime')} frame from the {frame.get('source')}"
async def main() -> None: cfg = load_config() server = await start_server(cfg, vision=describe) ... await stop.wait() # run until SIGTERM / Ctrl-C await server.close() # drain live calls gracefullyload_dotenv()reads.envfrom the working directory (existing environment always wins), so the example runs the same way the CLI does.load_config()reads every setting from environment variables and fails loud on a missing required variable or a non-numeric number - a typo stops startup with a clear message instead of silently misbehaving.start_server(cfg, vision=describe)starts the WebSocket server and returns a handle. Thevisionargument is the interesting part, below.await server.close()ends every live call with a spoken-protocolsession.end(not a hard socket drop) before the process exits.
The vision hook
Section titled “The vision hook”When your agent calls its look client tool (“what do you see?”), the bridge hands your function the latest camera or screen-share frame and the agent’s question. Whatever text you return is what the agent gets as the tool result - the raw frame never leaves your process.
The stub just proves the wiring. Replace it with any vision-capable model:
async def describe(frame: dict, question: str) -> str: # e.g. call OpenAI, Azure OpenAI, Claude, or a local VLM here with # the data URL f"data:{frame['mime']};base64,{frame['dataBase64']}" return await my_vision_model(frame, question)Prefer configuration over code? Leave vision= out and set VISION_API_URL / VISION_MODEL instead - the built-in describer calls any OpenAI-compatible chat-completions endpoint. Pass vision=None to disable path-2 vision entirely. The trade-offs between the two vision paths are covered in Vision and Tools.
From example to your own service
Section titled “From example to your own service”The example is the recommended embedding shape. To grow it:
- add your own logic around
start_server()(it is just an awaitable in your event loop); - swap the vision stub for a real model;
- set the governor variables (
MAX_CALL_MINUTES,EL_TTS_VOICE_ID,GOODBYE_TEXT) before going to production; - for tests, inject a fake agent with the
connect_elargument - see Library API.
If you only need the stock behavior, skip the embedding entirely and run the elevenlabs-msteams-bridge CLI.