standin-sdk 0.1.0__tar.gz
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.
- standin_sdk-0.1.0/.gitignore +46 -0
- standin_sdk-0.1.0/LICENSE +21 -0
- standin_sdk-0.1.0/PKG-INFO +223 -0
- standin_sdk-0.1.0/README.md +179 -0
- standin_sdk-0.1.0/pyproject.toml +153 -0
- standin_sdk-0.1.0/standin/__init__.py +491 -0
- standin_sdk-0.1.0/standin/_exceptions.py +34 -0
- standin_sdk-0.1.0/standin/_hmac.py +138 -0
- standin_sdk-0.1.0/standin/_protocol_runtime.py +80 -0
- standin_sdk-0.1.0/standin/ambient.py +324 -0
- standin_sdk-0.1.0/standin/attachments.py +550 -0
- standin_sdk-0.1.0/standin/audio.py +184 -0
- standin_sdk-0.1.0/standin/avatar.py +257 -0
- standin_sdk-0.1.0/standin/call_server.py +1145 -0
- standin_sdk-0.1.0/standin/calltools.py +366 -0
- standin_sdk-0.1.0/standin/chat.py +705 -0
- standin_sdk-0.1.0/standin/config.py +88 -0
- standin_sdk-0.1.0/standin/consult.py +434 -0
- standin_sdk-0.1.0/standin/delivery.py +251 -0
- standin_sdk-0.1.0/standin/echo_guard.py +121 -0
- standin_sdk-0.1.0/standin/fetch.py +234 -0
- standin_sdk-0.1.0/standin/gate.py +307 -0
- standin_sdk-0.1.0/standin/handler.py +365 -0
- standin_sdk-0.1.0/standin/lane.py +282 -0
- standin_sdk-0.1.0/standin/lipsync.py +344 -0
- standin_sdk-0.1.0/standin/log.py +6 -0
- standin_sdk-0.1.0/standin/media.py +207 -0
- standin_sdk-0.1.0/standin/minutes.py +891 -0
- standin_sdk-0.1.0/standin/outbound.py +1122 -0
- standin_sdk-0.1.0/standin/plugins/__init__.py +49 -0
- standin_sdk-0.1.0/standin/plugins/_lazy.py +118 -0
- standin_sdk-0.1.0/standin/plugins/cartesia/__init__.py +67 -0
- standin_sdk-0.1.0/standin/plugins/cartesia/__main__.py +9 -0
- standin_sdk-0.1.0/standin/plugins/cartesia/agent.py +317 -0
- standin_sdk-0.1.0/standin/plugins/cartesia/config.py +72 -0
- standin_sdk-0.1.0/standin/plugins/cartesia/handler.py +164 -0
- standin_sdk-0.1.0/standin/plugins/deepgram/__init__.py +97 -0
- standin_sdk-0.1.0/standin/plugins/deepgram/__main__.py +9 -0
- standin_sdk-0.1.0/standin/plugins/deepgram/agent.py +386 -0
- standin_sdk-0.1.0/standin/plugins/deepgram/config.py +119 -0
- standin_sdk-0.1.0/standin/plugins/deepgram/handler.py +338 -0
- standin_sdk-0.1.0/standin/plugins/echo/README.md +61 -0
- standin_sdk-0.1.0/standin/plugins/echo/__init__.py +75 -0
- standin_sdk-0.1.0/standin/plugins/echo/__main__.py +8 -0
- standin_sdk-0.1.0/standin/plugins/elevenlabs/__init__.py +85 -0
- standin_sdk-0.1.0/standin/plugins/elevenlabs/__main__.py +9 -0
- standin_sdk-0.1.0/standin/plugins/elevenlabs/agent.py +381 -0
- standin_sdk-0.1.0/standin/plugins/elevenlabs/config.py +87 -0
- standin_sdk-0.1.0/standin/plugins/elevenlabs/handler.py +494 -0
- standin_sdk-0.1.0/standin/plugins/hermes_agent/README.md +184 -0
- standin_sdk-0.1.0/standin/plugins/hermes_agent/__init__.py +143 -0
- standin_sdk-0.1.0/standin/plugins/hermes_agent/__main__.py +10 -0
- standin_sdk-0.1.0/standin/plugins/hermes_agent/api.py +318 -0
- standin_sdk-0.1.0/standin/plugins/hermes_agent/cli.py +74 -0
- standin_sdk-0.1.0/standin/plugins/hermes_agent/config.py +229 -0
- standin_sdk-0.1.0/standin/plugins/hermes_agent/consult.py +149 -0
- standin_sdk-0.1.0/standin/plugins/hermes_agent/handler.py +616 -0
- standin_sdk-0.1.0/standin/plugins/hermes_agent/log.py +6 -0
- standin_sdk-0.1.0/standin/plugins/hermes_agent/platform.py +247 -0
- standin_sdk-0.1.0/standin/plugins/hermes_agent/realtime.py +561 -0
- standin_sdk-0.1.0/standin/plugins/hermes_agent/service.py +120 -0
- standin_sdk-0.1.0/standin/plugins/hermes_agent/tools.py +126 -0
- standin_sdk-0.1.0/standin/plugins/livekit/README.md +115 -0
- standin_sdk-0.1.0/standin/plugins/livekit/__init__.py +106 -0
- standin_sdk-0.1.0/standin/plugins/livekit/call.py +267 -0
- standin_sdk-0.1.0/standin/plugins/livekit/handler.py +634 -0
- standin_sdk-0.1.0/standin/plugins/livekit/log.py +6 -0
- standin_sdk-0.1.0/standin/plugins/livekit/service.py +219 -0
- standin_sdk-0.1.0/standin/protocol.py +129 -0
- standin_sdk-0.1.0/standin/py.typed +0 -0
- standin_sdk-0.1.0/standin/render.py +168 -0
- standin_sdk-0.1.0/standin/smoke.py +327 -0
- standin_sdk-0.1.0/standin/startup.py +147 -0
- standin_sdk-0.1.0/standin/tile.py +251 -0
- standin_sdk-0.1.0/standin/version.py +4 -0
- standin_sdk-0.1.0/standin/vision.py +403 -0
- standin_sdk-0.1.0/standin/vision_tools.py +715 -0
- standin_sdk-0.1.0/standin/voice.py +436 -0
- standin_sdk-0.1.0/tests/test_ambient.py +277 -0
- standin_sdk-0.1.0/tests/test_attachments.py +556 -0
- standin_sdk-0.1.0/tests/test_call_server.py +1404 -0
- standin_sdk-0.1.0/tests/test_call_server_config.py +56 -0
- standin_sdk-0.1.0/tests/test_call_server_startup.py +68 -0
- standin_sdk-0.1.0/tests/test_calltools.py +270 -0
- standin_sdk-0.1.0/tests/test_chat.py +349 -0
- standin_sdk-0.1.0/tests/test_chat_secret.py +50 -0
- standin_sdk-0.1.0/tests/test_conformance.py +445 -0
- standin_sdk-0.1.0/tests/test_consult.py +312 -0
- standin_sdk-0.1.0/tests/test_delivery.py +203 -0
- standin_sdk-0.1.0/tests/test_hermes_agent_host_config.py +89 -0
- standin_sdk-0.1.0/tests/test_hermes_agent_realtime.py +110 -0
- standin_sdk-0.1.0/tests/test_hermes_platform.py +172 -0
- standin_sdk-0.1.0/tests/test_hmac_conformance.py +157 -0
- standin_sdk-0.1.0/tests/test_lane.py +291 -0
- standin_sdk-0.1.0/tests/test_lipsync.py +470 -0
- standin_sdk-0.1.0/tests/test_media.py +159 -0
- standin_sdk-0.1.0/tests/test_minutes.py +959 -0
- standin_sdk-0.1.0/tests/test_one_package.py +275 -0
- standin_sdk-0.1.0/tests/test_outbound.py +431 -0
- standin_sdk-0.1.0/tests/test_outbound_lane.py +461 -0
- standin_sdk-0.1.0/tests/test_plugin_cartesia.py +208 -0
- standin_sdk-0.1.0/tests/test_plugin_deepgram.py +378 -0
- standin_sdk-0.1.0/tests/test_plugin_echo.py +173 -0
- standin_sdk-0.1.0/tests/test_plugin_elevenlabs.py +494 -0
- standin_sdk-0.1.0/tests/test_plugin_hermes_agent.py +1240 -0
- standin_sdk-0.1.0/tests/test_plugin_livekit.py +357 -0
- standin_sdk-0.1.0/tests/test_plugin_livekit_runtime.py +156 -0
- standin_sdk-0.1.0/tests/test_smoke.py +76 -0
- standin_sdk-0.1.0/tests/test_startup_buffer.py +112 -0
- standin_sdk-0.1.0/tests/test_vision_tools.py +623 -0
- standin_sdk-0.1.0/tests/test_voice.py +361 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# This repo is PUBLIC. Anything committed here is world-readable the moment it is pushed,
|
|
2
|
+
# and rewriting history does not un-publish it.
|
|
3
|
+
|
|
4
|
+
# Real environment files. Only .env.example (placeholders) belongs in git.
|
|
5
|
+
.env
|
|
6
|
+
.env.*
|
|
7
|
+
!.env.example
|
|
8
|
+
|
|
9
|
+
# Credential material that gets dropped into a working tree during setup.
|
|
10
|
+
*.pem
|
|
11
|
+
*.key
|
|
12
|
+
*.p12
|
|
13
|
+
*.pfx
|
|
14
|
+
id_rsa
|
|
15
|
+
id_ed25519
|
|
16
|
+
.npmrc
|
|
17
|
+
.netrc
|
|
18
|
+
|
|
19
|
+
# macOS Finder metadata. One of these reached a public repo once already.
|
|
20
|
+
.DS_Store
|
|
21
|
+
|
|
22
|
+
# --- build and tool caches ---------------------------------------------------
|
|
23
|
+
# Added with the monorepo: the repo now carries a Python half and a TypeScript
|
|
24
|
+
# half, and each brings its own.
|
|
25
|
+
node_modules/
|
|
26
|
+
dist/
|
|
27
|
+
*.tsbuildinfo
|
|
28
|
+
__pycache__/
|
|
29
|
+
*.py[cod]
|
|
30
|
+
.pytest_cache/
|
|
31
|
+
.ruff_cache/
|
|
32
|
+
.mypy_cache/
|
|
33
|
+
.vite/
|
|
34
|
+
|
|
35
|
+
# Virtualenvs. `make install` creates libraries/python/.venv; it is a build
|
|
36
|
+
# artifact, not a checked-in dependency.
|
|
37
|
+
.venv/
|
|
38
|
+
venv/
|
|
39
|
+
|
|
40
|
+
# Stray session-id artifacts some tools drop in the working directory when a
|
|
41
|
+
# path like ":memory:" is used as a filename. Not ours and never publishable.
|
|
42
|
+
:memory:*
|
|
43
|
+
*.ses
|
|
44
|
+
|
|
45
|
+
# Local tooling
|
|
46
|
+
AGENTS.md
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Komaa DigiTech
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: standin-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Put your AI agent into a Microsoft Teams call. One SDK, every plugin.
|
|
5
|
+
Project-URL: Homepage, https://standin.komaa.com
|
|
6
|
+
Project-URL: Documentation, https://docs.komaa.com
|
|
7
|
+
Project-URL: Source, https://github.com/komaa-com/standin
|
|
8
|
+
Project-URL: Issues, https://github.com/komaa-com/standin/issues
|
|
9
|
+
Author-email: Komaa DigiTech <support@komaa.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: agents,ai,audio,hermes_agent,livekit,microsoft teams,realtime,speech-to-speech,standin,voice
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Framework :: AsyncIO
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Topic :: Communications :: Conferencing
|
|
25
|
+
Classifier: Topic :: Multimedia :: Sound/Audio
|
|
26
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
27
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
28
|
+
Classifier: Typing :: Typed
|
|
29
|
+
Requires-Python: >=3.10
|
|
30
|
+
Requires-Dist: aiohttp>=3.9
|
|
31
|
+
Provides-Extra: all
|
|
32
|
+
Requires-Dist: livekit-agents>=1.6.10; extra == 'all'
|
|
33
|
+
Requires-Dist: pillow>=10.0; extra == 'all'
|
|
34
|
+
Requires-Dist: pypdfium2>=4.0; extra == 'all'
|
|
35
|
+
Provides-Extra: hermes-agent
|
|
36
|
+
Provides-Extra: livekit
|
|
37
|
+
Requires-Dist: livekit-agents>=1.6.10; extra == 'livekit'
|
|
38
|
+
Provides-Extra: render
|
|
39
|
+
Requires-Dist: pillow>=10.0; extra == 'render'
|
|
40
|
+
Requires-Dist: pypdfium2>=4.0; extra == 'render'
|
|
41
|
+
Provides-Extra: tile
|
|
42
|
+
Requires-Dist: pillow>=10.0; extra == 'tile'
|
|
43
|
+
Description-Content-Type: text/markdown
|
|
44
|
+
|
|
45
|
+
# standin-sdk
|
|
46
|
+
|
|
47
|
+
One package. [StandIn](https://standin.komaa.com) in Python, core and every
|
|
48
|
+
plugin, in a single import.
|
|
49
|
+
|
|
50
|
+
StandIn is the hosted bridge that joins a Microsoft Teams call. It owns the
|
|
51
|
+
Microsoft side entirely, the bot registration, Graph, media negotiation, the
|
|
52
|
+
avatar tile, and talks to your worker over one authenticated socket per call.
|
|
53
|
+
This package is that socket's other end.
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from standin import CallServer, CallSession, ChatChannel, FrameAligner
|
|
57
|
+
from standin.plugins.livekit import TeamsCall
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Install
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pip install standin-sdk
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
That one line is already enough for ElevenLabs, Deepgram and Cartesia: they are
|
|
67
|
+
reached over an ordinary WebSocket, so they need nothing beyond aiohttp, which
|
|
68
|
+
is the only thing the base install pulls.
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
STANDIN_SECRET=... ELEVENLABS_API_KEY=... ELEVENLABS_AGENT_ID=... \
|
|
72
|
+
python -m standin.plugins.elevenlabs
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Add an extra only for a framework that runs inside your process:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
pip install "standin-sdk[livekit]"
|
|
79
|
+
pip install "standin-sdk[hermes-agent]"
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Each plugin has a runnable example at the root of the repo:
|
|
83
|
+
[ElevenLabs](../../examples/elevenlabs-msteams-connector),
|
|
84
|
+
[Deepgram](../../examples/deepgram-msteams-connector),
|
|
85
|
+
[Cartesia](../../examples/cartesia-msteams-connector),
|
|
86
|
+
[LiveKit](../../examples/livekit-msteams-connector) and
|
|
87
|
+
[Hermes Agent](../../examples/hermes-msteams-connector). Start a custom
|
|
88
|
+
plugin from [echo](standin/plugins/echo). OpenAI and OpenClaw are
|
|
89
|
+
TypeScript, in [the other half of the repo](../typescript).
|
|
90
|
+
|
|
91
|
+
## One package, on purpose
|
|
92
|
+
|
|
93
|
+
A call surface is never done: screen share, call back, camera, chat, managed
|
|
94
|
+
chat, adaptive cards. Every one of them has to reach every framework StandIn
|
|
95
|
+
supports. Split across a wheel per framework, each surface costs N hand-threaded
|
|
96
|
+
releases and N version matrices; here it costs one directory under
|
|
97
|
+
`standin/plugins/` and one line in `standin/__init__.py`.
|
|
98
|
+
|
|
99
|
+
The base install stays small anyway, because that is what extras are for:
|
|
100
|
+
|
|
101
|
+
| Install | You get |
|
|
102
|
+
|---|---|
|
|
103
|
+
| `pip install standin-sdk` | The core, and every plugin reached over a socket: echo, ElevenLabs, Deepgram and Cartesia. aiohttp is the only dependency. |
|
|
104
|
+
| `pip install "standin-sdk[livekit]"` | The above, plus livekit-agents. |
|
|
105
|
+
| `pip install "standin-sdk[hermes-agent]"` | The above, plus the Hermes adapter. Hermes Agent itself ships the host and loads the adapter in-process. |
|
|
106
|
+
| `pip install "standin-sdk[all]"` | Everything. |
|
|
107
|
+
|
|
108
|
+
`import standin` never imports a framework. Plugins load the first time you
|
|
109
|
+
name one, so LiveKit code on disk costs a Hermes user nothing, and a missing
|
|
110
|
+
extra raises `PluginNotInstalled` with the install line in it, not a
|
|
111
|
+
`ModuleNotFoundError` from inside somebody else's package.
|
|
112
|
+
|
|
113
|
+
## What it gives you
|
|
114
|
+
|
|
115
|
+
| | |
|
|
116
|
+
|---|---|
|
|
117
|
+
| `CallServer` | Answers the socket StandIn dials. Owns the HMAC handshake and its replay guard, capacity and draining, the wire protocol, sequence numbers and the audio timeline, and the watchdogs that end a call nobody closed. |
|
|
118
|
+
| `CallHandler` | The five-method seam a plugin implements. Every method optional. |
|
|
119
|
+
| `VideoFrame` | One frame of what the caller is showing, on the vision lane. |
|
|
120
|
+
| `ChatChannel` | The Microsoft Teams messages lane. Dialed **out** from your worker, so chat needs no listener, no open port, and no bot credential of your own. |
|
|
121
|
+
|
|
122
|
+
## Writing a plugin
|
|
123
|
+
|
|
124
|
+
The whole contract is five methods, and you implement only the ones you need:
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
from standin import CallServer, CallSession
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class EchoHandler:
|
|
131
|
+
async def on_start(self, session: CallSession) -> None:
|
|
132
|
+
self._call = session
|
|
133
|
+
|
|
134
|
+
async def on_caller_audio(self, pcm: bytes) -> None:
|
|
135
|
+
await self._call.send_audio(pcm) # PCM16, 16 kHz, mono
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
server = CallServer(handler_factory=EchoHandler)
|
|
139
|
+
await server.start()
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
This illustrates the handler contract. [echo](standin/plugins/echo) adds
|
|
143
|
+
the runnable entry point and keeps the listener alive:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
STANDIN_SECRET=... python -m standin.plugins.echo
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Call your number and you hear yourself. Run that before you suspect your own
|
|
150
|
+
agent: if the echo answers, your secret, your tunnel and your StandIn identity
|
|
151
|
+
are all correct.
|
|
152
|
+
|
|
153
|
+
Everything that is the same for every framework lives in `CallServer`, which is
|
|
154
|
+
why plugins stay small. Everything that differs, what runs the agent, is
|
|
155
|
+
yours.
|
|
156
|
+
|
|
157
|
+
## Configuration
|
|
158
|
+
|
|
159
|
+
Environment only, matching how the plugins read their keys.
|
|
160
|
+
|
|
161
|
+
| Variable | Default | Meaning |
|
|
162
|
+
|---|---|---|
|
|
163
|
+
| `STANDIN_SECRET` | *(required)* | Connection secret from the StandIn portal. Arms the listener. |
|
|
164
|
+
| `STANDIN_PORT` | `9442` | Port the call listener binds. |
|
|
165
|
+
| `STANDIN_HOST` | `0.0.0.0` | Bind address. Use `127.0.0.1` when only a local tunnel should reach it. |
|
|
166
|
+
| `STANDIN_WS_PATH` | `/msteams/calling` | Path StandIn dials. |
|
|
167
|
+
| `STANDIN_CHAT_URL` | `wss://teams.standin.komaa.com/api/chat/channel` | Chat channel the worker dials out to. |
|
|
168
|
+
|
|
169
|
+
The listener authenticates WebSocket upgrades with HMAC. Terminate TLS at your
|
|
170
|
+
public ingress so StandIn can reach it over `wss://`.
|
|
171
|
+
|
|
172
|
+
## Signing control requests
|
|
173
|
+
|
|
174
|
+
Use `sign_request` for HTTP control requests. HMAC v2 binds the method, request
|
|
175
|
+
path and hash of the entire body, including `tenantId`:
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
from standin import SIGNATURE_V2_HEADER, TIMESTAMP_HEADER, now_ms, sign_request
|
|
179
|
+
|
|
180
|
+
timestamp = str(now_ms())
|
|
181
|
+
headers = {
|
|
182
|
+
TIMESTAMP_HEADER: timestamp,
|
|
183
|
+
SIGNATURE_V2_HEADER: sign_request(secret, timestamp, "POST", "/api/calls", raw_body),
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Serialize the body once and send those same `raw_body` bytes. These helpers
|
|
188
|
+
prepare signatures; they do not send HTTP requests. `sign_body` / `verify_body`
|
|
189
|
+
are for chat POST bodies, with a 300-second replay window. WebSocket call and
|
|
190
|
+
chat-channel handshakes keep `sign_handshake` / `verify_handshake` and their
|
|
191
|
+
separate 60-second window.
|
|
192
|
+
|
|
193
|
+
## Audio
|
|
194
|
+
|
|
195
|
+
PCM16, 16 kHz, mono, little-endian, both directions. The server owns the
|
|
196
|
+
outbound sequence number and timeline, so a handler that swaps or re-publishes
|
|
197
|
+
its audio source cannot make timestamps jump backwards.
|
|
198
|
+
|
|
199
|
+
## The layout
|
|
200
|
+
|
|
201
|
+
```
|
|
202
|
+
standin/
|
|
203
|
+
__init__.py the public API, and the lazy hook that keeps it cheap
|
|
204
|
+
call_server.py handler.py chat.py audio.py protocol.py ...
|
|
205
|
+
vision.py what the caller shows you, and what you show back
|
|
206
|
+
avatar.py the face the caller sees: expression and lip-sync
|
|
207
|
+
fetch.py fetching a URL a model chose, safely
|
|
208
|
+
plugins/
|
|
209
|
+
echo/ answers a call with the caller's own voice. No extra.
|
|
210
|
+
elevenlabs/ an ElevenLabs agent takes the call. No extra.
|
|
211
|
+
deepgram/ a Deepgram Voice Agent takes the call. No extra.
|
|
212
|
+
cartesia/ a Cartesia Line agent takes the call. No extra.
|
|
213
|
+
livekit/ a LiveKit Agent takes the call.
|
|
214
|
+
hermes/ a Hermes agent takes the call, in the Hermes process.
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Links
|
|
218
|
+
|
|
219
|
+
- [Documentation](https://docs.komaa.com)
|
|
220
|
+
- [StandIn](https://standin.komaa.com)
|
|
221
|
+
- [Source](https://github.com/komaa-com/standin)
|
|
222
|
+
|
|
223
|
+
[MIT](LICENSE).
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# standin-sdk
|
|
2
|
+
|
|
3
|
+
One package. [StandIn](https://standin.komaa.com) in Python, core and every
|
|
4
|
+
plugin, in a single import.
|
|
5
|
+
|
|
6
|
+
StandIn is the hosted bridge that joins a Microsoft Teams call. It owns the
|
|
7
|
+
Microsoft side entirely, the bot registration, Graph, media negotiation, the
|
|
8
|
+
avatar tile, and talks to your worker over one authenticated socket per call.
|
|
9
|
+
This package is that socket's other end.
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
from standin import CallServer, CallSession, ChatChannel, FrameAligner
|
|
13
|
+
from standin.plugins.livekit import TeamsCall
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install standin-sdk
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
That one line is already enough for ElevenLabs, Deepgram and Cartesia: they are
|
|
23
|
+
reached over an ordinary WebSocket, so they need nothing beyond aiohttp, which
|
|
24
|
+
is the only thing the base install pulls.
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
STANDIN_SECRET=... ELEVENLABS_API_KEY=... ELEVENLABS_AGENT_ID=... \
|
|
28
|
+
python -m standin.plugins.elevenlabs
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Add an extra only for a framework that runs inside your process:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install "standin-sdk[livekit]"
|
|
35
|
+
pip install "standin-sdk[hermes-agent]"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Each plugin has a runnable example at the root of the repo:
|
|
39
|
+
[ElevenLabs](../../examples/elevenlabs-msteams-connector),
|
|
40
|
+
[Deepgram](../../examples/deepgram-msteams-connector),
|
|
41
|
+
[Cartesia](../../examples/cartesia-msteams-connector),
|
|
42
|
+
[LiveKit](../../examples/livekit-msteams-connector) and
|
|
43
|
+
[Hermes Agent](../../examples/hermes-msteams-connector). Start a custom
|
|
44
|
+
plugin from [echo](standin/plugins/echo). OpenAI and OpenClaw are
|
|
45
|
+
TypeScript, in [the other half of the repo](../typescript).
|
|
46
|
+
|
|
47
|
+
## One package, on purpose
|
|
48
|
+
|
|
49
|
+
A call surface is never done: screen share, call back, camera, chat, managed
|
|
50
|
+
chat, adaptive cards. Every one of them has to reach every framework StandIn
|
|
51
|
+
supports. Split across a wheel per framework, each surface costs N hand-threaded
|
|
52
|
+
releases and N version matrices; here it costs one directory under
|
|
53
|
+
`standin/plugins/` and one line in `standin/__init__.py`.
|
|
54
|
+
|
|
55
|
+
The base install stays small anyway, because that is what extras are for:
|
|
56
|
+
|
|
57
|
+
| Install | You get |
|
|
58
|
+
|---|---|
|
|
59
|
+
| `pip install standin-sdk` | The core, and every plugin reached over a socket: echo, ElevenLabs, Deepgram and Cartesia. aiohttp is the only dependency. |
|
|
60
|
+
| `pip install "standin-sdk[livekit]"` | The above, plus livekit-agents. |
|
|
61
|
+
| `pip install "standin-sdk[hermes-agent]"` | The above, plus the Hermes adapter. Hermes Agent itself ships the host and loads the adapter in-process. |
|
|
62
|
+
| `pip install "standin-sdk[all]"` | Everything. |
|
|
63
|
+
|
|
64
|
+
`import standin` never imports a framework. Plugins load the first time you
|
|
65
|
+
name one, so LiveKit code on disk costs a Hermes user nothing, and a missing
|
|
66
|
+
extra raises `PluginNotInstalled` with the install line in it, not a
|
|
67
|
+
`ModuleNotFoundError` from inside somebody else's package.
|
|
68
|
+
|
|
69
|
+
## What it gives you
|
|
70
|
+
|
|
71
|
+
| | |
|
|
72
|
+
|---|---|
|
|
73
|
+
| `CallServer` | Answers the socket StandIn dials. Owns the HMAC handshake and its replay guard, capacity and draining, the wire protocol, sequence numbers and the audio timeline, and the watchdogs that end a call nobody closed. |
|
|
74
|
+
| `CallHandler` | The five-method seam a plugin implements. Every method optional. |
|
|
75
|
+
| `VideoFrame` | One frame of what the caller is showing, on the vision lane. |
|
|
76
|
+
| `ChatChannel` | The Microsoft Teams messages lane. Dialed **out** from your worker, so chat needs no listener, no open port, and no bot credential of your own. |
|
|
77
|
+
|
|
78
|
+
## Writing a plugin
|
|
79
|
+
|
|
80
|
+
The whole contract is five methods, and you implement only the ones you need:
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from standin import CallServer, CallSession
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class EchoHandler:
|
|
87
|
+
async def on_start(self, session: CallSession) -> None:
|
|
88
|
+
self._call = session
|
|
89
|
+
|
|
90
|
+
async def on_caller_audio(self, pcm: bytes) -> None:
|
|
91
|
+
await self._call.send_audio(pcm) # PCM16, 16 kHz, mono
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
server = CallServer(handler_factory=EchoHandler)
|
|
95
|
+
await server.start()
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
This illustrates the handler contract. [echo](standin/plugins/echo) adds
|
|
99
|
+
the runnable entry point and keeps the listener alive:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
STANDIN_SECRET=... python -m standin.plugins.echo
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Call your number and you hear yourself. Run that before you suspect your own
|
|
106
|
+
agent: if the echo answers, your secret, your tunnel and your StandIn identity
|
|
107
|
+
are all correct.
|
|
108
|
+
|
|
109
|
+
Everything that is the same for every framework lives in `CallServer`, which is
|
|
110
|
+
why plugins stay small. Everything that differs, what runs the agent, is
|
|
111
|
+
yours.
|
|
112
|
+
|
|
113
|
+
## Configuration
|
|
114
|
+
|
|
115
|
+
Environment only, matching how the plugins read their keys.
|
|
116
|
+
|
|
117
|
+
| Variable | Default | Meaning |
|
|
118
|
+
|---|---|---|
|
|
119
|
+
| `STANDIN_SECRET` | *(required)* | Connection secret from the StandIn portal. Arms the listener. |
|
|
120
|
+
| `STANDIN_PORT` | `9442` | Port the call listener binds. |
|
|
121
|
+
| `STANDIN_HOST` | `0.0.0.0` | Bind address. Use `127.0.0.1` when only a local tunnel should reach it. |
|
|
122
|
+
| `STANDIN_WS_PATH` | `/msteams/calling` | Path StandIn dials. |
|
|
123
|
+
| `STANDIN_CHAT_URL` | `wss://teams.standin.komaa.com/api/chat/channel` | Chat channel the worker dials out to. |
|
|
124
|
+
|
|
125
|
+
The listener authenticates WebSocket upgrades with HMAC. Terminate TLS at your
|
|
126
|
+
public ingress so StandIn can reach it over `wss://`.
|
|
127
|
+
|
|
128
|
+
## Signing control requests
|
|
129
|
+
|
|
130
|
+
Use `sign_request` for HTTP control requests. HMAC v2 binds the method, request
|
|
131
|
+
path and hash of the entire body, including `tenantId`:
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
from standin import SIGNATURE_V2_HEADER, TIMESTAMP_HEADER, now_ms, sign_request
|
|
135
|
+
|
|
136
|
+
timestamp = str(now_ms())
|
|
137
|
+
headers = {
|
|
138
|
+
TIMESTAMP_HEADER: timestamp,
|
|
139
|
+
SIGNATURE_V2_HEADER: sign_request(secret, timestamp, "POST", "/api/calls", raw_body),
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Serialize the body once and send those same `raw_body` bytes. These helpers
|
|
144
|
+
prepare signatures; they do not send HTTP requests. `sign_body` / `verify_body`
|
|
145
|
+
are for chat POST bodies, with a 300-second replay window. WebSocket call and
|
|
146
|
+
chat-channel handshakes keep `sign_handshake` / `verify_handshake` and their
|
|
147
|
+
separate 60-second window.
|
|
148
|
+
|
|
149
|
+
## Audio
|
|
150
|
+
|
|
151
|
+
PCM16, 16 kHz, mono, little-endian, both directions. The server owns the
|
|
152
|
+
outbound sequence number and timeline, so a handler that swaps or re-publishes
|
|
153
|
+
its audio source cannot make timestamps jump backwards.
|
|
154
|
+
|
|
155
|
+
## The layout
|
|
156
|
+
|
|
157
|
+
```
|
|
158
|
+
standin/
|
|
159
|
+
__init__.py the public API, and the lazy hook that keeps it cheap
|
|
160
|
+
call_server.py handler.py chat.py audio.py protocol.py ...
|
|
161
|
+
vision.py what the caller shows you, and what you show back
|
|
162
|
+
avatar.py the face the caller sees: expression and lip-sync
|
|
163
|
+
fetch.py fetching a URL a model chose, safely
|
|
164
|
+
plugins/
|
|
165
|
+
echo/ answers a call with the caller's own voice. No extra.
|
|
166
|
+
elevenlabs/ an ElevenLabs agent takes the call. No extra.
|
|
167
|
+
deepgram/ a Deepgram Voice Agent takes the call. No extra.
|
|
168
|
+
cartesia/ a Cartesia Line agent takes the call. No extra.
|
|
169
|
+
livekit/ a LiveKit Agent takes the call.
|
|
170
|
+
hermes/ a Hermes agent takes the call, in the Hermes process.
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Links
|
|
174
|
+
|
|
175
|
+
- [Documentation](https://docs.komaa.com)
|
|
176
|
+
- [StandIn](https://standin.komaa.com)
|
|
177
|
+
- [Source](https://github.com/komaa-com/standin)
|
|
178
|
+
|
|
179
|
+
[MIT](LICENSE).
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# The Python half of StandIn: ONE package, ONE manifest.
|
|
2
|
+
#
|
|
3
|
+
# Everything ships as `standin-sdk`. The core protocol modules sit at the top of
|
|
4
|
+
# standin/, and every framework plugin sits under standin/plugins/,
|
|
5
|
+
# organised by the thing it integrates. There is no sdk/ package and no
|
|
6
|
+
# plugins/* packages, because a capability (screen share, call-back, camera,
|
|
7
|
+
# chat, adaptive cards, speech-to-speech) must land ONCE and reach every
|
|
8
|
+
# plugin, not be threaded by hand into N wheels.
|
|
9
|
+
#
|
|
10
|
+
# The TypeScript half is ../typescript, at parity, speaking the same wire
|
|
11
|
+
# protocol from ../../protocol. Which language a plugin lives in is
|
|
12
|
+
# decided by the framework it integrates, not by preference: OpenClaw and n8n
|
|
13
|
+
# are TypeScript, Hermes is Python, LiveKit is both.
|
|
14
|
+
#
|
|
15
|
+
# Adding a plugin: create standin/plugins/<name>/, add its heavy
|
|
16
|
+
# dependencies as an extra below, and add one line to standin/__init__.py's
|
|
17
|
+
# _PLUGINS tuple. Nothing else changes, and no new package is published.
|
|
18
|
+
|
|
19
|
+
[build-system]
|
|
20
|
+
requires = ["hatchling"]
|
|
21
|
+
build-backend = "hatchling.build"
|
|
22
|
+
|
|
23
|
+
[project]
|
|
24
|
+
name = "standin-sdk"
|
|
25
|
+
dynamic = ["version"]
|
|
26
|
+
description = "Put your AI agent into a Microsoft Teams call. One SDK, every plugin."
|
|
27
|
+
readme = "README.md"
|
|
28
|
+
license = "MIT"
|
|
29
|
+
license-files = ["LICENSE"]
|
|
30
|
+
requires-python = ">=3.10"
|
|
31
|
+
authors = [{ name = "Komaa DigiTech", email = "support@komaa.com" }]
|
|
32
|
+
keywords = [
|
|
33
|
+
"ai",
|
|
34
|
+
"agents",
|
|
35
|
+
"voice",
|
|
36
|
+
"realtime",
|
|
37
|
+
"audio",
|
|
38
|
+
"speech-to-speech",
|
|
39
|
+
"microsoft teams",
|
|
40
|
+
"livekit",
|
|
41
|
+
"hermes_agent",
|
|
42
|
+
"standin",
|
|
43
|
+
]
|
|
44
|
+
classifiers = [
|
|
45
|
+
"Development Status :: 4 - Beta",
|
|
46
|
+
"Intended Audience :: Developers",
|
|
47
|
+
"License :: OSI Approved :: MIT License",
|
|
48
|
+
"Operating System :: OS Independent",
|
|
49
|
+
"Framework :: AsyncIO",
|
|
50
|
+
"Topic :: Communications :: Conferencing",
|
|
51
|
+
"Topic :: Multimedia :: Sound/Audio",
|
|
52
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
53
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
54
|
+
"Programming Language :: Python :: 3",
|
|
55
|
+
"Programming Language :: Python :: 3.10",
|
|
56
|
+
"Programming Language :: Python :: 3.11",
|
|
57
|
+
"Programming Language :: Python :: 3.12",
|
|
58
|
+
"Programming Language :: Python :: 3.13",
|
|
59
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
60
|
+
"Typing :: Typed",
|
|
61
|
+
]
|
|
62
|
+
# aiohttp and nothing else. `import standin` must work on a base install, which
|
|
63
|
+
# is why no module under standin/plugins/ imports its framework at module
|
|
64
|
+
# load time: the extras below are the ONLY thing that pulls a framework in.
|
|
65
|
+
dependencies = ["aiohttp>=3.9"]
|
|
66
|
+
|
|
67
|
+
# The size control: one extra per plugin, so there is ONE documented
|
|
68
|
+
# install line and one place to look for what StandIn connects to:
|
|
69
|
+
#
|
|
70
|
+
# pip install "standin-sdk[livekit]"
|
|
71
|
+
#
|
|
72
|
+
# The base install answers a real Microsoft Teams call on its own (that is what
|
|
73
|
+
# standin.plugins.echo is), so an extra is only ever the framework you
|
|
74
|
+
# chose to drive it with.
|
|
75
|
+
[project.optional-dependencies]
|
|
76
|
+
# Empty on purpose, and it must stay a real extra. Hermes Agent ships its own
|
|
77
|
+
# host and loads this adapter through the entry point below, so there is nothing
|
|
78
|
+
# left for pip to fetch. Keeping the extra declared means the documented
|
|
79
|
+
# `pip install "standin-sdk[hermes-agent]"` resolves, installs the adapter, and
|
|
80
|
+
# adds nothing that pip could get wrong. Anything Hermes-only that IS packaged
|
|
81
|
+
# later goes here, not into the base.
|
|
82
|
+
hermes-agent = []
|
|
83
|
+
livekit = ["livekit-agents>=1.6.10"]
|
|
84
|
+
# Encoding frames for the avatar tile relay. Optional because most deployments
|
|
85
|
+
# never put their own video on the tile: they let StandIn render the avatar.
|
|
86
|
+
tile = ["pillow>=10.0"]
|
|
87
|
+
# Rendering a document onto the bot's tile. Optional because most deployments
|
|
88
|
+
# never show a file, and a PDF engine in every install is the wrong trade.
|
|
89
|
+
# Office documents additionally need LibreOffice on PATH, which is not a wheel.
|
|
90
|
+
render = ["pypdfium2>=4.0", "pillow>=10.0"]
|
|
91
|
+
all = ["livekit-agents>=1.6.10", "pillow>=10.0", "pypdfium2>=4.0"]
|
|
92
|
+
|
|
93
|
+
# How Hermes finds the plugin. The key is the name shown in `hermes plugins
|
|
94
|
+
# list` and used in `plugins.enabled`; the value is the module whose
|
|
95
|
+
# register(ctx) Hermes calls. The key is part of the operator's config.yaml, so
|
|
96
|
+
# it is a compatibility surface: renaming it would break every deployment.
|
|
97
|
+
[project.entry-points."hermes_agent.plugins"]
|
|
98
|
+
msteams_bridge = "standin.plugins.hermes_agent"
|
|
99
|
+
|
|
100
|
+
[project.urls]
|
|
101
|
+
Homepage = "https://standin.komaa.com"
|
|
102
|
+
Documentation = "https://docs.komaa.com"
|
|
103
|
+
Source = "https://github.com/komaa-com/standin"
|
|
104
|
+
Issues = "https://github.com/komaa-com/standin/issues"
|
|
105
|
+
|
|
106
|
+
[tool.hatch.version]
|
|
107
|
+
path = "standin/version.py"
|
|
108
|
+
|
|
109
|
+
[tool.hatch.build.targets.wheel]
|
|
110
|
+
packages = ["standin"]
|
|
111
|
+
|
|
112
|
+
[tool.hatch.build.targets.sdist]
|
|
113
|
+
include = ["/standin", "/tests", "/README.md", "/LICENSE"]
|
|
114
|
+
|
|
115
|
+
[dependency-groups]
|
|
116
|
+
dev = [
|
|
117
|
+
"pytest>=8.0",
|
|
118
|
+
"pytest-asyncio>=0.25",
|
|
119
|
+
"ruff",
|
|
120
|
+
"mypy",
|
|
121
|
+
]
|
|
122
|
+
|
|
123
|
+
[tool.ruff]
|
|
124
|
+
line-length = 100
|
|
125
|
+
target-version = "py310"
|
|
126
|
+
|
|
127
|
+
[tool.ruff.lint]
|
|
128
|
+
select = [
|
|
129
|
+
"E", # pycodestyle errors
|
|
130
|
+
"W", # pycodestyle warnings
|
|
131
|
+
"F", # pyflakes
|
|
132
|
+
"I", # isort
|
|
133
|
+
"B", # flake8-bugbear
|
|
134
|
+
"C4", # flake8-comprehensions
|
|
135
|
+
"UP", # pyupgrade
|
|
136
|
+
]
|
|
137
|
+
ignore = ["E501"]
|
|
138
|
+
|
|
139
|
+
[tool.ruff.lint.isort]
|
|
140
|
+
known-first-party = ["standin"]
|
|
141
|
+
|
|
142
|
+
[tool.pytest.ini_options]
|
|
143
|
+
asyncio_mode = "auto"
|
|
144
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
145
|
+
testpaths = ["tests"]
|
|
146
|
+
markers = [
|
|
147
|
+
"unit: fast tests with no network and no StandIn service (select with -m unit)",
|
|
148
|
+
"plugin(name): plugin test for the named framework plugin",
|
|
149
|
+
]
|
|
150
|
+
|
|
151
|
+
[tool.mypy]
|
|
152
|
+
strict = true
|
|
153
|
+
disallow_any_generics = false
|