annoscript 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.
- annoscript-0.1.0/.gitignore +13 -0
- annoscript-0.1.0/LICENSE +21 -0
- annoscript-0.1.0/PKG-INFO +145 -0
- annoscript-0.1.0/README.md +118 -0
- annoscript-0.1.0/annoscript/__init__.py +1 -0
- annoscript-0.1.0/annoscript/_backend_profiles.py +17 -0
- annoscript-0.1.0/annoscript/_client_common.py +287 -0
- annoscript-0.1.0/annoscript/_data.py +37 -0
- annoscript-0.1.0/annoscript/_dispatch_pool.py +47 -0
- annoscript-0.1.0/annoscript/_env.py +19 -0
- annoscript-0.1.0/annoscript/_examples.py +34 -0
- annoscript-0.1.0/annoscript/_expressions.py +93 -0
- annoscript-0.1.0/annoscript/_plan_viewer.html +953 -0
- annoscript-0.1.0/annoscript/_remote_jobs.py +211 -0
- annoscript-0.1.0/annoscript/_request_adapters.py +71 -0
- annoscript-0.1.0/annoscript/_speculation.py +321 -0
- annoscript-0.1.0/annoscript/_test_artifacts.py +33 -0
- annoscript-0.1.0/annoscript/_test_evaluation_http.py +117 -0
- annoscript-0.1.0/annoscript/_test_ir_runtime.py +138 -0
- annoscript-0.1.0/annoscript/_test_job_process.py +166 -0
- annoscript-0.1.0/annoscript/_test_plan_runtime.py +331 -0
- annoscript-0.1.0/annoscript/_test_remote_client.py +46 -0
- annoscript-0.1.0/annoscript/_test_remote_server.py +75 -0
- annoscript-0.1.0/annoscript/_test_synthetic.py +67 -0
- annoscript-0.1.0/annoscript/anno.py +270 -0
- annoscript-0.1.0/annoscript/backends/__init__.py +13 -0
- annoscript-0.1.0/annoscript/backends/_batch_wire.py +26 -0
- annoscript-0.1.0/annoscript/backends/_bounded_grammar.py +97 -0
- annoscript-0.1.0/annoscript/backends/_example_layout.py +23 -0
- annoscript-0.1.0/annoscript/backends/_generation_policy.py +116 -0
- annoscript-0.1.0/annoscript/backends/_grammar.py +83 -0
- annoscript-0.1.0/annoscript/backends/_preview.py +13 -0
- annoscript-0.1.0/annoscript/backends/_prompt.py +115 -0
- annoscript-0.1.0/annoscript/backends/_shared_rules.py +38 -0
- annoscript-0.1.0/annoscript/backends/adapters/__init__.py +17 -0
- annoscript-0.1.0/annoscript/backends/adapters/chat.py +39 -0
- annoscript-0.1.0/annoscript/backends/adapters/decision.py +119 -0
- annoscript-0.1.0/annoscript/backends/base.py +427 -0
- annoscript-0.1.0/annoscript/backends/config.py +67 -0
- annoscript-0.1.0/annoscript/backends/decision.py +150 -0
- annoscript-0.1.0/annoscript/backends/llm.py +338 -0
- annoscript-0.1.0/annoscript/client.py +247 -0
- annoscript-0.1.0/annoscript/client_tests.py +17 -0
- annoscript-0.1.0/annoscript/compiler.py +708 -0
- annoscript-0.1.0/annoscript/cross_version_tests.py +68 -0
- annoscript-0.1.0/annoscript/fixtures/request-adapters-v1.json +4741 -0
- annoscript-0.1.0/annoscript/formatter.py +171 -0
- annoscript-0.1.0/annoscript/grammar_tests.py +176 -0
- annoscript-0.1.0/annoscript/inspect.py +77 -0
- annoscript-0.1.0/annoscript/ir.py +458 -0
- annoscript-0.1.0/annoscript/job_cross_version_tests.py +118 -0
- annoscript-0.1.0/annoscript/job_protocol.py +180 -0
- annoscript-0.1.0/annoscript/jobs.py +255 -0
- annoscript-0.1.0/annoscript/metrics.py +153 -0
- annoscript-0.1.0/annoscript/p0_grammar_tests.py +147 -0
- annoscript-0.1.0/annoscript/passes/__init__.py +40 -0
- annoscript-0.1.0/annoscript/passes/_analysis.py +178 -0
- annoscript-0.1.0/annoscript/passes/control_flow.py +68 -0
- annoscript-0.1.0/annoscript/passes/dead_assignments.py +44 -0
- annoscript-0.1.0/annoscript/passes/folding.py +104 -0
- annoscript-0.1.0/annoscript/passes/propagation.py +53 -0
- annoscript-0.1.0/annoscript/passes/temporaries.py +45 -0
- annoscript-0.1.0/annoscript/plan.py +542 -0
- annoscript-0.1.0/annoscript/plan_formatter.py +452 -0
- annoscript-0.1.0/annoscript/plan_lowering.py +571 -0
- annoscript-0.1.0/annoscript/preview.py +115 -0
- annoscript-0.1.0/annoscript/remote.py +304 -0
- annoscript-0.1.0/annoscript/runtime.py +861 -0
- annoscript-0.1.0/annoscript/server/__init__.py +1 -0
- annoscript-0.1.0/annoscript/server/__main__.py +49 -0
- annoscript-0.1.0/annoscript/server/_plan_cache.py +79 -0
- annoscript-0.1.0/annoscript/server/api.py +146 -0
- annoscript-0.1.0/annoscript/server/metrics.py +140 -0
- annoscript-0.1.0/annoscript/server/migrate.py +61 -0
- annoscript-0.1.0/annoscript/server/releases.py +143 -0
- annoscript-0.1.0/annoscript/server/store.py +429 -0
- annoscript-0.1.0/annoscript/server/worker.py +290 -0
- annoscript-0.1.0/annoscript/substring_tests.py +79 -0
- annoscript-0.1.0/annoscript/test_advisory_lengths.py +115 -0
- annoscript-0.1.0/annoscript/test_backends.py +181 -0
- annoscript-0.1.0/annoscript/test_client.py +349 -0
- annoscript-0.1.0/annoscript/test_compact_batch.py +95 -0
- annoscript-0.1.0/annoscript/test_compiler.py +444 -0
- annoscript-0.1.0/annoscript/test_decision_adapters.py +410 -0
- annoscript-0.1.0/annoscript/test_decision_wire.py +190 -0
- annoscript-0.1.0/annoscript/test_diagnostics.py +97 -0
- annoscript-0.1.0/annoscript/test_engineering_p0.py +191 -0
- annoscript-0.1.0/annoscript/test_environment.py +65 -0
- annoscript-0.1.0/annoscript/test_example_backends.py +251 -0
- annoscript-0.1.0/annoscript/test_examples.py +149 -0
- annoscript-0.1.0/annoscript/test_field_names.py +221 -0
- annoscript-0.1.0/annoscript/test_formatter.py +153 -0
- annoscript-0.1.0/annoscript/test_fusion_scheduling.py +109 -0
- annoscript-0.1.0/annoscript/test_generation_policy.py +180 -0
- annoscript-0.1.0/annoscript/test_ir.py +148 -0
- annoscript-0.1.0/annoscript/test_jobs.py +371 -0
- annoscript-0.1.0/annoscript/test_jobs_server.py +511 -0
- annoscript-0.1.0/annoscript/test_llm_batch.py +428 -0
- annoscript-0.1.0/annoscript/test_metrics.py +59 -0
- annoscript-0.1.0/annoscript/test_metrics_server.py +261 -0
- annoscript-0.1.0/annoscript/test_minimal_fusion.py +59 -0
- annoscript-0.1.0/annoscript/test_nullable_generate.py +169 -0
- annoscript-0.1.0/annoscript/test_passes.py +408 -0
- annoscript-0.1.0/annoscript/test_plan.py +522 -0
- annoscript-0.1.0/annoscript/test_plan_constraints.py +305 -0
- annoscript-0.1.0/annoscript/test_plan_formatter.py +272 -0
- annoscript-0.1.0/annoscript/test_prompt_layout.py +140 -0
- annoscript-0.1.0/annoscript/test_qwen_backend.py +181 -0
- annoscript-0.1.0/annoscript/test_qwen_pilot_regression.py +180 -0
- annoscript-0.1.0/annoscript/test_release_client.py +20 -0
- annoscript-0.1.0/annoscript/test_remote.py +308 -0
- annoscript-0.1.0/annoscript/test_request_adapters.py +295 -0
- annoscript-0.1.0/annoscript/test_requirements.py +228 -0
- annoscript-0.1.0/annoscript/test_rule_dedup.py +67 -0
- annoscript-0.1.0/annoscript/test_runtime.py +314 -0
- annoscript-0.1.0/annoscript/test_runtime_client.py +133 -0
- annoscript-0.1.0/annoscript/test_source_fields.py +111 -0
- annoscript-0.1.0/annoscript/test_speculation.py +458 -0
- annoscript-0.1.0/annoscript/test_substring_backend.py +81 -0
- annoscript-0.1.0/annoscript/test_synthetic_example.py +61 -0
- annoscript-0.1.0/annoscript_examples/__init__.py +1 -0
- annoscript-0.1.0/annoscript_examples/fixtures.py +3 -0
- annoscript-0.1.0/annoscript_examples/request.execution_plan.json +3017 -0
- annoscript-0.1.0/annoscript_examples/request.json +858 -0
- annoscript-0.1.0/annoscript_examples/request.py +37 -0
- annoscript-0.1.0/annoscript_examples/request_preview.decision.json +23 -0
- annoscript-0.1.0/annoscript_examples/request_preview.generation.json +20 -0
- annoscript-0.1.0/annoscript_examples/run.py +24 -0
- annoscript-0.1.0/design_api.md +38 -0
- annoscript-0.1.0/job_protocol.md +348 -0
- annoscript-0.1.0/motivation.md +18 -0
- annoscript-0.1.0/pyproject.toml +43 -0
- annoscript-0.1.0/remote_protocol.md +105 -0
- annoscript-0.1.0/requirements-p0-tokenizer.txt +6 -0
- annoscript-0.1.0/requirements-server.txt +3 -0
- annoscript-0.1.0/requirements-test-grammar.txt +3 -0
- annoscript-0.1.0/requirements.txt +1 -0
- annoscript-0.1.0/third_party/xgrammar/README.md +53 -0
- annoscript-0.1.0/third_party/xgrammar/pin.json +12 -0
- annoscript-0.1.0/third_party/xgrammar/substring-nonempty.patch +293 -0
- annoscript-0.1.0/third_party/xgrammar/tests/utf8_regression.py +118 -0
- annoscript-0.1.0/third_party/xgrammar/utf8-strict.patch +535 -0
annoscript-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 stevenybw
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: annoscript
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: AnnoScript: Programmable AI Annotation
|
|
5
|
+
Project-URL: Homepage, https://annoscript.chukonu.ai
|
|
6
|
+
Project-URL: Repository, https://github.com/stevenybw/annoscript
|
|
7
|
+
Project-URL: Issues, https://github.com/stevenybw/annoscript/issues
|
|
8
|
+
Author: stevenybw
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Requires-Dist: httpx<1,>=0.28
|
|
21
|
+
Provides-Extra: server
|
|
22
|
+
Requires-Dist: fastapi<1,>=0.128.1; extra == 'server'
|
|
23
|
+
Requires-Dist: uvicorn<1,>=0.40; extra == 'server'
|
|
24
|
+
Provides-Extra: tokenizer
|
|
25
|
+
Requires-Dist: transformers==5.17.0; extra == 'tokenizer'
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# AnnoScript: Programmable AI Annotation
|
|
29
|
+
|
|
30
|
+
AnnoScript compiles Python annotation programs into a serializable intermediate
|
|
31
|
+
representation and an execution plan. Decision operations and generation
|
|
32
|
+
operations can use separate model backends while sharing program state.
|
|
33
|
+
|
|
34
|
+
The client supports Python 3.10+. Runtime and server execution require Python 3.12+.
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install annoscript
|
|
40
|
+
# Local execution and the durable server (Python 3.12+):
|
|
41
|
+
pip install 'annoscript[server]'
|
|
42
|
+
# Optional pinned local tokenizer support:
|
|
43
|
+
pip install 'annoscript[tokenizer]'
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Python imports use `annoscript`, for example `from annoscript.client import AnnoClient`.
|
|
47
|
+
The eDSL context is `annoscript.anno.Anno`; the runtime and durable-job clients are
|
|
48
|
+
`AnnoRuntime`, `AnnoJobClient`, and `AnnoRemoteClient` in their corresponding modules.
|
|
49
|
+
This release replaces the former Lev Python namespace. Version-1 serialized IR,
|
|
50
|
+
execution plans, durable-job protocols and the `legacy` backend profile remain compatible.
|
|
51
|
+
|
|
52
|
+
## Synthetic example
|
|
53
|
+
|
|
54
|
+
`annoscript_examples.request.annotate` classifies a synthetic document, conditionally
|
|
55
|
+
generates a title, and produces tags and source spans. The example demonstrates
|
|
56
|
+
all six operators without requiring an application dataset.
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
python -m annoscript.inspect annoscript_examples/request.json --show-source
|
|
60
|
+
python -m annoscript.inspect annoscript_examples/request.execution_plan.json
|
|
61
|
+
python -m annoscript.inspect annoscript_examples/request.execution_plan.json --format html -o /tmp/lev-plan.html
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
To regenerate the saved example with Python 3.12:
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from pathlib import Path
|
|
68
|
+
from annoscript.compiler import compile_program
|
|
69
|
+
from annoscript.plan_lowering import lower_to_plan
|
|
70
|
+
from annoscript_examples.request import annotate
|
|
71
|
+
|
|
72
|
+
program = compile_program(annotate)
|
|
73
|
+
Path("annoscript_examples/request.json").write_text(program.to_json(indent=2) + "\n")
|
|
74
|
+
Path("annoscript_examples/request.execution_plan.json").write_text(lower_to_plan(program).to_json(indent=2) + "\n")
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Configuration
|
|
78
|
+
|
|
79
|
+
Install `requirements.txt` for the client and `requirements-server.txt` for
|
|
80
|
+
local execution or the durable job server. Copy `.credential.example` to an
|
|
81
|
+
ignored local file and supply the desired endpoints and model names.
|
|
82
|
+
Environment variables override values from an explicitly supplied environment file.
|
|
83
|
+
Canonical `ANNO_*` keys take precedence over legacy `LEV_*` aliases within the same
|
|
84
|
+
layer. Process environment values override file values regardless of their prefix;
|
|
85
|
+
explicit API arguments take precedence over both. Credentials stay outside packages.
|
|
86
|
+
|
|
87
|
+
| Variable | Purpose |
|
|
88
|
+
| --- | --- |
|
|
89
|
+
| `ANNO_DECISION_BASE_URL` | Decision provider URL |
|
|
90
|
+
| `ANNO_DECISION_AUTH_TOKEN` | Decision provider credential |
|
|
91
|
+
| `ANNO_DECISION_MODEL` | Decision model identifier |
|
|
92
|
+
| `ANNO_GENERATE_BASE_URL` | Generation provider URL |
|
|
93
|
+
| `ANNO_GENERATE_AUTH_TOKEN` | Generation provider credential |
|
|
94
|
+
| `ANNO_GENERATE_MODEL` | Generation model identifier |
|
|
95
|
+
| `ANNO_GENERATE_PROFILE` | `legacy` (default) or `qwen_vllm` wire protocol |
|
|
96
|
+
| `ANNO_GENERATE_TOKENIZER_DIR` | Caller-provided tokenizer directory when required by policy |
|
|
97
|
+
|
|
98
|
+
Keep live service settings and credentials outside version control.
|
|
99
|
+
The `legacy` profile name describes a supported request protocol.
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
python -m annoscript_examples.run --env-file .credential --inputs /tmp/documents.json --mode speculative
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
This command calls the caller's configured services. Inspecting IR, previewing
|
|
106
|
+
resolved requests and running the offline tests do not call model services.
|
|
107
|
+
|
|
108
|
+
## Execution and backends
|
|
109
|
+
|
|
110
|
+
Programs use `choice`, `noul`, `score`, `generate`, `generate_list` and `extract`.
|
|
111
|
+
`constrain` declares hard runtime constraints. `system_prompt` supplies shared
|
|
112
|
+
instructions; preconditions validate inputs locally before dispatch.
|
|
113
|
+
|
|
114
|
+
`AnnoClient` supports strict, eager and speculative execution. The runtime
|
|
115
|
+
handles bounded concurrency, batching, cancellation, repair and result ordering.
|
|
116
|
+
Examples, field names, nullable output and per-call requirements are included in
|
|
117
|
+
the IR and execution-plan bindings. Decision and generation request adapters
|
|
118
|
+
are explicitly selectable and can be inspected without dispatch.
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
python -m annoscript.preview annoscript_examples/request_preview.decision.json
|
|
122
|
+
python -m annoscript.preview annoscript_examples/request_preview.generation.json
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Remote execution exchanges serialized IR. Durable jobs bind an immutable
|
|
126
|
+
release to resumable, idempotent result retrieval. See [remote protocol](remote_protocol.md)
|
|
127
|
+
and [durable jobs](job_protocol.md) for request formats and error semantics.
|
|
128
|
+
|
|
129
|
+
## Offline verification
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
ANNO_RUN_LIVE_TESTS=0 python -m annoscript.client_tests -q
|
|
133
|
+
ANNO_RUN_LIVE_TESTS=0 python -m unittest discover -q
|
|
134
|
+
python -m annoscript.grammar_tests -v
|
|
135
|
+
python -m annoscript.cross_version_tests --client-python /path/to/python3.10 --server-python /path/to/python3.12
|
|
136
|
+
python -m annoscript.job_cross_version_tests --client-python /path/to/python3.10 --server-python /path/to/python3.12
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
The CPU grammar suite requires Python 3.12 and `requirements-test-grammar.txt`.
|
|
140
|
+
Cross-version checks use loopback HTTP and deterministic model doubles. They do
|
|
141
|
+
not require provider credentials or download models. Additional pinned decoder
|
|
142
|
+
acceptance instructions are in [third_party/xgrammar](third_party/xgrammar/README.md).
|
|
143
|
+
|
|
144
|
+
See [motivation](motivation.md) and [operator interfaces](design_api.md) for the
|
|
145
|
+
design rationale and API contracts.
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# AnnoScript: Programmable AI Annotation
|
|
2
|
+
|
|
3
|
+
AnnoScript compiles Python annotation programs into a serializable intermediate
|
|
4
|
+
representation and an execution plan. Decision operations and generation
|
|
5
|
+
operations can use separate model backends while sharing program state.
|
|
6
|
+
|
|
7
|
+
The client supports Python 3.10+. Runtime and server execution require Python 3.12+.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install annoscript
|
|
13
|
+
# Local execution and the durable server (Python 3.12+):
|
|
14
|
+
pip install 'annoscript[server]'
|
|
15
|
+
# Optional pinned local tokenizer support:
|
|
16
|
+
pip install 'annoscript[tokenizer]'
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Python imports use `annoscript`, for example `from annoscript.client import AnnoClient`.
|
|
20
|
+
The eDSL context is `annoscript.anno.Anno`; the runtime and durable-job clients are
|
|
21
|
+
`AnnoRuntime`, `AnnoJobClient`, and `AnnoRemoteClient` in their corresponding modules.
|
|
22
|
+
This release replaces the former Lev Python namespace. Version-1 serialized IR,
|
|
23
|
+
execution plans, durable-job protocols and the `legacy` backend profile remain compatible.
|
|
24
|
+
|
|
25
|
+
## Synthetic example
|
|
26
|
+
|
|
27
|
+
`annoscript_examples.request.annotate` classifies a synthetic document, conditionally
|
|
28
|
+
generates a title, and produces tags and source spans. The example demonstrates
|
|
29
|
+
all six operators without requiring an application dataset.
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
python -m annoscript.inspect annoscript_examples/request.json --show-source
|
|
33
|
+
python -m annoscript.inspect annoscript_examples/request.execution_plan.json
|
|
34
|
+
python -m annoscript.inspect annoscript_examples/request.execution_plan.json --format html -o /tmp/lev-plan.html
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
To regenerate the saved example with Python 3.12:
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from pathlib import Path
|
|
41
|
+
from annoscript.compiler import compile_program
|
|
42
|
+
from annoscript.plan_lowering import lower_to_plan
|
|
43
|
+
from annoscript_examples.request import annotate
|
|
44
|
+
|
|
45
|
+
program = compile_program(annotate)
|
|
46
|
+
Path("annoscript_examples/request.json").write_text(program.to_json(indent=2) + "\n")
|
|
47
|
+
Path("annoscript_examples/request.execution_plan.json").write_text(lower_to_plan(program).to_json(indent=2) + "\n")
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Configuration
|
|
51
|
+
|
|
52
|
+
Install `requirements.txt` for the client and `requirements-server.txt` for
|
|
53
|
+
local execution or the durable job server. Copy `.credential.example` to an
|
|
54
|
+
ignored local file and supply the desired endpoints and model names.
|
|
55
|
+
Environment variables override values from an explicitly supplied environment file.
|
|
56
|
+
Canonical `ANNO_*` keys take precedence over legacy `LEV_*` aliases within the same
|
|
57
|
+
layer. Process environment values override file values regardless of their prefix;
|
|
58
|
+
explicit API arguments take precedence over both. Credentials stay outside packages.
|
|
59
|
+
|
|
60
|
+
| Variable | Purpose |
|
|
61
|
+
| --- | --- |
|
|
62
|
+
| `ANNO_DECISION_BASE_URL` | Decision provider URL |
|
|
63
|
+
| `ANNO_DECISION_AUTH_TOKEN` | Decision provider credential |
|
|
64
|
+
| `ANNO_DECISION_MODEL` | Decision model identifier |
|
|
65
|
+
| `ANNO_GENERATE_BASE_URL` | Generation provider URL |
|
|
66
|
+
| `ANNO_GENERATE_AUTH_TOKEN` | Generation provider credential |
|
|
67
|
+
| `ANNO_GENERATE_MODEL` | Generation model identifier |
|
|
68
|
+
| `ANNO_GENERATE_PROFILE` | `legacy` (default) or `qwen_vllm` wire protocol |
|
|
69
|
+
| `ANNO_GENERATE_TOKENIZER_DIR` | Caller-provided tokenizer directory when required by policy |
|
|
70
|
+
|
|
71
|
+
Keep live service settings and credentials outside version control.
|
|
72
|
+
The `legacy` profile name describes a supported request protocol.
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
python -m annoscript_examples.run --env-file .credential --inputs /tmp/documents.json --mode speculative
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
This command calls the caller's configured services. Inspecting IR, previewing
|
|
79
|
+
resolved requests and running the offline tests do not call model services.
|
|
80
|
+
|
|
81
|
+
## Execution and backends
|
|
82
|
+
|
|
83
|
+
Programs use `choice`, `noul`, `score`, `generate`, `generate_list` and `extract`.
|
|
84
|
+
`constrain` declares hard runtime constraints. `system_prompt` supplies shared
|
|
85
|
+
instructions; preconditions validate inputs locally before dispatch.
|
|
86
|
+
|
|
87
|
+
`AnnoClient` supports strict, eager and speculative execution. The runtime
|
|
88
|
+
handles bounded concurrency, batching, cancellation, repair and result ordering.
|
|
89
|
+
Examples, field names, nullable output and per-call requirements are included in
|
|
90
|
+
the IR and execution-plan bindings. Decision and generation request adapters
|
|
91
|
+
are explicitly selectable and can be inspected without dispatch.
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
python -m annoscript.preview annoscript_examples/request_preview.decision.json
|
|
95
|
+
python -m annoscript.preview annoscript_examples/request_preview.generation.json
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Remote execution exchanges serialized IR. Durable jobs bind an immutable
|
|
99
|
+
release to resumable, idempotent result retrieval. See [remote protocol](remote_protocol.md)
|
|
100
|
+
and [durable jobs](job_protocol.md) for request formats and error semantics.
|
|
101
|
+
|
|
102
|
+
## Offline verification
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
ANNO_RUN_LIVE_TESTS=0 python -m annoscript.client_tests -q
|
|
106
|
+
ANNO_RUN_LIVE_TESTS=0 python -m unittest discover -q
|
|
107
|
+
python -m annoscript.grammar_tests -v
|
|
108
|
+
python -m annoscript.cross_version_tests --client-python /path/to/python3.10 --server-python /path/to/python3.12
|
|
109
|
+
python -m annoscript.job_cross_version_tests --client-python /path/to/python3.10 --server-python /path/to/python3.12
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
The CPU grammar suite requires Python 3.12 and `requirements-test-grammar.txt`.
|
|
113
|
+
Cross-version checks use loopback HTTP and deterministic model doubles. They do
|
|
114
|
+
not require provider credentials or download models. Additional pinned decoder
|
|
115
|
+
acceptance instructions are in [third_party/xgrammar](third_party/xgrammar/README.md).
|
|
116
|
+
|
|
117
|
+
See [motivation](motivation.md) and [operator interfaces](design_api.md) for the
|
|
118
|
+
design rationale and API contracts.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""AnnoScript's Python eDSL, compiler, execution plans and API-backed runtime."""
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Generation wire profiles shared by configuration readers; no backend imports."""
|
|
2
|
+
|
|
3
|
+
GENERATION_PROFILES = ("legacy", "qwen_vllm")
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def validate_extract_mode(mode, profile):
|
|
7
|
+
if type(mode) is not str or mode not in ("validated", "substring_chars"):
|
|
8
|
+
raise ValueError("extract_mode must be validated or substring_chars")
|
|
9
|
+
if mode == "substring_chars" and profile != "qwen_vllm":
|
|
10
|
+
raise ValueError("substring_chars requires qwen_vllm")
|
|
11
|
+
return mode
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def validate_generation_profile(profile):
|
|
15
|
+
if type(profile) is not str or profile not in GENERATION_PROFILES:
|
|
16
|
+
raise ValueError("generation profile must be legacy or qwen_vllm")
|
|
17
|
+
return profile
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"""Python 3.10+ contracts shared by local and remote clients; no execution imports."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from collections.abc import Callable, Iterator
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from types import FunctionType, TracebackType
|
|
9
|
+
from typing import Generic, List, Literal, TypeAlias, TypeVar
|
|
10
|
+
from weakref import WeakKeyDictionary
|
|
11
|
+
|
|
12
|
+
from annoscript.anno import Anno
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
JSONValue: TypeAlias = (
|
|
16
|
+
str | int | float | bool | None | list["JSONValue"] | dict[str, "JSONValue"]
|
|
17
|
+
)
|
|
18
|
+
State: TypeAlias = str | list[JSONValue] | dict[str, JSONValue]
|
|
19
|
+
R = TypeVar("R", bound=JSONValue)
|
|
20
|
+
InputCheck: TypeAlias = Callable[[State], None]
|
|
21
|
+
|
|
22
|
+
# Keep local checks outside the program function's attributes and execution body.
|
|
23
|
+
_PRECONDITIONS: WeakKeyDictionary[
|
|
24
|
+
Callable[[Anno], JSONValue], tuple[InputCheck, ...]
|
|
25
|
+
] = WeakKeyDictionary()
|
|
26
|
+
# Only this registry's text values are exported as remote program configuration.
|
|
27
|
+
_SYSTEM_PROMPTS: WeakKeyDictionary[Callable[[Anno], JSONValue], str] = WeakKeyDictionary()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class InputValidationError(ValueError):
|
|
31
|
+
"""An input failed a client-local precondition, before remote submission."""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def require(condition: bool, *, message: str) -> None:
|
|
35
|
+
"""Check an input condition locally; raise InputValidationError on failure.
|
|
36
|
+
|
|
37
|
+
This ordinary Python function remains active under optimization. It never
|
|
38
|
+
invokes inference, repairs inputs, or propagates constraints to the eDSL.
|
|
39
|
+
"""
|
|
40
|
+
if not condition:
|
|
41
|
+
raise InputValidationError(message)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def precondition(
|
|
45
|
+
check: InputCheck,
|
|
46
|
+
) -> Callable[[Callable[[Anno], R]], Callable[[Anno], R]]:
|
|
47
|
+
"""Register a client-local input check and return the original program.
|
|
48
|
+
|
|
49
|
+
Checks are synchronous, deterministic, read-only Python functions. Return
|
|
50
|
+
None for success and raise InputValidationError for an invalid input. Other
|
|
51
|
+
exceptions indicate a checker defect and abort submission. Checks must not
|
|
52
|
+
invoke models or make network requests.
|
|
53
|
+
|
|
54
|
+
Registration does not run checks or wrap the program. Direct program calls
|
|
55
|
+
do not run checks; AnnoClient implementations run them before any remote
|
|
56
|
+
submission. Stacked decorators are checked in source order (top to bottom),
|
|
57
|
+
stopping at the first failed check for each input.
|
|
58
|
+
|
|
59
|
+
The registry is local only. Remote program packaging must exclude these
|
|
60
|
+
decorator declarations, check functions, and their validation dependencies.
|
|
61
|
+
"""
|
|
62
|
+
if not callable(check):
|
|
63
|
+
raise TypeError("precondition requires a callable input check")
|
|
64
|
+
|
|
65
|
+
def register(program: Callable[[Anno], R]) -> Callable[[Anno], R]:
|
|
66
|
+
if not isinstance(program, FunctionType):
|
|
67
|
+
raise TypeError("precondition must decorate a Python function")
|
|
68
|
+
_PRECONDITIONS[program] = (check,) + _PRECONDITIONS.get(program, ())
|
|
69
|
+
return program
|
|
70
|
+
|
|
71
|
+
return register
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def system_prompt(
|
|
75
|
+
prompt: str,
|
|
76
|
+
) -> Callable[[Callable[[Anno], R]], Callable[[Anno], R]]:
|
|
77
|
+
"""Declare shared model instructions and return the original program.
|
|
78
|
+
|
|
79
|
+
The client exports this text as program configuration. The runtime binds it
|
|
80
|
+
to every record's injected AnnoScript and applies it to LLM operations, including
|
|
81
|
+
calls inside helper functions. Decision requests use their own instructions
|
|
82
|
+
and requirements. API guarantees and hard constraints remain independently
|
|
83
|
+
enforced. Mapping shared instructions to a model is the runtime's job.
|
|
84
|
+
|
|
85
|
+
Registration neither wraps nor executes the program and creates no context.
|
|
86
|
+
Direct Python calls do not inject the prompt. Each program may declare one
|
|
87
|
+
system prompt; any repeated declaration raises ValueError. Empty strings
|
|
88
|
+
are allowed and preserved. Prompt text is sent as configuration, without
|
|
89
|
+
executing the decorator remotely or exporting local precondition checks.
|
|
90
|
+
"""
|
|
91
|
+
if not isinstance(prompt, str):
|
|
92
|
+
raise TypeError("system_prompt requires a string")
|
|
93
|
+
|
|
94
|
+
def register(program: Callable[[Anno], R]) -> Callable[[Anno], R]:
|
|
95
|
+
if not isinstance(program, FunctionType):
|
|
96
|
+
raise TypeError("system_prompt must decorate a Python function")
|
|
97
|
+
if program in _SYSTEM_PROMPTS:
|
|
98
|
+
raise ValueError("a program may declare only one system_prompt")
|
|
99
|
+
_SYSTEM_PROMPTS[program] = prompt
|
|
100
|
+
return program
|
|
101
|
+
|
|
102
|
+
return register
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@dataclass(frozen=True)
|
|
106
|
+
class ResultError:
|
|
107
|
+
"""A per-input failure, including local validation or unsatisfied constraints."""
|
|
108
|
+
|
|
109
|
+
code: str
|
|
110
|
+
message: str
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@dataclass(frozen=True)
|
|
114
|
+
class Result(Generic[R]):
|
|
115
|
+
"""One terminal outcome, identified by its zero-based input index.
|
|
116
|
+
|
|
117
|
+
Success preserves the program's return value, including ``None``. An error
|
|
118
|
+
carries no value. A list-valued result stays one result, not multiple items.
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
index: int
|
|
122
|
+
status: Literal["success", "error"]
|
|
123
|
+
value: R | None = None
|
|
124
|
+
error: ResultError | None = None
|
|
125
|
+
|
|
126
|
+
def __post_init__(self) -> None:
|
|
127
|
+
if type(self.index) is not int or self.index < 0:
|
|
128
|
+
raise ValueError("index must be a nonnegative integer")
|
|
129
|
+
if self.status == "success":
|
|
130
|
+
if self.error is not None:
|
|
131
|
+
raise ValueError("a successful result cannot contain an error")
|
|
132
|
+
elif self.status == "error":
|
|
133
|
+
if not isinstance(self.error, ResultError) or self.value is not None:
|
|
134
|
+
raise ValueError("an error result requires ResultError and no value")
|
|
135
|
+
else:
|
|
136
|
+
raise ValueError("status must be 'success' or 'error'")
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class ResultStream(Iterator[Result[R]], ABC):
|
|
140
|
+
"""A single-pass result iterator with explicit resource ownership.
|
|
141
|
+
|
|
142
|
+
Use ``with`` when stopping early: breaking a plain ``for`` loop does not
|
|
143
|
+
close the stream. Exhaustion and iteration errors automatically close it.
|
|
144
|
+
Concrete transports implement ``_next_result`` and ``_close`` and call
|
|
145
|
+
``super().__init__()`` when overriding construction.
|
|
146
|
+
|
|
147
|
+
Streams are intended for consumption and closure by a single thread.
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
def __init__(self) -> None:
|
|
151
|
+
self._closed = False
|
|
152
|
+
|
|
153
|
+
@property
|
|
154
|
+
def closed(self) -> bool:
|
|
155
|
+
return self._closed
|
|
156
|
+
|
|
157
|
+
def __iter__(self) -> ResultStream[R]:
|
|
158
|
+
return self
|
|
159
|
+
|
|
160
|
+
def __next__(self) -> Result[R]:
|
|
161
|
+
if self._closed:
|
|
162
|
+
raise StopIteration
|
|
163
|
+
try:
|
|
164
|
+
return self._next_result()
|
|
165
|
+
except StopIteration:
|
|
166
|
+
self.close()
|
|
167
|
+
raise
|
|
168
|
+
except BaseException:
|
|
169
|
+
try:
|
|
170
|
+
self.close()
|
|
171
|
+
except BaseException:
|
|
172
|
+
import logging
|
|
173
|
+
logging.getLogger(__name__).warning("Result cleanup failed while handling another exception")
|
|
174
|
+
raise
|
|
175
|
+
|
|
176
|
+
def close(self) -> None:
|
|
177
|
+
"""Stop receiving results and release resources; repeated calls are harmless.
|
|
178
|
+
|
|
179
|
+
Closing discards undelivered results. No further work may be submitted.
|
|
180
|
+
The transport should request cancellation of pending
|
|
181
|
+
work, without promising that already-running remote work can be stopped.
|
|
182
|
+
The stream stays closed even if transport cleanup raises an exception.
|
|
183
|
+
"""
|
|
184
|
+
if not self._closed:
|
|
185
|
+
self._closed = True
|
|
186
|
+
self._close()
|
|
187
|
+
|
|
188
|
+
def __enter__(self) -> ResultStream[R]:
|
|
189
|
+
if self._closed:
|
|
190
|
+
raise RuntimeError("cannot enter a closed result stream")
|
|
191
|
+
return self
|
|
192
|
+
|
|
193
|
+
def __exit__(
|
|
194
|
+
self,
|
|
195
|
+
exc_type: type[BaseException] | None,
|
|
196
|
+
exc_value: BaseException | None,
|
|
197
|
+
traceback: TracebackType | None,
|
|
198
|
+
) -> Literal[False]:
|
|
199
|
+
if exc_type is None:
|
|
200
|
+
self.close()
|
|
201
|
+
else:
|
|
202
|
+
try:
|
|
203
|
+
self.close()
|
|
204
|
+
except BaseException:
|
|
205
|
+
import logging
|
|
206
|
+
logging.getLogger(__name__).warning("Result cleanup failed while handling another exception")
|
|
207
|
+
return False
|
|
208
|
+
|
|
209
|
+
@abstractmethod
|
|
210
|
+
def _next_result(self) -> Result[R]:
|
|
211
|
+
"""Return the next result, or raise StopIteration when fully exhausted.
|
|
212
|
+
|
|
213
|
+
Return per-input failures as error results. Raise submission-level or
|
|
214
|
+
transport failures as exceptions; the stream will then close itself.
|
|
215
|
+
"""
|
|
216
|
+
raise NotImplementedError
|
|
217
|
+
|
|
218
|
+
@abstractmethod
|
|
219
|
+
def _close(self) -> None:
|
|
220
|
+
"""Release local resources and request cancellation, at most once.
|
|
221
|
+
|
|
222
|
+
This hook must not submit new work or wait for unfinished work to
|
|
223
|
+
complete. It is also called after normal exhaustion or iteration errors.
|
|
224
|
+
"""
|
|
225
|
+
raise NotImplementedError
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def get_program_config(program: Callable[[Anno], R]) -> dict[str, JSONValue]:
|
|
229
|
+
"""Return fresh, JSON-compatible execution settings for remote submission.
|
|
230
|
+
|
|
231
|
+
Export system-prompt text only, never local check functions or rules.
|
|
232
|
+
Programs without a declaration have no system_prompt configuration key.
|
|
233
|
+
Reading configuration does not execute the program or its preconditions.
|
|
234
|
+
"""
|
|
235
|
+
prompt = _SYSTEM_PROMPTS.get(program)
|
|
236
|
+
return {} if prompt is None else {"system_prompt": prompt}
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def validate_inputs(
|
|
240
|
+
program: Callable[[Anno], R], inputs: List[State]
|
|
241
|
+
) -> dict[int, ResultError]:
|
|
242
|
+
"""Run the whole batch's preconditions locally, without running program.
|
|
243
|
+
|
|
244
|
+
Call before any remote submission. Return invalid-input errors keyed by
|
|
245
|
+
original list index; the remaining indices passed all registered checks.
|
|
246
|
+
Only InputValidationError becomes an item error. Unexpected exceptions
|
|
247
|
+
and non-None check returns abort validation and must prevent submission.
|
|
248
|
+
|
|
249
|
+
This helper validates the outer list and program-specific preconditions.
|
|
250
|
+
Concrete clients also validate the generic JSON input contract.
|
|
251
|
+
"""
|
|
252
|
+
if not isinstance(inputs, list):
|
|
253
|
+
raise TypeError("inputs must be a list")
|
|
254
|
+
checks = _PRECONDITIONS.get(program, ())
|
|
255
|
+
errors = {}
|
|
256
|
+
for index, state in enumerate(inputs):
|
|
257
|
+
try:
|
|
258
|
+
for check in checks:
|
|
259
|
+
if check(state) is not None:
|
|
260
|
+
raise TypeError("precondition checks must return None on success")
|
|
261
|
+
except InputValidationError as error:
|
|
262
|
+
errors[index] = ResultError(code="invalid_input", message=str(error))
|
|
263
|
+
return errors
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def prepare_inputs(program, inputs):
|
|
267
|
+
"""Snapshot JSON inputs, then check all valid shapes before any submission.
|
|
268
|
+
|
|
269
|
+
Return (indexed states, local errors), retaining the original input indices.
|
|
270
|
+
Unexpected checker exceptions propagate before either client dispatches work.
|
|
271
|
+
"""
|
|
272
|
+
from annoscript._data import snapshot
|
|
273
|
+
|
|
274
|
+
if not isinstance(inputs, list):
|
|
275
|
+
raise TypeError("inputs must be a list")
|
|
276
|
+
errors, valid = {}, []
|
|
277
|
+
for index, state in enumerate(inputs):
|
|
278
|
+
try:
|
|
279
|
+
valid.append((index, snapshot(state, state=True)))
|
|
280
|
+
except (ValueError, RecursionError):
|
|
281
|
+
errors[index] = ResultError(
|
|
282
|
+
"invalid_input",
|
|
283
|
+
"input must contain finite JSON data and have string, array or object shape",
|
|
284
|
+
)
|
|
285
|
+
checked = validate_inputs(program, [state for _, state in valid])
|
|
286
|
+
errors.update({valid[i][0]: error for i, error in checked.items()})
|
|
287
|
+
return [item for i, item in enumerate(valid) if i not in checked], errors
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""JSON snapshots and immutable inference payloads; no application code runs here."""
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
from types import MappingProxyType
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def snapshot(value, path="$", *, state=False):
|
|
9
|
+
if state and type(value) not in (str, list, dict):
|
|
10
|
+
raise ValueError(f"{path}: input must be a string, list or object")
|
|
11
|
+
if value is None or type(value) in (str, bool, int):
|
|
12
|
+
return value
|
|
13
|
+
if type(value) is float and math.isfinite(value):
|
|
14
|
+
return value
|
|
15
|
+
if type(value) is list:
|
|
16
|
+
return [snapshot(v, f"{path}[{i}]") for i, v in enumerate(value)]
|
|
17
|
+
if type(value) is dict:
|
|
18
|
+
if any(type(k) is not str for k in value):
|
|
19
|
+
raise ValueError(f"{path}: object keys must be strings")
|
|
20
|
+
return {k: snapshot(v, f"{path}.{k}") for k, v in value.items()}
|
|
21
|
+
raise ValueError(f"{path}: expected finite JSON data")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def freeze(value):
|
|
25
|
+
if isinstance(value, Mapping):
|
|
26
|
+
return MappingProxyType({k: freeze(v) for k, v in value.items()})
|
|
27
|
+
if isinstance(value, (list, tuple)):
|
|
28
|
+
return tuple(freeze(v) for v in value)
|
|
29
|
+
return value
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def thaw(value):
|
|
33
|
+
if isinstance(value, Mapping):
|
|
34
|
+
return {k: thaw(v) for k, v in value.items()}
|
|
35
|
+
if isinstance(value, tuple):
|
|
36
|
+
return [thaw(v) for v in value]
|
|
37
|
+
return value
|