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.
Files changed (40) hide show
  1. smooai_smooth_operator_server-1.23.1/.gitignore +41 -0
  2. smooai_smooth_operator_server-1.23.1/PKG-INFO +146 -0
  3. smooai_smooth_operator_server-1.23.1/README.md +134 -0
  4. smooai_smooth_operator_server-1.23.1/pyproject.toml +47 -0
  5. smooai_smooth_operator_server-1.23.1/src/smooth_operator_server/__init__.py +87 -0
  6. smooai_smooth_operator_server-1.23.1/src/smooth_operator_server/__main__.py +27 -0
  7. smooai_smooth_operator_server-1.23.1/src/smooth_operator_server/agent_config.py +343 -0
  8. smooai_smooth_operator_server-1.23.1/src/smooth_operator_server/auth.py +136 -0
  9. smooai_smooth_operator_server-1.23.1/src/smooth_operator_server/backplane.py +47 -0
  10. smooai_smooth_operator_server-1.23.1/src/smooth_operator_server/confirmation.py +74 -0
  11. smooai_smooth_operator_server-1.23.1/src/smooth_operator_server/dispatcher.py +490 -0
  12. smooai_smooth_operator_server-1.23.1/src/smooth_operator_server/extensions.py +173 -0
  13. smooai_smooth_operator_server-1.23.1/src/smooth_operator_server/model_info.py +122 -0
  14. smooai_smooth_operator_server-1.23.1/src/smooth_operator_server/otp.py +140 -0
  15. smooai_smooth_operator_server-1.23.1/src/smooth_operator_server/protocol.py +259 -0
  16. smooai_smooth_operator_server-1.23.1/src/smooth_operator_server/server.py +335 -0
  17. smooai_smooth_operator_server-1.23.1/src/smooth_operator_server/session_store.py +240 -0
  18. smooai_smooth_operator_server-1.23.1/src/smooth_operator_server/turn_runner.py +425 -0
  19. smooai_smooth_operator_server-1.23.1/src/smooth_operator_server/workflow.py +174 -0
  20. smooai_smooth_operator_server-1.23.1/tests/sep/echo_peer.py +72 -0
  21. smooai_smooth_operator_server-1.23.1/tests/test_agent_config.py +194 -0
  22. smooai_smooth_operator_server-1.23.1/tests/test_auth.py +90 -0
  23. smooai_smooth_operator_server-1.23.1/tests/test_confirm_tool_action.py +264 -0
  24. smooai_smooth_operator_server-1.23.1/tests/test_extension_host.py +201 -0
  25. smooai_smooth_operator_server-1.23.1/tests/test_graceful_drain.py +95 -0
  26. smooai_smooth_operator_server-1.23.1/tests/test_list_conversations_resume.py +172 -0
  27. smooai_smooth_operator_server-1.23.1/tests/test_model_info.py +141 -0
  28. smooai_smooth_operator_server-1.23.1/tests/test_otp_conformance.py +82 -0
  29. smooai_smooth_operator_server-1.23.1/tests/test_otp_flow.py +331 -0
  30. smooai_smooth_operator_server-1.23.1/tests/test_otp_seam.py +61 -0
  31. smooai_smooth_operator_server-1.23.1/tests/test_protocol_conformance.py +100 -0
  32. smooai_smooth_operator_server-1.23.1/tests/test_scenario_parity.py +164 -0
  33. smooai_smooth_operator_server-1.23.1/tests/test_server.py +157 -0
  34. smooai_smooth_operator_server-1.23.1/tests/test_session_auth.py +46 -0
  35. smooai_smooth_operator_server-1.23.1/tests/test_starvation_defaults.py +71 -0
  36. smooai_smooth_operator_server-1.23.1/tests/test_tool_auth_e2e.py +187 -0
  37. smooai_smooth_operator_server-1.23.1/tests/test_turn_runner_agent_config.py +259 -0
  38. smooai_smooth_operator_server-1.23.1/tests/test_turn_runner_citations.py +82 -0
  39. smooai_smooth_operator_server-1.23.1/tests/test_workflow.py +149 -0
  40. 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()