smooai-smooth-operator 1.2.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.
- smooai_smooth_operator-1.2.0/.gitignore +41 -0
- smooai_smooth_operator-1.2.0/.gitkeep +0 -0
- smooai_smooth_operator-1.2.0/PKG-INFO +164 -0
- smooai_smooth_operator-1.2.0/README.md +152 -0
- smooai_smooth_operator-1.2.0/core/README.md +1 -0
- smooai_smooth_operator-1.2.0/core/pyproject.toml +37 -0
- smooai_smooth_operator-1.2.0/core/src/smooth_operator_core/__init__.py +98 -0
- smooai_smooth_operator-1.2.0/core/src/smooth_operator_core/agent.py +673 -0
- smooai_smooth_operator-1.2.0/core/src/smooth_operator_core/cast.py +124 -0
- smooai_smooth_operator-1.2.0/core/src/smooth_operator_core/checkpoint.py +46 -0
- smooai_smooth_operator-1.2.0/core/src/smooth_operator_core/compaction.py +66 -0
- smooai_smooth_operator-1.2.0/core/src/smooth_operator_core/cost.py +80 -0
- smooai_smooth_operator-1.2.0/core/src/smooth_operator_core/human_gate.py +74 -0
- smooai_smooth_operator-1.2.0/core/src/smooth_operator_core/knowledge.py +75 -0
- smooai_smooth_operator-1.2.0/core/src/smooth_operator_core/llm_provider.py +232 -0
- smooai_smooth_operator-1.2.0/core/src/smooth_operator_core/memory.py +56 -0
- smooai_smooth_operator-1.2.0/core/src/smooth_operator_core/rerank.py +61 -0
- smooai_smooth_operator-1.2.0/core/src/smooth_operator_core/thread.py +55 -0
- smooai_smooth_operator-1.2.0/core/src/smooth_operator_core/tool_search.py +133 -0
- smooai_smooth_operator-1.2.0/core/src/smooth_operator_core/vector.py +92 -0
- smooai_smooth_operator-1.2.0/core/src/smooth_operator_core/workflow.py +122 -0
- smooai_smooth_operator-1.2.0/core/tests/test_agent.py +75 -0
- smooai_smooth_operator-1.2.0/core/tests/test_cast.py +168 -0
- smooai_smooth_operator-1.2.0/core/tests/test_checkpoint.py +76 -0
- smooai_smooth_operator-1.2.0/core/tests/test_compaction.py +60 -0
- smooai_smooth_operator-1.2.0/core/tests/test_cost.py +80 -0
- smooai_smooth_operator-1.2.0/core/tests/test_evals.py +170 -0
- smooai_smooth_operator-1.2.0/core/tests/test_human_gate.py +202 -0
- smooai_smooth_operator-1.2.0/core/tests/test_llm_provider.py +123 -0
- smooai_smooth_operator-1.2.0/core/tests/test_memory.py +67 -0
- smooai_smooth_operator-1.2.0/core/tests/test_parallel_tools.py +150 -0
- smooai_smooth_operator-1.2.0/core/tests/test_rerank.py +96 -0
- smooai_smooth_operator-1.2.0/core/tests/test_retry.py +45 -0
- smooai_smooth_operator-1.2.0/core/tests/test_stream.py +126 -0
- smooai_smooth_operator-1.2.0/core/tests/test_subagent.py +66 -0
- smooai_smooth_operator-1.2.0/core/tests/test_thread.py +133 -0
- smooai_smooth_operator-1.2.0/core/tests/test_tool_search.py +162 -0
- smooai_smooth_operator-1.2.0/core/tests/test_vector.py +66 -0
- smooai_smooth_operator-1.2.0/core/tests/test_workflow.py +126 -0
- smooai_smooth_operator-1.2.0/core/uv.lock +481 -0
- smooai_smooth_operator-1.2.0/pyproject.toml +52 -0
- smooai_smooth_operator-1.2.0/scripts/generate.py +215 -0
- smooai_smooth_operator-1.2.0/src/smooth_operator/__init__.py +134 -0
- smooai_smooth_operator-1.2.0/src/smooth_operator/_generated.py +1644 -0
- smooai_smooth_operator-1.2.0/src/smooth_operator/client.py +470 -0
- smooai_smooth_operator-1.2.0/src/smooth_operator/transport.py +152 -0
- smooai_smooth_operator-1.2.0/src/smooth_operator/types.py +230 -0
- smooai_smooth_operator-1.2.0/src/smooth_operator/validate.py +149 -0
- smooai_smooth_operator-1.2.0/tests/test_client.py +381 -0
- smooai_smooth_operator-1.2.0/tests/test_conformance.py +78 -0
- smooai_smooth_operator-1.2.0/tests/test_domain.py +125 -0
- smooai_smooth_operator-1.2.0/tests/test_e2e_live.py +207 -0
- smooai_smooth_operator-1.2.0/tests/test_robustness.py +204 -0
- smooai_smooth_operator-1.2.0/uv.lock +906 -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
|
|
File without changes
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: smooai-smooth-operator
|
|
3
|
+
Version: 1.2.0
|
|
4
|
+
Summary: Python protocol types and native async WebSocket client for the smooth-operator protocol. Generated from the language-neutral JSON Schemas in spec/.
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Requires-Dist: jsonschema>=4.21
|
|
8
|
+
Requires-Dist: pydantic[email]>=2
|
|
9
|
+
Provides-Extra: websockets
|
|
10
|
+
Requires-Dist: websockets>=12; extra == 'websockets'
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
<p align="center"><img src="../assets/smooth-logo.svg" alt="Smooth" width="360" /></p>
|
|
14
|
+
|
|
15
|
+
<p align="center"><strong>smooth-operator — Python client.</strong> A native, fully-async WebSocket client for the smooth-operator protocol, with pydantic v2 models.</p>
|
|
16
|
+
|
|
17
|
+
<p align="center">
|
|
18
|
+
<a href="../LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="MIT License" /></a>
|
|
19
|
+
<img src="https://img.shields.io/badge/tests-26%20passing-success" alt="26 tests passing" />
|
|
20
|
+
<img src="https://img.shields.io/badge/serverless%20%C2%B7%20polyglot%20%C2%B7%20TDD-6f42c1" alt="serverless · polyglot · TDD" />
|
|
21
|
+
<a href="https://lom.smoo.ai"><img src="https://img.shields.io/badge/hosted-lom.smoo.ai-0aa" alt="lom.smoo.ai" /></a>
|
|
22
|
+
</p>
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## What is this?
|
|
27
|
+
|
|
28
|
+
The **native async Python client** for the [smooth-operator](../docs/PROTOCOL.md) WebSocket protocol. The pydantic v2 models in `smooth_operator._generated` are generated from the language-neutral JSON Schemas in [`../spec/`](../spec) (and committed), using pydantic discriminated unions so events deserialize to the right concrete type. The wire is camelCase; you work in idiomatic snake_case.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## 30-second quickstart
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
uv add smooai-smooth-operator # PyPI publish pending — install from the local path today
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Until this package is published to PyPI, install it from a sibling checkout
|
|
39
|
+
(`uv add ../smooth-operator/python`, or `pip install -e path/to/smooth-operator/python`).
|
|
40
|
+
The PyPI distribution name is **`smooai-smooth-operator`** (the import package stays
|
|
41
|
+
`smooth_operator`) — don't `pip install smooth-operator` from the public index until
|
|
42
|
+
the SmooAI release lands.
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
import asyncio
|
|
46
|
+
from smooth_operator import SmoothAgentClient
|
|
47
|
+
|
|
48
|
+
async def main():
|
|
49
|
+
client = SmoothAgentClient(url="ws://127.0.0.1:8787/ws")
|
|
50
|
+
await client.connect()
|
|
51
|
+
|
|
52
|
+
session = await client.create_conversation_session(agent_id=agent_id, user_name="Alice")
|
|
53
|
+
|
|
54
|
+
turn = client.send_message(session_id=session.session_id, message="How long is your return window?")
|
|
55
|
+
final = await turn # the terminal eventual_response
|
|
56
|
+
print(final.data.payload.message_id)
|
|
57
|
+
|
|
58
|
+
asyncio.run(main())
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
(Point `url` at your own [`smooth-operator-server`](../rust/README.md) or the hosted endpoint.)
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Watch it stream
|
|
66
|
+
|
|
67
|
+
`send_message` returns a turn you can `async for` over for live events **and** `await` for the authoritative terminal response.
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
turn = client.send_message(session_id=session.session_id, message="Where is my order?")
|
|
71
|
+
|
|
72
|
+
async for event in turn:
|
|
73
|
+
if event.type == "stream_chunk":
|
|
74
|
+
print(f"\n ↳ node: {event.node}") # workflow node boundary
|
|
75
|
+
elif event.type == "stream_token":
|
|
76
|
+
print(event.token, end="", flush=True) # tokens, live
|
|
77
|
+
elif event.type == "write_confirmation_required":
|
|
78
|
+
# HITL: approve, and the resumed stream flows back into this same turn.
|
|
79
|
+
await client.confirm_tool_action(
|
|
80
|
+
session_id=session.session_id, request_id=turn.request_id, approved=True
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
final = await turn # the terminal eventual_response
|
|
84
|
+
print("\nmessageId:", final.data.payload.message_id)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
```mermaid
|
|
88
|
+
%%{init: {'theme':'base','themeVariables':{'background':'#020618','primaryColor':'#0b1426','primaryTextColor':'#e6edf6','primaryBorderColor':'#2b3a52','lineColor':'#7c8aa0','actorBkg':'#0b1426','actorBorder':'#2b3a52','actorTextColor':'#e6edf6','signalColor':'#7c8aa0','signalTextColor':'#e6edf6','noteBkgColor':'#f49f0a','noteTextColor':'#1a0f00','noteBorderColor':'#ff6b6c','fontFamily':'ui-sans-serif, system-ui, sans-serif'}}}%%
|
|
89
|
+
sequenceDiagram
|
|
90
|
+
participant App
|
|
91
|
+
participant C as SmoothAgentClient
|
|
92
|
+
participant S as Service
|
|
93
|
+
App->>C: send_message(...)
|
|
94
|
+
C->>S: { action: send_message }
|
|
95
|
+
S-->>C: immediate_response (202)
|
|
96
|
+
S-->>C: stream_token / stream_chunk …
|
|
97
|
+
S-->>C: eventual_response (200)
|
|
98
|
+
C-->>App: async-for yields events · await resolves final
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## camelCase wire, snake_case Python
|
|
104
|
+
|
|
105
|
+
The JSON wire form is camelCase (`requestId`, `sessionId`); the pydantic models use snake_case attributes with camelCase aliases and `populate_by_name = True`. So you construct/access with `session.session_id`, and `model_dump(by_alias=True)` emits the camelCase wire form.
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Polyglot — one spec, five clients
|
|
110
|
+
|
|
111
|
+
```mermaid
|
|
112
|
+
%%{init: {'theme':'base','themeVariables':{'background':'#020618','primaryColor':'#0b1426','primaryTextColor':'#e6edf6','primaryBorderColor':'#2b3a52','lineColor':'#7c8aa0','secondaryColor':'#0b1426','tertiaryColor':'#0b1426','fontFamily':'ui-sans-serif, system-ui, sans-serif','clusterBkg':'#0b1426','clusterBorder':'#22304a'}}}%%
|
|
113
|
+
flowchart LR
|
|
114
|
+
SPEC["spec/ (JSON Schema)"] --> PY["Python<br/>smooth_operator"]
|
|
115
|
+
SPEC --> TS["TypeScript"]
|
|
116
|
+
SPEC --> GO["Go"]
|
|
117
|
+
SPEC --> NET[".NET (+ MEAI IChatClient facade)"]
|
|
118
|
+
SPEC --> RS["Rust"]
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Test-driven by default
|
|
124
|
+
|
|
125
|
+
> **Nothing here is vibe-coded — it's verified against a real LLM gateway.**
|
|
126
|
+
|
|
127
|
+
```mermaid
|
|
128
|
+
%%{init: {'theme':'base','themeVariables':{'background':'#020618','primaryColor':'#0b1426','primaryTextColor':'#e6edf6','primaryBorderColor':'#2b3a52','lineColor':'#7c8aa0','secondaryColor':'#0b1426','tertiaryColor':'#0b1426','fontFamily':'ui-sans-serif, system-ui, sans-serif','clusterBkg':'#0b1426','clusterBorder':'#22304a'}}}%%
|
|
129
|
+
flowchart TD
|
|
130
|
+
J["🎯 LLM-as-judge quality evals (Rust harness)"]
|
|
131
|
+
E["🌐 Live cross-language E2E — this client boots the real server + drives a real claude-haiku-4-5 turn"]
|
|
132
|
+
C["🧪 Conformance fixtures (shared across all 5 clients)"]
|
|
133
|
+
U["⚡ Unit tests (discriminated-union parsing, alias round-trip, correlation)"]
|
|
134
|
+
J --> E --> C --> U
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
**26 tests.** The live cross-language E2E boots a real `smooth-operator-server` subprocess (KB seeded) and drives a real `claude-haiku-4-5` turn over WebSocket: ≥1 streamed event, a knowledge-grounded "17", per-session memory.
|
|
138
|
+
|
|
139
|
+
**A real bug the live E2E caught (mocks masked it):** `agentId` is UUID-typed in `spec/`, so pydantic rejected a bare string the lenient Go/TS clients accepted — surfacing a real cross-client `string`-vs-`UUID` alignment gap. A mock fixture using a valid UUID would have hidden it.
|
|
140
|
+
|
|
141
|
+
**The proof story:** an LLM-as-judge scored a multi-turn answer **1/5** (the runtime forgot turn 1's context); the failing eval drove a per-session-memory fix; **it now scores 5/5** — a regression a substring test would have missed. See [`docs/EVALS.md`](../docs/EVALS.md).
|
|
142
|
+
|
|
143
|
+
Live tests are **gated, never silently skipped** — `SMOOTH_AGENT_E2E=1` + `SMOOAI_GATEWAY_KEY` to run; skip cleanly otherwise.
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
uv run pytest # no creds
|
|
147
|
+
SMOOTH_AGENT_E2E=1 uv run pytest -m e2e # live cross-language E2E
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Develop & regenerate
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
uv sync
|
|
154
|
+
uv run python -c "import smooth_operator"
|
|
155
|
+
uv run python scripts/generate.py # regen pydantic models from ../spec via datamodel-code-generator
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Smoo-powered or bring-your-own
|
|
159
|
+
|
|
160
|
+
Point `url` at the hosted **[lom.smoo.ai](https://lom.smoo.ai)** endpoint, or at your own self-hosted `smooth-operator-server` — same protocol, same client.
|
|
161
|
+
|
|
162
|
+
## License
|
|
163
|
+
|
|
164
|
+
MIT © 2026 Smoo AI
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
<p align="center"><img src="../assets/smooth-logo.svg" alt="Smooth" width="360" /></p>
|
|
2
|
+
|
|
3
|
+
<p align="center"><strong>smooth-operator — Python client.</strong> A native, fully-async WebSocket client for the smooth-operator protocol, with pydantic v2 models.</p>
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<a href="../LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="MIT License" /></a>
|
|
7
|
+
<img src="https://img.shields.io/badge/tests-26%20passing-success" alt="26 tests passing" />
|
|
8
|
+
<img src="https://img.shields.io/badge/serverless%20%C2%B7%20polyglot%20%C2%B7%20TDD-6f42c1" alt="serverless · polyglot · TDD" />
|
|
9
|
+
<a href="https://lom.smoo.ai"><img src="https://img.shields.io/badge/hosted-lom.smoo.ai-0aa" alt="lom.smoo.ai" /></a>
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## What is this?
|
|
15
|
+
|
|
16
|
+
The **native async Python client** for the [smooth-operator](../docs/PROTOCOL.md) WebSocket protocol. The pydantic v2 models in `smooth_operator._generated` are generated from the language-neutral JSON Schemas in [`../spec/`](../spec) (and committed), using pydantic discriminated unions so events deserialize to the right concrete type. The wire is camelCase; you work in idiomatic snake_case.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 30-second quickstart
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
uv add smooai-smooth-operator # PyPI publish pending — install from the local path today
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Until this package is published to PyPI, install it from a sibling checkout
|
|
27
|
+
(`uv add ../smooth-operator/python`, or `pip install -e path/to/smooth-operator/python`).
|
|
28
|
+
The PyPI distribution name is **`smooai-smooth-operator`** (the import package stays
|
|
29
|
+
`smooth_operator`) — don't `pip install smooth-operator` from the public index until
|
|
30
|
+
the SmooAI release lands.
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
import asyncio
|
|
34
|
+
from smooth_operator import SmoothAgentClient
|
|
35
|
+
|
|
36
|
+
async def main():
|
|
37
|
+
client = SmoothAgentClient(url="ws://127.0.0.1:8787/ws")
|
|
38
|
+
await client.connect()
|
|
39
|
+
|
|
40
|
+
session = await client.create_conversation_session(agent_id=agent_id, user_name="Alice")
|
|
41
|
+
|
|
42
|
+
turn = client.send_message(session_id=session.session_id, message="How long is your return window?")
|
|
43
|
+
final = await turn # the terminal eventual_response
|
|
44
|
+
print(final.data.payload.message_id)
|
|
45
|
+
|
|
46
|
+
asyncio.run(main())
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
(Point `url` at your own [`smooth-operator-server`](../rust/README.md) or the hosted endpoint.)
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Watch it stream
|
|
54
|
+
|
|
55
|
+
`send_message` returns a turn you can `async for` over for live events **and** `await` for the authoritative terminal response.
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
turn = client.send_message(session_id=session.session_id, message="Where is my order?")
|
|
59
|
+
|
|
60
|
+
async for event in turn:
|
|
61
|
+
if event.type == "stream_chunk":
|
|
62
|
+
print(f"\n ↳ node: {event.node}") # workflow node boundary
|
|
63
|
+
elif event.type == "stream_token":
|
|
64
|
+
print(event.token, end="", flush=True) # tokens, live
|
|
65
|
+
elif event.type == "write_confirmation_required":
|
|
66
|
+
# HITL: approve, and the resumed stream flows back into this same turn.
|
|
67
|
+
await client.confirm_tool_action(
|
|
68
|
+
session_id=session.session_id, request_id=turn.request_id, approved=True
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
final = await turn # the terminal eventual_response
|
|
72
|
+
print("\nmessageId:", final.data.payload.message_id)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
```mermaid
|
|
76
|
+
%%{init: {'theme':'base','themeVariables':{'background':'#020618','primaryColor':'#0b1426','primaryTextColor':'#e6edf6','primaryBorderColor':'#2b3a52','lineColor':'#7c8aa0','actorBkg':'#0b1426','actorBorder':'#2b3a52','actorTextColor':'#e6edf6','signalColor':'#7c8aa0','signalTextColor':'#e6edf6','noteBkgColor':'#f49f0a','noteTextColor':'#1a0f00','noteBorderColor':'#ff6b6c','fontFamily':'ui-sans-serif, system-ui, sans-serif'}}}%%
|
|
77
|
+
sequenceDiagram
|
|
78
|
+
participant App
|
|
79
|
+
participant C as SmoothAgentClient
|
|
80
|
+
participant S as Service
|
|
81
|
+
App->>C: send_message(...)
|
|
82
|
+
C->>S: { action: send_message }
|
|
83
|
+
S-->>C: immediate_response (202)
|
|
84
|
+
S-->>C: stream_token / stream_chunk …
|
|
85
|
+
S-->>C: eventual_response (200)
|
|
86
|
+
C-->>App: async-for yields events · await resolves final
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## camelCase wire, snake_case Python
|
|
92
|
+
|
|
93
|
+
The JSON wire form is camelCase (`requestId`, `sessionId`); the pydantic models use snake_case attributes with camelCase aliases and `populate_by_name = True`. So you construct/access with `session.session_id`, and `model_dump(by_alias=True)` emits the camelCase wire form.
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Polyglot — one spec, five clients
|
|
98
|
+
|
|
99
|
+
```mermaid
|
|
100
|
+
%%{init: {'theme':'base','themeVariables':{'background':'#020618','primaryColor':'#0b1426','primaryTextColor':'#e6edf6','primaryBorderColor':'#2b3a52','lineColor':'#7c8aa0','secondaryColor':'#0b1426','tertiaryColor':'#0b1426','fontFamily':'ui-sans-serif, system-ui, sans-serif','clusterBkg':'#0b1426','clusterBorder':'#22304a'}}}%%
|
|
101
|
+
flowchart LR
|
|
102
|
+
SPEC["spec/ (JSON Schema)"] --> PY["Python<br/>smooth_operator"]
|
|
103
|
+
SPEC --> TS["TypeScript"]
|
|
104
|
+
SPEC --> GO["Go"]
|
|
105
|
+
SPEC --> NET[".NET (+ MEAI IChatClient facade)"]
|
|
106
|
+
SPEC --> RS["Rust"]
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Test-driven by default
|
|
112
|
+
|
|
113
|
+
> **Nothing here is vibe-coded — it's verified against a real LLM gateway.**
|
|
114
|
+
|
|
115
|
+
```mermaid
|
|
116
|
+
%%{init: {'theme':'base','themeVariables':{'background':'#020618','primaryColor':'#0b1426','primaryTextColor':'#e6edf6','primaryBorderColor':'#2b3a52','lineColor':'#7c8aa0','secondaryColor':'#0b1426','tertiaryColor':'#0b1426','fontFamily':'ui-sans-serif, system-ui, sans-serif','clusterBkg':'#0b1426','clusterBorder':'#22304a'}}}%%
|
|
117
|
+
flowchart TD
|
|
118
|
+
J["🎯 LLM-as-judge quality evals (Rust harness)"]
|
|
119
|
+
E["🌐 Live cross-language E2E — this client boots the real server + drives a real claude-haiku-4-5 turn"]
|
|
120
|
+
C["🧪 Conformance fixtures (shared across all 5 clients)"]
|
|
121
|
+
U["⚡ Unit tests (discriminated-union parsing, alias round-trip, correlation)"]
|
|
122
|
+
J --> E --> C --> U
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
**26 tests.** The live cross-language E2E boots a real `smooth-operator-server` subprocess (KB seeded) and drives a real `claude-haiku-4-5` turn over WebSocket: ≥1 streamed event, a knowledge-grounded "17", per-session memory.
|
|
126
|
+
|
|
127
|
+
**A real bug the live E2E caught (mocks masked it):** `agentId` is UUID-typed in `spec/`, so pydantic rejected a bare string the lenient Go/TS clients accepted — surfacing a real cross-client `string`-vs-`UUID` alignment gap. A mock fixture using a valid UUID would have hidden it.
|
|
128
|
+
|
|
129
|
+
**The proof story:** an LLM-as-judge scored a multi-turn answer **1/5** (the runtime forgot turn 1's context); the failing eval drove a per-session-memory fix; **it now scores 5/5** — a regression a substring test would have missed. See [`docs/EVALS.md`](../docs/EVALS.md).
|
|
130
|
+
|
|
131
|
+
Live tests are **gated, never silently skipped** — `SMOOTH_AGENT_E2E=1` + `SMOOAI_GATEWAY_KEY` to run; skip cleanly otherwise.
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
uv run pytest # no creds
|
|
135
|
+
SMOOTH_AGENT_E2E=1 uv run pytest -m e2e # live cross-language E2E
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Develop & regenerate
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
uv sync
|
|
142
|
+
uv run python -c "import smooth_operator"
|
|
143
|
+
uv run python scripts/generate.py # regen pydantic models from ../spec via datamodel-code-generator
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Smoo-powered or bring-your-own
|
|
147
|
+
|
|
148
|
+
Point `url` at the hosted **[lom.smoo.ai](https://lom.smoo.ai)** endpoint, or at your own self-hosted `smooth-operator-server` — same protocol, same client.
|
|
149
|
+
|
|
150
|
+
## License
|
|
151
|
+
|
|
152
|
+
MIT © 2026 Smoo AI
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# smooth-operator-core (Python)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "smooai-smooth-operator-core"
|
|
3
|
+
version = "1.2.0"
|
|
4
|
+
description = "Native Python implementation of the smooth-operator agent engine — an in-process, OpenAI-compatible agentic tool-calling loop with knowledge grounding. The Python sibling of the Rust reference engine and the C# core."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = { text = "MIT" }
|
|
7
|
+
requires-python = ">=3.11"
|
|
8
|
+
dependencies = [
|
|
9
|
+
# The engine drives any OpenAI-compatible chat endpoint via the official SDK
|
|
10
|
+
# (pointed at the gateway). This is the IChatClient analogue.
|
|
11
|
+
"openai>=1.40",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[dependency-groups]
|
|
15
|
+
dev = [
|
|
16
|
+
"pytest>=8",
|
|
17
|
+
"pytest-asyncio>=0.23",
|
|
18
|
+
"ruff>=0.4",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[build-system]
|
|
22
|
+
requires = ["hatchling"]
|
|
23
|
+
build-backend = "hatchling.build"
|
|
24
|
+
|
|
25
|
+
[tool.hatch.build.targets.wheel]
|
|
26
|
+
packages = ["src/smooth_operator_core"]
|
|
27
|
+
|
|
28
|
+
[tool.pytest.ini_options]
|
|
29
|
+
asyncio_mode = "auto"
|
|
30
|
+
testpaths = ["tests"]
|
|
31
|
+
|
|
32
|
+
[tool.ruff]
|
|
33
|
+
line-length = 120
|
|
34
|
+
target-version = "py311"
|
|
35
|
+
|
|
36
|
+
[tool.ruff.lint]
|
|
37
|
+
extend-select = ["I"]
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""smooth-operator-core (Python): a native, in-process agent engine.
|
|
2
|
+
|
|
3
|
+
The Phase-0 Python sibling of the Rust reference engine and the C# core — an
|
|
4
|
+
agentic tool-calling loop over any OpenAI-compatible chat client, with in-memory
|
|
5
|
+
knowledge grounding. See ``docs/Architecture/Python Core.md``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .agent import (
|
|
9
|
+
AgentOptions,
|
|
10
|
+
AgentRunResponse,
|
|
11
|
+
DoneEvent,
|
|
12
|
+
FunctionTool,
|
|
13
|
+
SmoothAgent,
|
|
14
|
+
StreamEvent,
|
|
15
|
+
TextEvent,
|
|
16
|
+
Tool,
|
|
17
|
+
ToolCallEvent,
|
|
18
|
+
ToolResultEvent,
|
|
19
|
+
delegate_tool,
|
|
20
|
+
)
|
|
21
|
+
from .cast import Cast, Clearance, OperatorRole, RoleKind
|
|
22
|
+
from .checkpoint import Checkpoint, CheckpointStore, InMemoryCheckpointStore
|
|
23
|
+
from .cost import CostBudget, CostTracker, ModelPricing, Usage
|
|
24
|
+
from .human_gate import (
|
|
25
|
+
DelegateHumanGate,
|
|
26
|
+
HumanApprovalRequest,
|
|
27
|
+
HumanApprovalResponse,
|
|
28
|
+
HumanDecision,
|
|
29
|
+
HumanGate,
|
|
30
|
+
)
|
|
31
|
+
from .knowledge import InMemoryKnowledge, Knowledge, KnowledgeHit
|
|
32
|
+
from .llm_provider import (
|
|
33
|
+
LlmProvider,
|
|
34
|
+
MockLlmProvider,
|
|
35
|
+
RecordedCall,
|
|
36
|
+
text_response,
|
|
37
|
+
tool_call_response,
|
|
38
|
+
usage,
|
|
39
|
+
)
|
|
40
|
+
from .memory import InMemoryMemory, Memory, MemoryEntry
|
|
41
|
+
from .rerank import LexicalReranker, NoopReranker, Reranker
|
|
42
|
+
from .thread import SmoothAgentThread
|
|
43
|
+
from .tool_search import ToolSearch
|
|
44
|
+
from .vector import Embedder, HashEmbedder, VectorKnowledge
|
|
45
|
+
from .workflow import END, Workflow, WorkflowError
|
|
46
|
+
|
|
47
|
+
__all__ = [
|
|
48
|
+
"AgentOptions",
|
|
49
|
+
"AgentRunResponse",
|
|
50
|
+
"Cast",
|
|
51
|
+
"DoneEvent",
|
|
52
|
+
"StreamEvent",
|
|
53
|
+
"TextEvent",
|
|
54
|
+
"ToolCallEvent",
|
|
55
|
+
"ToolResultEvent",
|
|
56
|
+
"usage",
|
|
57
|
+
"Checkpoint",
|
|
58
|
+
"CheckpointStore",
|
|
59
|
+
"Clearance",
|
|
60
|
+
"CostBudget",
|
|
61
|
+
"CostTracker",
|
|
62
|
+
"DelegateHumanGate",
|
|
63
|
+
"Embedder",
|
|
64
|
+
"FunctionTool",
|
|
65
|
+
"delegate_tool",
|
|
66
|
+
"HashEmbedder",
|
|
67
|
+
"HumanApprovalRequest",
|
|
68
|
+
"HumanApprovalResponse",
|
|
69
|
+
"HumanDecision",
|
|
70
|
+
"HumanGate",
|
|
71
|
+
"InMemoryCheckpointStore",
|
|
72
|
+
"InMemoryKnowledge",
|
|
73
|
+
"InMemoryMemory",
|
|
74
|
+
"Knowledge",
|
|
75
|
+
"KnowledgeHit",
|
|
76
|
+
"LexicalReranker",
|
|
77
|
+
"LlmProvider",
|
|
78
|
+
"Memory",
|
|
79
|
+
"MemoryEntry",
|
|
80
|
+
"MockLlmProvider",
|
|
81
|
+
"ModelPricing",
|
|
82
|
+
"NoopReranker",
|
|
83
|
+
"OperatorRole",
|
|
84
|
+
"RecordedCall",
|
|
85
|
+
"Reranker",
|
|
86
|
+
"RoleKind",
|
|
87
|
+
"SmoothAgent",
|
|
88
|
+
"SmoothAgentThread",
|
|
89
|
+
"Tool",
|
|
90
|
+
"ToolSearch",
|
|
91
|
+
"Usage",
|
|
92
|
+
"VectorKnowledge",
|
|
93
|
+
"Workflow",
|
|
94
|
+
"WorkflowError",
|
|
95
|
+
"END",
|
|
96
|
+
"text_response",
|
|
97
|
+
"tool_call_response",
|
|
98
|
+
]
|