snail-dsl 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.
- snail_dsl-0.1.0/LICENSE +17 -0
- snail_dsl-0.1.0/PKG-INFO +165 -0
- snail_dsl-0.1.0/README.md +135 -0
- snail_dsl-0.1.0/pyproject.toml +51 -0
- snail_dsl-0.1.0/setup.cfg +4 -0
- snail_dsl-0.1.0/src/snail/__init__.py +47 -0
- snail_dsl-0.1.0/src/snail/lint.py +88 -0
- snail_dsl-0.1.0/src/snail/manifest.py +108 -0
- snail_dsl-0.1.0/src/snail/node.py +244 -0
- snail_dsl-0.1.0/src/snail/program.py +449 -0
- snail_dsl-0.1.0/src/snail/types/__init__.py +11 -0
- snail_dsl-0.1.0/src/snail/types/ood.py +32 -0
- snail_dsl-0.1.0/src/snail/types/result.py +60 -0
- snail_dsl-0.1.0/src/snail/wrappers.py +265 -0
- snail_dsl-0.1.0/src/snail_dsl.egg-info/PKG-INFO +165 -0
- snail_dsl-0.1.0/src/snail_dsl.egg-info/SOURCES.txt +21 -0
- snail_dsl-0.1.0/src/snail_dsl.egg-info/dependency_links.txt +1 -0
- snail_dsl-0.1.0/src/snail_dsl.egg-info/requires.txt +7 -0
- snail_dsl-0.1.0/src/snail_dsl.egg-info/top_level.txt +1 -0
- snail_dsl-0.1.0/tests/test_lint.py +72 -0
- snail_dsl-0.1.0/tests/test_node.py +208 -0
- snail_dsl-0.1.0/tests/test_program.py +176 -0
- snail_dsl-0.1.0/tests/test_wrappers.py +174 -0
snail_dsl-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
Copyright 2026 Tico Internet LLC
|
|
6
|
+
|
|
7
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
8
|
+
you may not use this file except in compliance with the License.
|
|
9
|
+
You may obtain a copy of the License at
|
|
10
|
+
|
|
11
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
|
|
13
|
+
Unless required by applicable law or agreed to in writing, software
|
|
14
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
15
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
|
+
See the License for the specific language governing permissions and
|
|
17
|
+
limitations under the License.
|
snail_dsl-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: snail-dsl
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Single Node Activated Inference Layer — a Python DSL for composing frozen neural primitives into typed dataflow programs.
|
|
5
|
+
Author: Tico Internet LLC
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/lordxmen2k/SNAIL-DSL
|
|
8
|
+
Project-URL: Repository, https://github.com/lordxmen2k/SNAIL-DSL
|
|
9
|
+
Keywords: ai,ml,dsl,frozen-models,dataflow,neural-networks,agents
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: pydantic>=2.0
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
26
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
27
|
+
Requires-Dist: build>=0.10; extra == "dev"
|
|
28
|
+
Requires-Dist: twine>=4.0; extra == "dev"
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
|
|
31
|
+
# SNAIL — Single Node Activated Inference Layer
|
|
32
|
+
|
|
33
|
+
A Python DSL for composing **frozen, single-pass neural primitives** into **statically-typed dataflow programs**.
|
|
34
|
+
|
|
35
|
+
If an MCP tool and a markdown skill had a baby, and you told the LLM it wasn't allowed to interpret the recipe — it was just one of many nodes in the recipe — you'd get SNAIL.
|
|
36
|
+
|
|
37
|
+
## Why
|
|
38
|
+
|
|
39
|
+
Most AI models are trained to be comprehensive. SNAIL trains tiny, single-task nodes — **activated exactly once**, **locked output**, **frozen forever** — and composes them into deterministic graphs.
|
|
40
|
+
|
|
41
|
+
A SNAIL program is a **recipe that's more resilient than a `.md` spec** and **more deterministic than an MCP tool call**. Every step is a typed function call, not a prompt the LLM might re-interpret. Every run emits a manifest. Every node knows what it doesn't know (OOD is a first-class type).
|
|
42
|
+
|
|
43
|
+
## Install
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install snail-dsl
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Quick start
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from snail import node, Program, edge
|
|
53
|
+
from pydantic import BaseModel
|
|
54
|
+
|
|
55
|
+
# Declare the type contract for each node's output.
|
|
56
|
+
class InvoiceTotal(BaseModel):
|
|
57
|
+
ok: dict | None = None # { "total": float, "currency": str, "confidence": float }
|
|
58
|
+
ood: dict | None = None # { "reason": str, "confidence": float, "threshold": float }
|
|
59
|
+
|
|
60
|
+
@node(
|
|
61
|
+
name="extract_invoice_total",
|
|
62
|
+
input_schema=dict, # replace with a real pydantic model
|
|
63
|
+
output_schema=InvoiceTotal,
|
|
64
|
+
distribution="synthetic_invoices_v2",
|
|
65
|
+
frozen_weights="weights/extract_invoice_total_v3.snail",
|
|
66
|
+
confidence_threshold=0.7,
|
|
67
|
+
)
|
|
68
|
+
def extract_invoice_total(ctx, weights, image):
|
|
69
|
+
raw = weights.model.forward(image["tensor"])
|
|
70
|
+
return InvoiceTotal(ok={
|
|
71
|
+
"total": raw["total"],
|
|
72
|
+
"currency": raw["currency"],
|
|
73
|
+
"confidence": raw["confidence"],
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
# Compose into a typed DAG.
|
|
77
|
+
invoice_pipeline = Program(
|
|
78
|
+
name="invoice_pipeline_v1",
|
|
79
|
+
nodes=[extract_invoice_total],
|
|
80
|
+
edges=[],
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
result = invoice_pipeline.run({"image_path": "scan.png"})
|
|
84
|
+
print(result.manifest)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The decorator enforces:
|
|
88
|
+
|
|
89
|
+
- Single forward pass per call — no retries, no second thoughts.
|
|
90
|
+
- Weights loaded once, frozen for the lifetime of the program.
|
|
91
|
+
- Output type-checked against `output_schema`; mismatches become OOD.
|
|
92
|
+
- Confidence below `confidence_threshold` is silently flipped to OOD.
|
|
93
|
+
- Output is locked (immutable) before being handed back to the caller.
|
|
94
|
+
|
|
95
|
+
## The four primitives
|
|
96
|
+
|
|
97
|
+
| Primitive | What it does |
|
|
98
|
+
|---|---|
|
|
99
|
+
| `@node` | Decorator that wraps a Python function as a frozen, single-pass, OOD-aware node. |
|
|
100
|
+
| `Program` | Container that builds a typed DAG of nodes. Validates at construction time. |
|
|
101
|
+
| `edge()` | Builder for typed field-to-field connections between nodes. |
|
|
102
|
+
| `manifest` | Structured per-run log: which nodes fired, what they got, what they returned. |
|
|
103
|
+
|
|
104
|
+
## Architecture
|
|
105
|
+
|
|
106
|
+
- **Nodes** are frozen, single forward pass, locked output.
|
|
107
|
+
- **Composition** is a static DAG declared in Python.
|
|
108
|
+
- **Training** is 100% synthetic, offline.
|
|
109
|
+
- **OOD** is a first-class type (Ok / OOD discriminated union).
|
|
110
|
+
- **Every node ships with golden tests** (`pytest`).
|
|
111
|
+
- **Every run emits a manifest.**
|
|
112
|
+
|
|
113
|
+
## Wrapping external models
|
|
114
|
+
|
|
115
|
+
External models (HuggingFace, hosted APIs, pure functions) become nodes through wrappers. They never get called directly.
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
from snail.wrappers import ExternalLocalNode, HostedNode, DeterministicNode
|
|
119
|
+
|
|
120
|
+
# Pin a local model to exact weights hash
|
|
121
|
+
classify_layout = ExternalLocalNode(
|
|
122
|
+
name="classify_layout",
|
|
123
|
+
input_schema=InvoiceImage,
|
|
124
|
+
output_schema=LayoutClass,
|
|
125
|
+
distribution="rvl_cdip_subset",
|
|
126
|
+
call=lambda img: yolo_model.predict(img.path),
|
|
127
|
+
weight_pin="yolov8n@sha256:abc123...",
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# Wrap a hosted API
|
|
131
|
+
summarize = HostedNode(
|
|
132
|
+
name="summarize",
|
|
133
|
+
input_schema=InvoiceText,
|
|
134
|
+
output_schema=InvoiceSummary,
|
|
135
|
+
endpoint="anthropic://claude-sonnet-4-5",
|
|
136
|
+
prompt_template="Summarize: {text}",
|
|
137
|
+
api_key_env="ANTHROPIC_API_KEY",
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# Or just a pure function
|
|
141
|
+
validate_email = DeterministicNode(
|
|
142
|
+
name="validate_email",
|
|
143
|
+
input_schema=EmailField,
|
|
144
|
+
output_schema=EmailValidated,
|
|
145
|
+
fn=lambda x: x if "@" in x.value else None,
|
|
146
|
+
ood_on_none=True,
|
|
147
|
+
)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Testing
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
pip install -e ".[dev]"
|
|
154
|
+
pytest
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Golden tests live in `tests/golden/`. The lint rule (catches direct `import openai`, `import anthropic`, etc. outside wrappers) is enforced via the `snail.lint` module.
|
|
158
|
+
|
|
159
|
+
## License
|
|
160
|
+
|
|
161
|
+
Apache License 2.0. Copyright 2026 Tico Internet LLC.
|
|
162
|
+
|
|
163
|
+
## Status
|
|
164
|
+
|
|
165
|
+
v0.1.0 — alpha. The core primitives (`@node`, `Program`, `edge()`, wrappers, manifest, lint) are working. The training pipeline for producing `.snail` weights is not yet included — that ships in v0.2.0.
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# SNAIL — Single Node Activated Inference Layer
|
|
2
|
+
|
|
3
|
+
A Python DSL for composing **frozen, single-pass neural primitives** into **statically-typed dataflow programs**.
|
|
4
|
+
|
|
5
|
+
If an MCP tool and a markdown skill had a baby, and you told the LLM it wasn't allowed to interpret the recipe — it was just one of many nodes in the recipe — you'd get SNAIL.
|
|
6
|
+
|
|
7
|
+
## Why
|
|
8
|
+
|
|
9
|
+
Most AI models are trained to be comprehensive. SNAIL trains tiny, single-task nodes — **activated exactly once**, **locked output**, **frozen forever** — and composes them into deterministic graphs.
|
|
10
|
+
|
|
11
|
+
A SNAIL program is a **recipe that's more resilient than a `.md` spec** and **more deterministic than an MCP tool call**. Every step is a typed function call, not a prompt the LLM might re-interpret. Every run emits a manifest. Every node knows what it doesn't know (OOD is a first-class type).
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install snail-dsl
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quick start
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from snail import node, Program, edge
|
|
23
|
+
from pydantic import BaseModel
|
|
24
|
+
|
|
25
|
+
# Declare the type contract for each node's output.
|
|
26
|
+
class InvoiceTotal(BaseModel):
|
|
27
|
+
ok: dict | None = None # { "total": float, "currency": str, "confidence": float }
|
|
28
|
+
ood: dict | None = None # { "reason": str, "confidence": float, "threshold": float }
|
|
29
|
+
|
|
30
|
+
@node(
|
|
31
|
+
name="extract_invoice_total",
|
|
32
|
+
input_schema=dict, # replace with a real pydantic model
|
|
33
|
+
output_schema=InvoiceTotal,
|
|
34
|
+
distribution="synthetic_invoices_v2",
|
|
35
|
+
frozen_weights="weights/extract_invoice_total_v3.snail",
|
|
36
|
+
confidence_threshold=0.7,
|
|
37
|
+
)
|
|
38
|
+
def extract_invoice_total(ctx, weights, image):
|
|
39
|
+
raw = weights.model.forward(image["tensor"])
|
|
40
|
+
return InvoiceTotal(ok={
|
|
41
|
+
"total": raw["total"],
|
|
42
|
+
"currency": raw["currency"],
|
|
43
|
+
"confidence": raw["confidence"],
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
# Compose into a typed DAG.
|
|
47
|
+
invoice_pipeline = Program(
|
|
48
|
+
name="invoice_pipeline_v1",
|
|
49
|
+
nodes=[extract_invoice_total],
|
|
50
|
+
edges=[],
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
result = invoice_pipeline.run({"image_path": "scan.png"})
|
|
54
|
+
print(result.manifest)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The decorator enforces:
|
|
58
|
+
|
|
59
|
+
- Single forward pass per call — no retries, no second thoughts.
|
|
60
|
+
- Weights loaded once, frozen for the lifetime of the program.
|
|
61
|
+
- Output type-checked against `output_schema`; mismatches become OOD.
|
|
62
|
+
- Confidence below `confidence_threshold` is silently flipped to OOD.
|
|
63
|
+
- Output is locked (immutable) before being handed back to the caller.
|
|
64
|
+
|
|
65
|
+
## The four primitives
|
|
66
|
+
|
|
67
|
+
| Primitive | What it does |
|
|
68
|
+
|---|---|
|
|
69
|
+
| `@node` | Decorator that wraps a Python function as a frozen, single-pass, OOD-aware node. |
|
|
70
|
+
| `Program` | Container that builds a typed DAG of nodes. Validates at construction time. |
|
|
71
|
+
| `edge()` | Builder for typed field-to-field connections between nodes. |
|
|
72
|
+
| `manifest` | Structured per-run log: which nodes fired, what they got, what they returned. |
|
|
73
|
+
|
|
74
|
+
## Architecture
|
|
75
|
+
|
|
76
|
+
- **Nodes** are frozen, single forward pass, locked output.
|
|
77
|
+
- **Composition** is a static DAG declared in Python.
|
|
78
|
+
- **Training** is 100% synthetic, offline.
|
|
79
|
+
- **OOD** is a first-class type (Ok / OOD discriminated union).
|
|
80
|
+
- **Every node ships with golden tests** (`pytest`).
|
|
81
|
+
- **Every run emits a manifest.**
|
|
82
|
+
|
|
83
|
+
## Wrapping external models
|
|
84
|
+
|
|
85
|
+
External models (HuggingFace, hosted APIs, pure functions) become nodes through wrappers. They never get called directly.
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from snail.wrappers import ExternalLocalNode, HostedNode, DeterministicNode
|
|
89
|
+
|
|
90
|
+
# Pin a local model to exact weights hash
|
|
91
|
+
classify_layout = ExternalLocalNode(
|
|
92
|
+
name="classify_layout",
|
|
93
|
+
input_schema=InvoiceImage,
|
|
94
|
+
output_schema=LayoutClass,
|
|
95
|
+
distribution="rvl_cdip_subset",
|
|
96
|
+
call=lambda img: yolo_model.predict(img.path),
|
|
97
|
+
weight_pin="yolov8n@sha256:abc123...",
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# Wrap a hosted API
|
|
101
|
+
summarize = HostedNode(
|
|
102
|
+
name="summarize",
|
|
103
|
+
input_schema=InvoiceText,
|
|
104
|
+
output_schema=InvoiceSummary,
|
|
105
|
+
endpoint="anthropic://claude-sonnet-4-5",
|
|
106
|
+
prompt_template="Summarize: {text}",
|
|
107
|
+
api_key_env="ANTHROPIC_API_KEY",
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# Or just a pure function
|
|
111
|
+
validate_email = DeterministicNode(
|
|
112
|
+
name="validate_email",
|
|
113
|
+
input_schema=EmailField,
|
|
114
|
+
output_schema=EmailValidated,
|
|
115
|
+
fn=lambda x: x if "@" in x.value else None,
|
|
116
|
+
ood_on_none=True,
|
|
117
|
+
)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Testing
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
pip install -e ".[dev]"
|
|
124
|
+
pytest
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Golden tests live in `tests/golden/`. The lint rule (catches direct `import openai`, `import anthropic`, etc. outside wrappers) is enforced via the `snail.lint` module.
|
|
128
|
+
|
|
129
|
+
## License
|
|
130
|
+
|
|
131
|
+
Apache License 2.0. Copyright 2026 Tico Internet LLC.
|
|
132
|
+
|
|
133
|
+
## Status
|
|
134
|
+
|
|
135
|
+
v0.1.0 — alpha. The core primitives (`@node`, `Program`, `edge()`, wrappers, manifest, lint) are working. The training pipeline for producing `.snail` weights is not yet included — that ships in v0.2.0.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "snail-dsl"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Single Node Activated Inference Layer — a Python DSL for composing frozen neural primitives into typed dataflow programs."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "Apache-2.0"}
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "Tico Internet LLC"},
|
|
14
|
+
]
|
|
15
|
+
keywords = ["ai", "ml", "dsl", "frozen-models", "dataflow", "neural-networks", "agents"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Intended Audience :: Science/Research",
|
|
20
|
+
"License :: OSI Approved :: Apache Software License",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.10",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
26
|
+
"Typing :: Typed",
|
|
27
|
+
]
|
|
28
|
+
dependencies = [
|
|
29
|
+
"pydantic>=2.0",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.optional-dependencies]
|
|
33
|
+
dev = [
|
|
34
|
+
"pytest>=7.0",
|
|
35
|
+
"pytest-cov>=4.0",
|
|
36
|
+
"build>=0.10",
|
|
37
|
+
"twine>=4.0",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
[project.urls]
|
|
41
|
+
Homepage = "https://github.com/lordxmen2k/SNAIL-DSL"
|
|
42
|
+
Repository = "https://github.com/lordxmen2k/SNAIL-DSL"
|
|
43
|
+
|
|
44
|
+
[tool.setuptools.packages.find]
|
|
45
|
+
where = ["src"]
|
|
46
|
+
|
|
47
|
+
[tool.pytest.ini_options]
|
|
48
|
+
testpaths = ["tests"]
|
|
49
|
+
python_files = ["test_*.py"]
|
|
50
|
+
python_classes = ["Test*"]
|
|
51
|
+
python_functions = ["test_*"]
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""SNAIL — Single Node Activated Inference Layer.
|
|
2
|
+
|
|
3
|
+
A Python DSL for composing frozen, single-pass neural primitives
|
|
4
|
+
into statically-typed dataflow programs.
|
|
5
|
+
|
|
6
|
+
Public API (import from `snail`):
|
|
7
|
+
node — decorator that wraps a function as a frozen SNAIL node
|
|
8
|
+
NodeContext — per-call context passed to node bodies
|
|
9
|
+
Program — typed DAG container for nodes
|
|
10
|
+
edge — builder for typed connections between nodes
|
|
11
|
+
NodeResult — base class for all node output schemas
|
|
12
|
+
OODSignal — out-of-distribution signal type
|
|
13
|
+
Manifest — per-run audit log
|
|
14
|
+
|
|
15
|
+
Subpackages:
|
|
16
|
+
snail.wrappers — ExternalLocalNode, HostedNode, DeterministicNode
|
|
17
|
+
snail.lint — discipline check that catches direct model SDK imports
|
|
18
|
+
snail.types — internal type definitions
|
|
19
|
+
|
|
20
|
+
License: Apache 2.0. Copyright 2026 Tico Internet LLC.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from snail.node import node, NodeContext, FrozenWeights
|
|
24
|
+
from snail.program import Program, Edge, edge, NodeProxy, ProgramRunResult
|
|
25
|
+
from snail.types.result import NodeResult
|
|
26
|
+
from snail.types.ood import OODSignal
|
|
27
|
+
from snail.manifest import Manifest, ManifestBuilder
|
|
28
|
+
|
|
29
|
+
__version__ = "0.1.0"
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
# core
|
|
33
|
+
"node",
|
|
34
|
+
"NodeContext",
|
|
35
|
+
"FrozenWeights",
|
|
36
|
+
"Program",
|
|
37
|
+
"Edge",
|
|
38
|
+
"edge",
|
|
39
|
+
"NodeProxy",
|
|
40
|
+
"ProgramRunResult",
|
|
41
|
+
# types
|
|
42
|
+
"NodeResult",
|
|
43
|
+
"OODSignal",
|
|
44
|
+
# observability
|
|
45
|
+
"Manifest",
|
|
46
|
+
"ManifestBuilder",
|
|
47
|
+
]
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""SNAIL lint rule — catches direct imports of model SDKs outside wrappers.
|
|
2
|
+
|
|
3
|
+
If you import openai/anthropic/transformers/etc. at module level in a
|
|
4
|
+
file that isn't a SNAIL wrapper, that's a violation. All model access
|
|
5
|
+
must go through @node or one of the snail.wrappers factories.
|
|
6
|
+
|
|
7
|
+
This is a discipline check, not a style preference. The lint module
|
|
8
|
+
exposes a `check_source(path)` function for CI integration and a
|
|
9
|
+
`FORBIDDEN_TOP_LEVEL_IMPORTS` set for users to extend.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
import ast
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
# These modules, if imported at module level outside snail.wrappers,
|
|
17
|
+
# indicate someone is calling a model directly without going through @node.
|
|
18
|
+
FORBIDDEN_TOP_LEVEL_IMPORTS: set[str] = {
|
|
19
|
+
"openai",
|
|
20
|
+
"anthropic",
|
|
21
|
+
"transformers",
|
|
22
|
+
"torch", # torch at module level outside training scripts
|
|
23
|
+
"tensorflow",
|
|
24
|
+
"jax",
|
|
25
|
+
"flax",
|
|
26
|
+
"replicate",
|
|
27
|
+
"huggingface_hub",
|
|
28
|
+
"litellm",
|
|
29
|
+
"ollama",
|
|
30
|
+
"langchain",
|
|
31
|
+
"langgraph",
|
|
32
|
+
"crewai",
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def check_source(path: str | Path) -> list[str]:
|
|
37
|
+
"""Return a list of violation messages for the given Python file.
|
|
38
|
+
|
|
39
|
+
Empty list = clean.
|
|
40
|
+
"""
|
|
41
|
+
p = Path(path)
|
|
42
|
+
if not p.exists():
|
|
43
|
+
return [f"file not found: {p}"]
|
|
44
|
+
|
|
45
|
+
# Don't lint the wrappers module itself — that's where model SDKs are
|
|
46
|
+
# allowed to be imported.
|
|
47
|
+
if "snail/wrappers" in str(p) or "snail\\wrappers" in str(p):
|
|
48
|
+
return []
|
|
49
|
+
# Don't lint the lint module itself.
|
|
50
|
+
if "snail/lint" in str(p):
|
|
51
|
+
return []
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
tree = ast.parse(p.read_text())
|
|
55
|
+
except SyntaxError as e:
|
|
56
|
+
return [f"SyntaxError: {e}"]
|
|
57
|
+
|
|
58
|
+
violations: list[str] = []
|
|
59
|
+
for node in ast.walk(tree):
|
|
60
|
+
if isinstance(node, ast.Import):
|
|
61
|
+
for alias in node.names:
|
|
62
|
+
top = alias.name.split(".")[0]
|
|
63
|
+
if top in FORBIDDEN_TOP_LEVEL_IMPORTS:
|
|
64
|
+
violations.append(
|
|
65
|
+
f"{p}:{node.lineno}: direct import of {alias.name!r}. "
|
|
66
|
+
"Model SDKs must go through @node or snail.wrappers."
|
|
67
|
+
)
|
|
68
|
+
elif isinstance(node, ast.ImportFrom):
|
|
69
|
+
if node.module:
|
|
70
|
+
top = node.module.split(".")[0]
|
|
71
|
+
if top in FORBIDDEN_TOP_LEVEL_IMPORTS:
|
|
72
|
+
violations.append(
|
|
73
|
+
f"{p}:{node.lineno}: direct import from {node.module!r}. "
|
|
74
|
+
"Model SDKs must go through @node or snail.wrappers."
|
|
75
|
+
)
|
|
76
|
+
return violations
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def check_directory(root: str | Path, exclude_dirs: set[str] | None = None) -> list[str]:
|
|
80
|
+
"""Walk a directory tree and lint every .py file. Returns all violations."""
|
|
81
|
+
exclude = exclude_dirs or {".venv", "venv", "build", "dist", "__pycache__", ".git"}
|
|
82
|
+
root = Path(root)
|
|
83
|
+
all_violations: list[str] = []
|
|
84
|
+
for p in root.rglob("*.py"):
|
|
85
|
+
if any(part in exclude for part in p.parts):
|
|
86
|
+
continue
|
|
87
|
+
all_violations.extend(check_source(p))
|
|
88
|
+
return all_violations
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Run manifests — structured per-run log of what fired, when, and what it returned.
|
|
2
|
+
|
|
3
|
+
Every Program.run() emits a manifest. Manifests are JSON-serializable.
|
|
4
|
+
They are the audit trail for any SNAIL program — show them to compliance,
|
|
5
|
+
to your CEO, to the user who asked why their refund took 2 seconds.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
import json
|
|
10
|
+
import time
|
|
11
|
+
import uuid
|
|
12
|
+
from dataclasses import dataclass, field, asdict
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class ManifestNodeEvent:
|
|
18
|
+
node_name: str
|
|
19
|
+
variant: str # "ok" or "ood"
|
|
20
|
+
latency_ms: float
|
|
21
|
+
confidence: float | None = None
|
|
22
|
+
timestamp: float = field(default_factory=time.time)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class ManifestErrorEvent:
|
|
27
|
+
node_name: str
|
|
28
|
+
error: str
|
|
29
|
+
timestamp: float = field(default_factory=time.time)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class Manifest:
|
|
34
|
+
program_name: str
|
|
35
|
+
run_id: str
|
|
36
|
+
started_at: float
|
|
37
|
+
ended_at: float | None
|
|
38
|
+
total_duration_ms: float
|
|
39
|
+
node_events: list[ManifestNodeEvent]
|
|
40
|
+
errors: list[ManifestErrorEvent]
|
|
41
|
+
|
|
42
|
+
def to_dict(self) -> dict[str, Any]:
|
|
43
|
+
return {
|
|
44
|
+
"program_name": self.program_name,
|
|
45
|
+
"run_id": self.run_id,
|
|
46
|
+
"started_at": self.started_at,
|
|
47
|
+
"ended_at": self.ended_at,
|
|
48
|
+
"total_duration_ms": self.total_duration_ms,
|
|
49
|
+
"node_events": [asdict(e) for e in self.node_events],
|
|
50
|
+
"errors": [asdict(e) for e in self.errors],
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
def to_json(self, indent: int | None = 2) -> str:
|
|
54
|
+
return json.dumps(self.to_dict(), indent=indent, default=str)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class ManifestBuilder:
|
|
58
|
+
"""Accumulates events during a single Program.run() and emits a Manifest."""
|
|
59
|
+
|
|
60
|
+
def __init__(self, program_name: str, run_id: str | None = None):
|
|
61
|
+
self.program_name = program_name
|
|
62
|
+
self.run_id = run_id or str(uuid.uuid4())
|
|
63
|
+
self._started_at: float | None = None
|
|
64
|
+
self._ended_at: float | None = None
|
|
65
|
+
self._node_events: list[ManifestNodeEvent] = []
|
|
66
|
+
self._errors: list[ManifestErrorEvent] = []
|
|
67
|
+
|
|
68
|
+
def record_start(self) -> None:
|
|
69
|
+
self._started_at = time.time()
|
|
70
|
+
|
|
71
|
+
def record_end(self) -> None:
|
|
72
|
+
self._ended_at = time.time()
|
|
73
|
+
|
|
74
|
+
def record_node(
|
|
75
|
+
self,
|
|
76
|
+
*,
|
|
77
|
+
node_name: str,
|
|
78
|
+
variant: str,
|
|
79
|
+
latency_ms: float,
|
|
80
|
+
confidence: float | None = None,
|
|
81
|
+
) -> None:
|
|
82
|
+
self._node_events.append(
|
|
83
|
+
ManifestNodeEvent(
|
|
84
|
+
node_name=node_name,
|
|
85
|
+
variant=variant,
|
|
86
|
+
latency_ms=latency_ms,
|
|
87
|
+
confidence=confidence,
|
|
88
|
+
)
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
def record_error(self, *, node_name: str, error: str) -> None:
|
|
92
|
+
self._errors.append(
|
|
93
|
+
ManifestErrorEvent(node_name=node_name, error=error)
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
def build(self) -> Manifest:
|
|
97
|
+
if self._started_at is None:
|
|
98
|
+
raise RuntimeError("ManifestBuilder.build() called before record_start()")
|
|
99
|
+
ended = self._ended_at if self._ended_at is not None else time.time()
|
|
100
|
+
return Manifest(
|
|
101
|
+
program_name=self.program_name,
|
|
102
|
+
run_id=self.run_id,
|
|
103
|
+
started_at=self._started_at,
|
|
104
|
+
ended_at=self._ended_at,
|
|
105
|
+
total_duration_ms=(ended - self._started_at) * 1000.0,
|
|
106
|
+
node_events=list(self._node_events),
|
|
107
|
+
errors=list(self._errors),
|
|
108
|
+
)
|