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.
Files changed (111) hide show
  1. standin_sdk-0.1.0/.gitignore +46 -0
  2. standin_sdk-0.1.0/LICENSE +21 -0
  3. standin_sdk-0.1.0/PKG-INFO +223 -0
  4. standin_sdk-0.1.0/README.md +179 -0
  5. standin_sdk-0.1.0/pyproject.toml +153 -0
  6. standin_sdk-0.1.0/standin/__init__.py +491 -0
  7. standin_sdk-0.1.0/standin/_exceptions.py +34 -0
  8. standin_sdk-0.1.0/standin/_hmac.py +138 -0
  9. standin_sdk-0.1.0/standin/_protocol_runtime.py +80 -0
  10. standin_sdk-0.1.0/standin/ambient.py +324 -0
  11. standin_sdk-0.1.0/standin/attachments.py +550 -0
  12. standin_sdk-0.1.0/standin/audio.py +184 -0
  13. standin_sdk-0.1.0/standin/avatar.py +257 -0
  14. standin_sdk-0.1.0/standin/call_server.py +1145 -0
  15. standin_sdk-0.1.0/standin/calltools.py +366 -0
  16. standin_sdk-0.1.0/standin/chat.py +705 -0
  17. standin_sdk-0.1.0/standin/config.py +88 -0
  18. standin_sdk-0.1.0/standin/consult.py +434 -0
  19. standin_sdk-0.1.0/standin/delivery.py +251 -0
  20. standin_sdk-0.1.0/standin/echo_guard.py +121 -0
  21. standin_sdk-0.1.0/standin/fetch.py +234 -0
  22. standin_sdk-0.1.0/standin/gate.py +307 -0
  23. standin_sdk-0.1.0/standin/handler.py +365 -0
  24. standin_sdk-0.1.0/standin/lane.py +282 -0
  25. standin_sdk-0.1.0/standin/lipsync.py +344 -0
  26. standin_sdk-0.1.0/standin/log.py +6 -0
  27. standin_sdk-0.1.0/standin/media.py +207 -0
  28. standin_sdk-0.1.0/standin/minutes.py +891 -0
  29. standin_sdk-0.1.0/standin/outbound.py +1122 -0
  30. standin_sdk-0.1.0/standin/plugins/__init__.py +49 -0
  31. standin_sdk-0.1.0/standin/plugins/_lazy.py +118 -0
  32. standin_sdk-0.1.0/standin/plugins/cartesia/__init__.py +67 -0
  33. standin_sdk-0.1.0/standin/plugins/cartesia/__main__.py +9 -0
  34. standin_sdk-0.1.0/standin/plugins/cartesia/agent.py +317 -0
  35. standin_sdk-0.1.0/standin/plugins/cartesia/config.py +72 -0
  36. standin_sdk-0.1.0/standin/plugins/cartesia/handler.py +164 -0
  37. standin_sdk-0.1.0/standin/plugins/deepgram/__init__.py +97 -0
  38. standin_sdk-0.1.0/standin/plugins/deepgram/__main__.py +9 -0
  39. standin_sdk-0.1.0/standin/plugins/deepgram/agent.py +386 -0
  40. standin_sdk-0.1.0/standin/plugins/deepgram/config.py +119 -0
  41. standin_sdk-0.1.0/standin/plugins/deepgram/handler.py +338 -0
  42. standin_sdk-0.1.0/standin/plugins/echo/README.md +61 -0
  43. standin_sdk-0.1.0/standin/plugins/echo/__init__.py +75 -0
  44. standin_sdk-0.1.0/standin/plugins/echo/__main__.py +8 -0
  45. standin_sdk-0.1.0/standin/plugins/elevenlabs/__init__.py +85 -0
  46. standin_sdk-0.1.0/standin/plugins/elevenlabs/__main__.py +9 -0
  47. standin_sdk-0.1.0/standin/plugins/elevenlabs/agent.py +381 -0
  48. standin_sdk-0.1.0/standin/plugins/elevenlabs/config.py +87 -0
  49. standin_sdk-0.1.0/standin/plugins/elevenlabs/handler.py +494 -0
  50. standin_sdk-0.1.0/standin/plugins/hermes_agent/README.md +184 -0
  51. standin_sdk-0.1.0/standin/plugins/hermes_agent/__init__.py +143 -0
  52. standin_sdk-0.1.0/standin/plugins/hermes_agent/__main__.py +10 -0
  53. standin_sdk-0.1.0/standin/plugins/hermes_agent/api.py +318 -0
  54. standin_sdk-0.1.0/standin/plugins/hermes_agent/cli.py +74 -0
  55. standin_sdk-0.1.0/standin/plugins/hermes_agent/config.py +229 -0
  56. standin_sdk-0.1.0/standin/plugins/hermes_agent/consult.py +149 -0
  57. standin_sdk-0.1.0/standin/plugins/hermes_agent/handler.py +616 -0
  58. standin_sdk-0.1.0/standin/plugins/hermes_agent/log.py +6 -0
  59. standin_sdk-0.1.0/standin/plugins/hermes_agent/platform.py +247 -0
  60. standin_sdk-0.1.0/standin/plugins/hermes_agent/realtime.py +561 -0
  61. standin_sdk-0.1.0/standin/plugins/hermes_agent/service.py +120 -0
  62. standin_sdk-0.1.0/standin/plugins/hermes_agent/tools.py +126 -0
  63. standin_sdk-0.1.0/standin/plugins/livekit/README.md +115 -0
  64. standin_sdk-0.1.0/standin/plugins/livekit/__init__.py +106 -0
  65. standin_sdk-0.1.0/standin/plugins/livekit/call.py +267 -0
  66. standin_sdk-0.1.0/standin/plugins/livekit/handler.py +634 -0
  67. standin_sdk-0.1.0/standin/plugins/livekit/log.py +6 -0
  68. standin_sdk-0.1.0/standin/plugins/livekit/service.py +219 -0
  69. standin_sdk-0.1.0/standin/protocol.py +129 -0
  70. standin_sdk-0.1.0/standin/py.typed +0 -0
  71. standin_sdk-0.1.0/standin/render.py +168 -0
  72. standin_sdk-0.1.0/standin/smoke.py +327 -0
  73. standin_sdk-0.1.0/standin/startup.py +147 -0
  74. standin_sdk-0.1.0/standin/tile.py +251 -0
  75. standin_sdk-0.1.0/standin/version.py +4 -0
  76. standin_sdk-0.1.0/standin/vision.py +403 -0
  77. standin_sdk-0.1.0/standin/vision_tools.py +715 -0
  78. standin_sdk-0.1.0/standin/voice.py +436 -0
  79. standin_sdk-0.1.0/tests/test_ambient.py +277 -0
  80. standin_sdk-0.1.0/tests/test_attachments.py +556 -0
  81. standin_sdk-0.1.0/tests/test_call_server.py +1404 -0
  82. standin_sdk-0.1.0/tests/test_call_server_config.py +56 -0
  83. standin_sdk-0.1.0/tests/test_call_server_startup.py +68 -0
  84. standin_sdk-0.1.0/tests/test_calltools.py +270 -0
  85. standin_sdk-0.1.0/tests/test_chat.py +349 -0
  86. standin_sdk-0.1.0/tests/test_chat_secret.py +50 -0
  87. standin_sdk-0.1.0/tests/test_conformance.py +445 -0
  88. standin_sdk-0.1.0/tests/test_consult.py +312 -0
  89. standin_sdk-0.1.0/tests/test_delivery.py +203 -0
  90. standin_sdk-0.1.0/tests/test_hermes_agent_host_config.py +89 -0
  91. standin_sdk-0.1.0/tests/test_hermes_agent_realtime.py +110 -0
  92. standin_sdk-0.1.0/tests/test_hermes_platform.py +172 -0
  93. standin_sdk-0.1.0/tests/test_hmac_conformance.py +157 -0
  94. standin_sdk-0.1.0/tests/test_lane.py +291 -0
  95. standin_sdk-0.1.0/tests/test_lipsync.py +470 -0
  96. standin_sdk-0.1.0/tests/test_media.py +159 -0
  97. standin_sdk-0.1.0/tests/test_minutes.py +959 -0
  98. standin_sdk-0.1.0/tests/test_one_package.py +275 -0
  99. standin_sdk-0.1.0/tests/test_outbound.py +431 -0
  100. standin_sdk-0.1.0/tests/test_outbound_lane.py +461 -0
  101. standin_sdk-0.1.0/tests/test_plugin_cartesia.py +208 -0
  102. standin_sdk-0.1.0/tests/test_plugin_deepgram.py +378 -0
  103. standin_sdk-0.1.0/tests/test_plugin_echo.py +173 -0
  104. standin_sdk-0.1.0/tests/test_plugin_elevenlabs.py +494 -0
  105. standin_sdk-0.1.0/tests/test_plugin_hermes_agent.py +1240 -0
  106. standin_sdk-0.1.0/tests/test_plugin_livekit.py +357 -0
  107. standin_sdk-0.1.0/tests/test_plugin_livekit_runtime.py +156 -0
  108. standin_sdk-0.1.0/tests/test_smoke.py +76 -0
  109. standin_sdk-0.1.0/tests/test_startup_buffer.py +112 -0
  110. standin_sdk-0.1.0/tests/test_vision_tools.py +623 -0
  111. 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