smooai-smooth-operator-server 1.23.1__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.
- smooai_smooth_operator_server-1.23.1/.gitignore +41 -0
- smooai_smooth_operator_server-1.23.1/PKG-INFO +146 -0
- smooai_smooth_operator_server-1.23.1/README.md +134 -0
- smooai_smooth_operator_server-1.23.1/pyproject.toml +47 -0
- smooai_smooth_operator_server-1.23.1/src/smooth_operator_server/__init__.py +87 -0
- smooai_smooth_operator_server-1.23.1/src/smooth_operator_server/__main__.py +27 -0
- smooai_smooth_operator_server-1.23.1/src/smooth_operator_server/agent_config.py +343 -0
- smooai_smooth_operator_server-1.23.1/src/smooth_operator_server/auth.py +136 -0
- smooai_smooth_operator_server-1.23.1/src/smooth_operator_server/backplane.py +47 -0
- smooai_smooth_operator_server-1.23.1/src/smooth_operator_server/confirmation.py +74 -0
- smooai_smooth_operator_server-1.23.1/src/smooth_operator_server/dispatcher.py +490 -0
- smooai_smooth_operator_server-1.23.1/src/smooth_operator_server/extensions.py +173 -0
- smooai_smooth_operator_server-1.23.1/src/smooth_operator_server/model_info.py +122 -0
- smooai_smooth_operator_server-1.23.1/src/smooth_operator_server/otp.py +140 -0
- smooai_smooth_operator_server-1.23.1/src/smooth_operator_server/protocol.py +259 -0
- smooai_smooth_operator_server-1.23.1/src/smooth_operator_server/server.py +335 -0
- smooai_smooth_operator_server-1.23.1/src/smooth_operator_server/session_store.py +240 -0
- smooai_smooth_operator_server-1.23.1/src/smooth_operator_server/turn_runner.py +425 -0
- smooai_smooth_operator_server-1.23.1/src/smooth_operator_server/workflow.py +174 -0
- smooai_smooth_operator_server-1.23.1/tests/sep/echo_peer.py +72 -0
- smooai_smooth_operator_server-1.23.1/tests/test_agent_config.py +194 -0
- smooai_smooth_operator_server-1.23.1/tests/test_auth.py +90 -0
- smooai_smooth_operator_server-1.23.1/tests/test_confirm_tool_action.py +264 -0
- smooai_smooth_operator_server-1.23.1/tests/test_extension_host.py +201 -0
- smooai_smooth_operator_server-1.23.1/tests/test_graceful_drain.py +95 -0
- smooai_smooth_operator_server-1.23.1/tests/test_list_conversations_resume.py +172 -0
- smooai_smooth_operator_server-1.23.1/tests/test_model_info.py +141 -0
- smooai_smooth_operator_server-1.23.1/tests/test_otp_conformance.py +82 -0
- smooai_smooth_operator_server-1.23.1/tests/test_otp_flow.py +331 -0
- smooai_smooth_operator_server-1.23.1/tests/test_otp_seam.py +61 -0
- smooai_smooth_operator_server-1.23.1/tests/test_protocol_conformance.py +100 -0
- smooai_smooth_operator_server-1.23.1/tests/test_scenario_parity.py +164 -0
- smooai_smooth_operator_server-1.23.1/tests/test_server.py +157 -0
- smooai_smooth_operator_server-1.23.1/tests/test_session_auth.py +46 -0
- smooai_smooth_operator_server-1.23.1/tests/test_starvation_defaults.py +71 -0
- smooai_smooth_operator_server-1.23.1/tests/test_tool_auth_e2e.py +187 -0
- smooai_smooth_operator_server-1.23.1/tests/test_turn_runner_agent_config.py +259 -0
- smooai_smooth_operator_server-1.23.1/tests/test_turn_runner_citations.py +82 -0
- smooai_smooth_operator_server-1.23.1/tests/test_workflow.py +149 -0
- smooai_smooth_operator_server-1.23.1/uv.lock +563 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Rust
|
|
2
|
+
target/
|
|
3
|
+
**/*.rs.bk
|
|
4
|
+
# Cargo.lock IS committed: this workspace ships a binary (smooth-operator-server)
|
|
5
|
+
# and the Dockerfile builds with `cargo build --locked`, which requires it.
|
|
6
|
+
|
|
7
|
+
# Node / TypeScript
|
|
8
|
+
node_modules/
|
|
9
|
+
dist/
|
|
10
|
+
*.tsbuildinfo
|
|
11
|
+
.turbo/
|
|
12
|
+
|
|
13
|
+
# Go
|
|
14
|
+
/go/bin/
|
|
15
|
+
|
|
16
|
+
# .NET
|
|
17
|
+
bin/
|
|
18
|
+
obj/
|
|
19
|
+
|
|
20
|
+
# Python
|
|
21
|
+
__pycache__/
|
|
22
|
+
*.pyc
|
|
23
|
+
.venv/
|
|
24
|
+
.mypy_cache/
|
|
25
|
+
.ruff_cache/
|
|
26
|
+
|
|
27
|
+
# SST / deploy
|
|
28
|
+
.sst/
|
|
29
|
+
.open-next/
|
|
30
|
+
cdk.out/
|
|
31
|
+
|
|
32
|
+
# Env / secrets
|
|
33
|
+
.env
|
|
34
|
+
.env.local
|
|
35
|
+
*.pem
|
|
36
|
+
|
|
37
|
+
# OS / editor
|
|
38
|
+
.DS_Store
|
|
39
|
+
.idea/
|
|
40
|
+
.vscode/*
|
|
41
|
+
!.vscode/extensions.json
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: smooai-smooth-operator-server
|
|
3
|
+
Version: 1.23.1
|
|
4
|
+
Summary: Native async WebSocket server for the smooth-operator protocol — parity with the Rust and C# reference servers, consuming the in-process smooai-smooth-operator-core engine.
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Requires-Dist: smooai-smooth-operator-core>=1.3.2
|
|
8
|
+
Requires-Dist: websockets>=12
|
|
9
|
+
Provides-Extra: gateway
|
|
10
|
+
Requires-Dist: openai>=1.40; extra == 'gateway'
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# smooai-smooth-operator-server
|
|
14
|
+
|
|
15
|
+
<p>
|
|
16
|
+
<a href="https://smoo.ai"><img src="https://img.shields.io/badge/Smoo_AI-platform-00A6A6?style=for-the-badge&labelColor=020618" alt="Smoo AI"></a>
|
|
17
|
+
<a href="https://github.com/SmooAI/smooth-operator/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-FF6B6C?style=for-the-badge&labelColor=020618" alt="license"></a>
|
|
18
|
+
<a href="https://www.python.org"><img src="https://img.shields.io/badge/Python-%E2%89%A53.11-00A6A6?style=for-the-badge&labelColor=020618" alt="Python ≥ 3.11"></a>
|
|
19
|
+
</p>
|
|
20
|
+
|
|
21
|
+
**Wiring a chat loop is a weekend project. A production agent _server_ is not.**
|
|
22
|
+
|
|
23
|
+
Sessions that survive a reconnect. A wire protocol your clients can actually speak. Streaming turns you can watch token by token. Tools the model can call — and hard limits on the ones it must never call. Human-in-the-loop when a tool wants to write.
|
|
24
|
+
|
|
25
|
+
`smooai-smooth-operator-server` is that server, async and native to Python. It speaks the [smooth-operator](https://github.com/SmooAI/smooth-operator) wire protocol ([`spec/`](https://github.com/SmooAI/smooth-operator/tree/main/spec)) and consumes the in-process [`smooai-smooth-operator-core`](https://pypi.org/project/smooai-smooth-operator-core/) engine — each turn runs a `SmoothAgent` and maps its stream onto `stream_token` / `stream_chunk` / `eventual_response`. It's the Python sibling of the [Rust](../../rust/smooth-operator-server), [Go](../../go/server), [TypeScript](../../typescript/server), and [C#](../../dotnet/server) servers, all speaking the one protocol.
|
|
26
|
+
|
|
27
|
+
> The client lives in [`python/src`](../src) (`smooai-smooth-operator`). This is the **server** half.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Spin up a real agent server
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
python -m smooth_operator_server
|
|
35
|
+
# → smooth-operator-server (local flavor, python) listening on ws://127.0.0.1:8787/ws
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
That's a full agent backend — sessions, streaming turns, tool-calling, citations — on one WebSocket, in-memory, auth off, zero config. Env knobs: `SMOOTH_OPERATOR_BIND` (default `127.0.0.1:8787`), `SMOOTH_OPERATOR_SEED_KB=1` for the demo knowledge docs. The gateway is read from `SMOOAI_GATEWAY_URL` / `SMOOAI_GATEWAY_KEY` — with no key, `send_message` returns a clean `LLM_UNAVAILABLE` error and the rest of the protocol still works.
|
|
39
|
+
|
|
40
|
+
Or embed it in your own async app:
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
import asyncio
|
|
44
|
+
from smooth_operator_server import ServerState, serve
|
|
45
|
+
from smooth_operator_server.session_store import InMemorySessionStore
|
|
46
|
+
|
|
47
|
+
async def main():
|
|
48
|
+
state = ServerState(store=InMemorySessionStore(), chat_client=my_openai_client)
|
|
49
|
+
server = await serve(state, "127.0.0.1", 0) # port 0 → ephemeral
|
|
50
|
+
print(server.ws_url())
|
|
51
|
+
# ... drive real streaming turns ...
|
|
52
|
+
await server.shutdown() # graceful drain + clean exit
|
|
53
|
+
|
|
54
|
+
asyncio.run(main())
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Extensible — and safe by construction
|
|
60
|
+
|
|
61
|
+
An agent is only useful when it can *do* things, and only trustworthy when you can say what it may never do. This server gives you both seams.
|
|
62
|
+
|
|
63
|
+
**Give it your tools.** Hand engine `Tool`s to `ServerState` and they merge with the built-ins for every turn:
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from smooth_operator_core import Tool # the engine's tool base
|
|
67
|
+
|
|
68
|
+
class OpenTicket(Tool):
|
|
69
|
+
name = "open_ticket"
|
|
70
|
+
description = "Open a support ticket for the current customer."
|
|
71
|
+
parameters = {"type": "object", "properties": {"subject": {"type": "string"}}}
|
|
72
|
+
|
|
73
|
+
async def execute(self, arguments):
|
|
74
|
+
return f"ticket opened: {arguments}"
|
|
75
|
+
|
|
76
|
+
state = ServerState(
|
|
77
|
+
store=InMemorySessionStore(),
|
|
78
|
+
chat_client=my_client,
|
|
79
|
+
tools=[OpenTicket()],
|
|
80
|
+
)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**Or let it gain tools with no redeploy.** The server hosts [SEP extensions](https://github.com/SmooAI/smooth-operator/blob/main/docs/TOOLS.md) — out-of-process tool providers discovered at runtime, their `ui/confirm` prompts bridged into the protocol's confirmation frames for HITL. Gated: an extension contributes tools **only** if you name it in `SMOOTH_EXTENSIONS_ALLOW`. Nothing loads by default.
|
|
84
|
+
|
|
85
|
+
**Now declare the lines it can't cross.** Install an `AgentConfigResolver`, and every tool — built-in, yours, or from an extension — flows through the same gates:
|
|
86
|
+
|
|
87
|
+
- **Per-agent allow-list** — an agent's `tool_config.enabledTools` restricts its turn to exactly those tools. Off the list, off the table.
|
|
88
|
+
- **The authLevel gate** — a tool declaring `supports_auth_requirement = True` is *blocked at call time* on a public agent when tagged `admin`, or when tagged `end_user` and the session isn't identity-verified (its OTP bit, or your `SessionAuthenticator` seam). **Fail-closed** — an absent authenticator means "not authenticated".
|
|
89
|
+
- **End-user OTP flow** — a refused `end_user` tool can offer a one-time-code identity flow via the `OtpService` seam; the server never generates, delivers, or validates a code (the host owns generation, delivery, expiry, attempt counting).
|
|
90
|
+
|
|
91
|
+
You decide what the agent can touch; the runner enforces it.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## What it does
|
|
96
|
+
|
|
97
|
+
| Piece | Module | Mirrors |
|
|
98
|
+
| --- | --- | --- |
|
|
99
|
+
| WS transport + per-connection loop + single writer | `server.py` | Rust `server.rs`, C# `SmoothOperatorWebSocketExtensions` |
|
|
100
|
+
| Frame dispatch (`ping` / `create` / `get` / `send_message`) | `dispatcher.py` | C# `FrameDispatcher`, Rust `handler.rs` |
|
|
101
|
+
| Session + message store | `session_store.py` | C# `SessionStore`, Rust storage adapter |
|
|
102
|
+
| Streaming turn (engine → protocol events) | `turn_runner.py` | C# `TurnRunner`, Rust `runner.rs` |
|
|
103
|
+
| Per-agent config (instructions / workflow / persona / tools) | `agent_config.py` | monorepo `agents` schema |
|
|
104
|
+
| Conversation-workflow steps + post-turn judge | `workflow.py` | monorepo `general-agent/workflow.ts` |
|
|
105
|
+
| SEP extension hosting | `extensions.py` | Rust `extensions.rs` |
|
|
106
|
+
| Auth verifier seam (permissive + local HS256 JWT) | `auth.py` | C# `Auth.cs`, Rust verifier seam |
|
|
107
|
+
|
|
108
|
+
**Graceful SIGTERM drain.** A shared `asyncio.Event` cancel switch is the single source of truth for "stop". Each connection loop races "cancel set" vs "next inbound frame" — with the turn dispatch awaited *inside* the frame branch, so an in-flight turn finishes before the loop exits, then a backplane `detach` always runs.
|
|
109
|
+
|
|
110
|
+
**Per-agent config + conversation workflows.** `create_conversation_session` carries only an agent UUID, so config is resolved server-side per turn: an agent's `instructions` become its system prompt; `personality` / `greeting` are appended (greeting only on the first turn); a `conversation_workflow` (goal + ordered steps) renders the current step into the prompt and a cheap post-turn judge advances the pointer when the criteria are met. Parsing is tolerant (malformed → server default, never crashes a session) and the judge is failure-tolerant (any error → stay on the current step). With no resolver installed, behavior is unchanged.
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## Five languages, one protocol
|
|
115
|
+
|
|
116
|
+
The *same* server — same wire protocol, same conformance corpus — exists in five languages. Run it where your stack already lives.
|
|
117
|
+
|
|
118
|
+
| Language | Server package | Registry |
|
|
119
|
+
| --- | --- | --- |
|
|
120
|
+
| **Python** | `smooai-smooth-operator-server` | in-repo (this package) |
|
|
121
|
+
| **Rust** | `smooai-smooth-operator-server` | [crates.io](https://crates.io/crates/smooai-smooth-operator-server) |
|
|
122
|
+
| **C# / .NET** | `SmooAI.SmoothOperator.Server` | [in-repo](../../dotnet/server) |
|
|
123
|
+
| **TypeScript** | `@smooai/smooth-operator-server` | [in-repo](../../typescript/server) |
|
|
124
|
+
| **Go** | `github.com/SmooAI/smooth-operator/go/server` | [in-repo](../../go/server) |
|
|
125
|
+
|
|
126
|
+
Every native client — [TypeScript](https://www.npmjs.com/package/@smooai/smooth-operator), Go, .NET, Python, Rust — connects to any of them unmodified.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Develop
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
cd python/server
|
|
134
|
+
uv sync
|
|
135
|
+
uv run --quiet ruff format .
|
|
136
|
+
uv run --quiet ruff check .
|
|
137
|
+
uv run --quiet pytest -q
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
Part of the **[smooth-operator](https://github.com/SmooAI/smooth-operator)** service — Smoo AI's polyglot AI agent service. Don't want to run it yourself? **[lom.smoo.ai](https://lom.smoo.ai)** hosts it for you.
|
|
143
|
+
|
|
144
|
+
## License
|
|
145
|
+
|
|
146
|
+
MIT © 2026 Smoo AI. See [LICENSE](https://github.com/SmooAI/smooth-operator/blob/main/LICENSE).
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# smooai-smooth-operator-server
|
|
2
|
+
|
|
3
|
+
<p>
|
|
4
|
+
<a href="https://smoo.ai"><img src="https://img.shields.io/badge/Smoo_AI-platform-00A6A6?style=for-the-badge&labelColor=020618" alt="Smoo AI"></a>
|
|
5
|
+
<a href="https://github.com/SmooAI/smooth-operator/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-FF6B6C?style=for-the-badge&labelColor=020618" alt="license"></a>
|
|
6
|
+
<a href="https://www.python.org"><img src="https://img.shields.io/badge/Python-%E2%89%A53.11-00A6A6?style=for-the-badge&labelColor=020618" alt="Python ≥ 3.11"></a>
|
|
7
|
+
</p>
|
|
8
|
+
|
|
9
|
+
**Wiring a chat loop is a weekend project. A production agent _server_ is not.**
|
|
10
|
+
|
|
11
|
+
Sessions that survive a reconnect. A wire protocol your clients can actually speak. Streaming turns you can watch token by token. Tools the model can call — and hard limits on the ones it must never call. Human-in-the-loop when a tool wants to write.
|
|
12
|
+
|
|
13
|
+
`smooai-smooth-operator-server` is that server, async and native to Python. It speaks the [smooth-operator](https://github.com/SmooAI/smooth-operator) wire protocol ([`spec/`](https://github.com/SmooAI/smooth-operator/tree/main/spec)) and consumes the in-process [`smooai-smooth-operator-core`](https://pypi.org/project/smooai-smooth-operator-core/) engine — each turn runs a `SmoothAgent` and maps its stream onto `stream_token` / `stream_chunk` / `eventual_response`. It's the Python sibling of the [Rust](../../rust/smooth-operator-server), [Go](../../go/server), [TypeScript](../../typescript/server), and [C#](../../dotnet/server) servers, all speaking the one protocol.
|
|
14
|
+
|
|
15
|
+
> The client lives in [`python/src`](../src) (`smooai-smooth-operator`). This is the **server** half.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Spin up a real agent server
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
python -m smooth_operator_server
|
|
23
|
+
# → smooth-operator-server (local flavor, python) listening on ws://127.0.0.1:8787/ws
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
That's a full agent backend — sessions, streaming turns, tool-calling, citations — on one WebSocket, in-memory, auth off, zero config. Env knobs: `SMOOTH_OPERATOR_BIND` (default `127.0.0.1:8787`), `SMOOTH_OPERATOR_SEED_KB=1` for the demo knowledge docs. The gateway is read from `SMOOAI_GATEWAY_URL` / `SMOOAI_GATEWAY_KEY` — with no key, `send_message` returns a clean `LLM_UNAVAILABLE` error and the rest of the protocol still works.
|
|
27
|
+
|
|
28
|
+
Or embed it in your own async app:
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
import asyncio
|
|
32
|
+
from smooth_operator_server import ServerState, serve
|
|
33
|
+
from smooth_operator_server.session_store import InMemorySessionStore
|
|
34
|
+
|
|
35
|
+
async def main():
|
|
36
|
+
state = ServerState(store=InMemorySessionStore(), chat_client=my_openai_client)
|
|
37
|
+
server = await serve(state, "127.0.0.1", 0) # port 0 → ephemeral
|
|
38
|
+
print(server.ws_url())
|
|
39
|
+
# ... drive real streaming turns ...
|
|
40
|
+
await server.shutdown() # graceful drain + clean exit
|
|
41
|
+
|
|
42
|
+
asyncio.run(main())
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Extensible — and safe by construction
|
|
48
|
+
|
|
49
|
+
An agent is only useful when it can *do* things, and only trustworthy when you can say what it may never do. This server gives you both seams.
|
|
50
|
+
|
|
51
|
+
**Give it your tools.** Hand engine `Tool`s to `ServerState` and they merge with the built-ins for every turn:
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from smooth_operator_core import Tool # the engine's tool base
|
|
55
|
+
|
|
56
|
+
class OpenTicket(Tool):
|
|
57
|
+
name = "open_ticket"
|
|
58
|
+
description = "Open a support ticket for the current customer."
|
|
59
|
+
parameters = {"type": "object", "properties": {"subject": {"type": "string"}}}
|
|
60
|
+
|
|
61
|
+
async def execute(self, arguments):
|
|
62
|
+
return f"ticket opened: {arguments}"
|
|
63
|
+
|
|
64
|
+
state = ServerState(
|
|
65
|
+
store=InMemorySessionStore(),
|
|
66
|
+
chat_client=my_client,
|
|
67
|
+
tools=[OpenTicket()],
|
|
68
|
+
)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**Or let it gain tools with no redeploy.** The server hosts [SEP extensions](https://github.com/SmooAI/smooth-operator/blob/main/docs/TOOLS.md) — out-of-process tool providers discovered at runtime, their `ui/confirm` prompts bridged into the protocol's confirmation frames for HITL. Gated: an extension contributes tools **only** if you name it in `SMOOTH_EXTENSIONS_ALLOW`. Nothing loads by default.
|
|
72
|
+
|
|
73
|
+
**Now declare the lines it can't cross.** Install an `AgentConfigResolver`, and every tool — built-in, yours, or from an extension — flows through the same gates:
|
|
74
|
+
|
|
75
|
+
- **Per-agent allow-list** — an agent's `tool_config.enabledTools` restricts its turn to exactly those tools. Off the list, off the table.
|
|
76
|
+
- **The authLevel gate** — a tool declaring `supports_auth_requirement = True` is *blocked at call time* on a public agent when tagged `admin`, or when tagged `end_user` and the session isn't identity-verified (its OTP bit, or your `SessionAuthenticator` seam). **Fail-closed** — an absent authenticator means "not authenticated".
|
|
77
|
+
- **End-user OTP flow** — a refused `end_user` tool can offer a one-time-code identity flow via the `OtpService` seam; the server never generates, delivers, or validates a code (the host owns generation, delivery, expiry, attempt counting).
|
|
78
|
+
|
|
79
|
+
You decide what the agent can touch; the runner enforces it.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## What it does
|
|
84
|
+
|
|
85
|
+
| Piece | Module | Mirrors |
|
|
86
|
+
| --- | --- | --- |
|
|
87
|
+
| WS transport + per-connection loop + single writer | `server.py` | Rust `server.rs`, C# `SmoothOperatorWebSocketExtensions` |
|
|
88
|
+
| Frame dispatch (`ping` / `create` / `get` / `send_message`) | `dispatcher.py` | C# `FrameDispatcher`, Rust `handler.rs` |
|
|
89
|
+
| Session + message store | `session_store.py` | C# `SessionStore`, Rust storage adapter |
|
|
90
|
+
| Streaming turn (engine → protocol events) | `turn_runner.py` | C# `TurnRunner`, Rust `runner.rs` |
|
|
91
|
+
| Per-agent config (instructions / workflow / persona / tools) | `agent_config.py` | monorepo `agents` schema |
|
|
92
|
+
| Conversation-workflow steps + post-turn judge | `workflow.py` | monorepo `general-agent/workflow.ts` |
|
|
93
|
+
| SEP extension hosting | `extensions.py` | Rust `extensions.rs` |
|
|
94
|
+
| Auth verifier seam (permissive + local HS256 JWT) | `auth.py` | C# `Auth.cs`, Rust verifier seam |
|
|
95
|
+
|
|
96
|
+
**Graceful SIGTERM drain.** A shared `asyncio.Event` cancel switch is the single source of truth for "stop". Each connection loop races "cancel set" vs "next inbound frame" — with the turn dispatch awaited *inside* the frame branch, so an in-flight turn finishes before the loop exits, then a backplane `detach` always runs.
|
|
97
|
+
|
|
98
|
+
**Per-agent config + conversation workflows.** `create_conversation_session` carries only an agent UUID, so config is resolved server-side per turn: an agent's `instructions` become its system prompt; `personality` / `greeting` are appended (greeting only on the first turn); a `conversation_workflow` (goal + ordered steps) renders the current step into the prompt and a cheap post-turn judge advances the pointer when the criteria are met. Parsing is tolerant (malformed → server default, never crashes a session) and the judge is failure-tolerant (any error → stay on the current step). With no resolver installed, behavior is unchanged.
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Five languages, one protocol
|
|
103
|
+
|
|
104
|
+
The *same* server — same wire protocol, same conformance corpus — exists in five languages. Run it where your stack already lives.
|
|
105
|
+
|
|
106
|
+
| Language | Server package | Registry |
|
|
107
|
+
| --- | --- | --- |
|
|
108
|
+
| **Python** | `smooai-smooth-operator-server` | in-repo (this package) |
|
|
109
|
+
| **Rust** | `smooai-smooth-operator-server` | [crates.io](https://crates.io/crates/smooai-smooth-operator-server) |
|
|
110
|
+
| **C# / .NET** | `SmooAI.SmoothOperator.Server` | [in-repo](../../dotnet/server) |
|
|
111
|
+
| **TypeScript** | `@smooai/smooth-operator-server` | [in-repo](../../typescript/server) |
|
|
112
|
+
| **Go** | `github.com/SmooAI/smooth-operator/go/server` | [in-repo](../../go/server) |
|
|
113
|
+
|
|
114
|
+
Every native client — [TypeScript](https://www.npmjs.com/package/@smooai/smooth-operator), Go, .NET, Python, Rust — connects to any of them unmodified.
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Develop
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
cd python/server
|
|
122
|
+
uv sync
|
|
123
|
+
uv run --quiet ruff format .
|
|
124
|
+
uv run --quiet ruff check .
|
|
125
|
+
uv run --quiet pytest -q
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
Part of the **[smooth-operator](https://github.com/SmooAI/smooth-operator)** service — Smoo AI's polyglot AI agent service. Don't want to run it yourself? **[lom.smoo.ai](https://lom.smoo.ai)** hosts it for you.
|
|
131
|
+
|
|
132
|
+
## License
|
|
133
|
+
|
|
134
|
+
MIT © 2026 Smoo AI. See [LICENSE](https://github.com/SmooAI/smooth-operator/blob/main/LICENSE).
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "smooai-smooth-operator-server"
|
|
3
|
+
version = "1.23.1"
|
|
4
|
+
description = "Native async WebSocket server for the smooth-operator protocol — parity with the Rust and C# reference servers, consuming the in-process smooai-smooth-operator-core engine."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = { text = "MIT" }
|
|
7
|
+
requires-python = ">=3.11"
|
|
8
|
+
dependencies = [
|
|
9
|
+
# The engine the server runs in-process per turn. `SmoothAgent.run_stream(...)`
|
|
10
|
+
# is mapped onto the protocol's stream_token / stream_chunk / eventual_response.
|
|
11
|
+
"smooai-smooth-operator-core>=1.3.2",
|
|
12
|
+
# asyncio WebSocket transport (server side).
|
|
13
|
+
"websockets>=12",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[project.optional-dependencies]
|
|
17
|
+
# A live (non-mock) turn needs an OpenAI-compatible async client against the
|
|
18
|
+
# SmooAI gateway. Optional: the server is fully testable on MockLlmProvider with
|
|
19
|
+
# no gateway, and protocol-only paths (create/get/ping) need no LLM at all.
|
|
20
|
+
gateway = ["openai>=1.40"]
|
|
21
|
+
|
|
22
|
+
[dependency-groups]
|
|
23
|
+
dev = [
|
|
24
|
+
"pytest>=8",
|
|
25
|
+
"pytest-asyncio>=0.23",
|
|
26
|
+
"ruff>=0.4",
|
|
27
|
+
# The turn round-trip test drives the engine on MockLlmProvider, which ships
|
|
28
|
+
# in smooai-smooth-operator-core — no gateway needed.
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[build-system]
|
|
32
|
+
requires = ["hatchling"]
|
|
33
|
+
build-backend = "hatchling.build"
|
|
34
|
+
|
|
35
|
+
[tool.hatch.build.targets.wheel]
|
|
36
|
+
packages = ["src/smooth_operator_server"]
|
|
37
|
+
|
|
38
|
+
[tool.pytest.ini_options]
|
|
39
|
+
asyncio_mode = "auto"
|
|
40
|
+
testpaths = ["tests"]
|
|
41
|
+
|
|
42
|
+
[tool.ruff]
|
|
43
|
+
line-length = 120
|
|
44
|
+
target-version = "py311"
|
|
45
|
+
|
|
46
|
+
[tool.ruff.lint]
|
|
47
|
+
extend-select = ["I"]
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Native async WebSocket server for the smooth-operator protocol.
|
|
2
|
+
|
|
3
|
+
A parity implementation of the Rust (``rust/smooth-operator-server``) and C#
|
|
4
|
+
(``dotnet/server``) reference servers, consuming the in-process
|
|
5
|
+
``smooai-smooth-operator-core`` engine. The server runs a :class:`SmoothAgent` per
|
|
6
|
+
turn and maps its stream events onto the wire protocol's ``stream_token`` /
|
|
7
|
+
``stream_chunk`` / ``eventual_response`` events.
|
|
8
|
+
|
|
9
|
+
Quick start (embeddable, in-memory, auth off)::
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
from smooth_operator_server import serve_local
|
|
13
|
+
|
|
14
|
+
asyncio.run(serve_local("127.0.0.1:8787", seed_kb=True))
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from . import protocol
|
|
20
|
+
from .auth import (
|
|
21
|
+
AccessContext,
|
|
22
|
+
AuthVerifier,
|
|
23
|
+
LocalTokenVerifier,
|
|
24
|
+
NoAuthVerifier,
|
|
25
|
+
Principal,
|
|
26
|
+
)
|
|
27
|
+
from .backplane import Backplane, InMemoryBackplane
|
|
28
|
+
from .dispatcher import FrameDispatcher
|
|
29
|
+
from .otp import (
|
|
30
|
+
OtpChannel,
|
|
31
|
+
OtpContact,
|
|
32
|
+
OtpDelivery,
|
|
33
|
+
OtpError,
|
|
34
|
+
OtpInvalid,
|
|
35
|
+
OtpService,
|
|
36
|
+
OtpVerified,
|
|
37
|
+
OtpVerifyOutcome,
|
|
38
|
+
)
|
|
39
|
+
from .server import (
|
|
40
|
+
DEFAULT_HOST,
|
|
41
|
+
DEFAULT_PORT,
|
|
42
|
+
Server,
|
|
43
|
+
ServerState,
|
|
44
|
+
serve,
|
|
45
|
+
serve_local,
|
|
46
|
+
)
|
|
47
|
+
from .session_store import (
|
|
48
|
+
InMemorySessionStore,
|
|
49
|
+
MessageDirection,
|
|
50
|
+
SessionStore,
|
|
51
|
+
StoredMessage,
|
|
52
|
+
StoredSession,
|
|
53
|
+
)
|
|
54
|
+
from .turn_runner import TurnResult, TurnRunner
|
|
55
|
+
|
|
56
|
+
__all__ = [
|
|
57
|
+
"protocol",
|
|
58
|
+
"AccessContext",
|
|
59
|
+
"AuthVerifier",
|
|
60
|
+
"LocalTokenVerifier",
|
|
61
|
+
"NoAuthVerifier",
|
|
62
|
+
"Principal",
|
|
63
|
+
"Backplane",
|
|
64
|
+
"InMemoryBackplane",
|
|
65
|
+
"FrameDispatcher",
|
|
66
|
+
"OtpChannel",
|
|
67
|
+
"OtpContact",
|
|
68
|
+
"OtpDelivery",
|
|
69
|
+
"OtpError",
|
|
70
|
+
"OtpInvalid",
|
|
71
|
+
"OtpService",
|
|
72
|
+
"OtpVerified",
|
|
73
|
+
"OtpVerifyOutcome",
|
|
74
|
+
"DEFAULT_HOST",
|
|
75
|
+
"DEFAULT_PORT",
|
|
76
|
+
"Server",
|
|
77
|
+
"ServerState",
|
|
78
|
+
"serve",
|
|
79
|
+
"serve_local",
|
|
80
|
+
"InMemorySessionStore",
|
|
81
|
+
"MessageDirection",
|
|
82
|
+
"SessionStore",
|
|
83
|
+
"StoredMessage",
|
|
84
|
+
"StoredSession",
|
|
85
|
+
"TurnResult",
|
|
86
|
+
"TurnRunner",
|
|
87
|
+
]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Run the local-flavor server: ``python -m smooth_operator_server``.
|
|
2
|
+
|
|
3
|
+
Boots a fully in-memory, auth-off server on ``SMOOTH_OPERATOR_BIND`` (default
|
|
4
|
+
``127.0.0.1:8787``) and serves until killed. ``SMOOTH_OPERATOR_SEED_KB=1`` loads the
|
|
5
|
+
demo knowledge docs. The LLM gateway is read from ``SMOOAI_GATEWAY_URL`` /
|
|
6
|
+
``SMOOAI_GATEWAY_KEY``; absent, ``send_message`` errors cleanly.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import os
|
|
13
|
+
|
|
14
|
+
from .server import DEFAULT_HOST, DEFAULT_PORT, serve_local
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def main() -> None:
|
|
18
|
+
addr = os.environ.get("SMOOTH_OPERATOR_BIND", f"{DEFAULT_HOST}:{DEFAULT_PORT}")
|
|
19
|
+
seed_kb = os.environ.get("SMOOTH_OPERATOR_SEED_KB", "") not in ("", "0", "false", "False")
|
|
20
|
+
try:
|
|
21
|
+
asyncio.run(serve_local(addr, seed_kb=seed_kb))
|
|
22
|
+
except KeyboardInterrupt:
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
if __name__ == "__main__":
|
|
27
|
+
main()
|