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.
Files changed (142) hide show
  1. annoscript-0.1.0/.gitignore +13 -0
  2. annoscript-0.1.0/LICENSE +21 -0
  3. annoscript-0.1.0/PKG-INFO +145 -0
  4. annoscript-0.1.0/README.md +118 -0
  5. annoscript-0.1.0/annoscript/__init__.py +1 -0
  6. annoscript-0.1.0/annoscript/_backend_profiles.py +17 -0
  7. annoscript-0.1.0/annoscript/_client_common.py +287 -0
  8. annoscript-0.1.0/annoscript/_data.py +37 -0
  9. annoscript-0.1.0/annoscript/_dispatch_pool.py +47 -0
  10. annoscript-0.1.0/annoscript/_env.py +19 -0
  11. annoscript-0.1.0/annoscript/_examples.py +34 -0
  12. annoscript-0.1.0/annoscript/_expressions.py +93 -0
  13. annoscript-0.1.0/annoscript/_plan_viewer.html +953 -0
  14. annoscript-0.1.0/annoscript/_remote_jobs.py +211 -0
  15. annoscript-0.1.0/annoscript/_request_adapters.py +71 -0
  16. annoscript-0.1.0/annoscript/_speculation.py +321 -0
  17. annoscript-0.1.0/annoscript/_test_artifacts.py +33 -0
  18. annoscript-0.1.0/annoscript/_test_evaluation_http.py +117 -0
  19. annoscript-0.1.0/annoscript/_test_ir_runtime.py +138 -0
  20. annoscript-0.1.0/annoscript/_test_job_process.py +166 -0
  21. annoscript-0.1.0/annoscript/_test_plan_runtime.py +331 -0
  22. annoscript-0.1.0/annoscript/_test_remote_client.py +46 -0
  23. annoscript-0.1.0/annoscript/_test_remote_server.py +75 -0
  24. annoscript-0.1.0/annoscript/_test_synthetic.py +67 -0
  25. annoscript-0.1.0/annoscript/anno.py +270 -0
  26. annoscript-0.1.0/annoscript/backends/__init__.py +13 -0
  27. annoscript-0.1.0/annoscript/backends/_batch_wire.py +26 -0
  28. annoscript-0.1.0/annoscript/backends/_bounded_grammar.py +97 -0
  29. annoscript-0.1.0/annoscript/backends/_example_layout.py +23 -0
  30. annoscript-0.1.0/annoscript/backends/_generation_policy.py +116 -0
  31. annoscript-0.1.0/annoscript/backends/_grammar.py +83 -0
  32. annoscript-0.1.0/annoscript/backends/_preview.py +13 -0
  33. annoscript-0.1.0/annoscript/backends/_prompt.py +115 -0
  34. annoscript-0.1.0/annoscript/backends/_shared_rules.py +38 -0
  35. annoscript-0.1.0/annoscript/backends/adapters/__init__.py +17 -0
  36. annoscript-0.1.0/annoscript/backends/adapters/chat.py +39 -0
  37. annoscript-0.1.0/annoscript/backends/adapters/decision.py +119 -0
  38. annoscript-0.1.0/annoscript/backends/base.py +427 -0
  39. annoscript-0.1.0/annoscript/backends/config.py +67 -0
  40. annoscript-0.1.0/annoscript/backends/decision.py +150 -0
  41. annoscript-0.1.0/annoscript/backends/llm.py +338 -0
  42. annoscript-0.1.0/annoscript/client.py +247 -0
  43. annoscript-0.1.0/annoscript/client_tests.py +17 -0
  44. annoscript-0.1.0/annoscript/compiler.py +708 -0
  45. annoscript-0.1.0/annoscript/cross_version_tests.py +68 -0
  46. annoscript-0.1.0/annoscript/fixtures/request-adapters-v1.json +4741 -0
  47. annoscript-0.1.0/annoscript/formatter.py +171 -0
  48. annoscript-0.1.0/annoscript/grammar_tests.py +176 -0
  49. annoscript-0.1.0/annoscript/inspect.py +77 -0
  50. annoscript-0.1.0/annoscript/ir.py +458 -0
  51. annoscript-0.1.0/annoscript/job_cross_version_tests.py +118 -0
  52. annoscript-0.1.0/annoscript/job_protocol.py +180 -0
  53. annoscript-0.1.0/annoscript/jobs.py +255 -0
  54. annoscript-0.1.0/annoscript/metrics.py +153 -0
  55. annoscript-0.1.0/annoscript/p0_grammar_tests.py +147 -0
  56. annoscript-0.1.0/annoscript/passes/__init__.py +40 -0
  57. annoscript-0.1.0/annoscript/passes/_analysis.py +178 -0
  58. annoscript-0.1.0/annoscript/passes/control_flow.py +68 -0
  59. annoscript-0.1.0/annoscript/passes/dead_assignments.py +44 -0
  60. annoscript-0.1.0/annoscript/passes/folding.py +104 -0
  61. annoscript-0.1.0/annoscript/passes/propagation.py +53 -0
  62. annoscript-0.1.0/annoscript/passes/temporaries.py +45 -0
  63. annoscript-0.1.0/annoscript/plan.py +542 -0
  64. annoscript-0.1.0/annoscript/plan_formatter.py +452 -0
  65. annoscript-0.1.0/annoscript/plan_lowering.py +571 -0
  66. annoscript-0.1.0/annoscript/preview.py +115 -0
  67. annoscript-0.1.0/annoscript/remote.py +304 -0
  68. annoscript-0.1.0/annoscript/runtime.py +861 -0
  69. annoscript-0.1.0/annoscript/server/__init__.py +1 -0
  70. annoscript-0.1.0/annoscript/server/__main__.py +49 -0
  71. annoscript-0.1.0/annoscript/server/_plan_cache.py +79 -0
  72. annoscript-0.1.0/annoscript/server/api.py +146 -0
  73. annoscript-0.1.0/annoscript/server/metrics.py +140 -0
  74. annoscript-0.1.0/annoscript/server/migrate.py +61 -0
  75. annoscript-0.1.0/annoscript/server/releases.py +143 -0
  76. annoscript-0.1.0/annoscript/server/store.py +429 -0
  77. annoscript-0.1.0/annoscript/server/worker.py +290 -0
  78. annoscript-0.1.0/annoscript/substring_tests.py +79 -0
  79. annoscript-0.1.0/annoscript/test_advisory_lengths.py +115 -0
  80. annoscript-0.1.0/annoscript/test_backends.py +181 -0
  81. annoscript-0.1.0/annoscript/test_client.py +349 -0
  82. annoscript-0.1.0/annoscript/test_compact_batch.py +95 -0
  83. annoscript-0.1.0/annoscript/test_compiler.py +444 -0
  84. annoscript-0.1.0/annoscript/test_decision_adapters.py +410 -0
  85. annoscript-0.1.0/annoscript/test_decision_wire.py +190 -0
  86. annoscript-0.1.0/annoscript/test_diagnostics.py +97 -0
  87. annoscript-0.1.0/annoscript/test_engineering_p0.py +191 -0
  88. annoscript-0.1.0/annoscript/test_environment.py +65 -0
  89. annoscript-0.1.0/annoscript/test_example_backends.py +251 -0
  90. annoscript-0.1.0/annoscript/test_examples.py +149 -0
  91. annoscript-0.1.0/annoscript/test_field_names.py +221 -0
  92. annoscript-0.1.0/annoscript/test_formatter.py +153 -0
  93. annoscript-0.1.0/annoscript/test_fusion_scheduling.py +109 -0
  94. annoscript-0.1.0/annoscript/test_generation_policy.py +180 -0
  95. annoscript-0.1.0/annoscript/test_ir.py +148 -0
  96. annoscript-0.1.0/annoscript/test_jobs.py +371 -0
  97. annoscript-0.1.0/annoscript/test_jobs_server.py +511 -0
  98. annoscript-0.1.0/annoscript/test_llm_batch.py +428 -0
  99. annoscript-0.1.0/annoscript/test_metrics.py +59 -0
  100. annoscript-0.1.0/annoscript/test_metrics_server.py +261 -0
  101. annoscript-0.1.0/annoscript/test_minimal_fusion.py +59 -0
  102. annoscript-0.1.0/annoscript/test_nullable_generate.py +169 -0
  103. annoscript-0.1.0/annoscript/test_passes.py +408 -0
  104. annoscript-0.1.0/annoscript/test_plan.py +522 -0
  105. annoscript-0.1.0/annoscript/test_plan_constraints.py +305 -0
  106. annoscript-0.1.0/annoscript/test_plan_formatter.py +272 -0
  107. annoscript-0.1.0/annoscript/test_prompt_layout.py +140 -0
  108. annoscript-0.1.0/annoscript/test_qwen_backend.py +181 -0
  109. annoscript-0.1.0/annoscript/test_qwen_pilot_regression.py +180 -0
  110. annoscript-0.1.0/annoscript/test_release_client.py +20 -0
  111. annoscript-0.1.0/annoscript/test_remote.py +308 -0
  112. annoscript-0.1.0/annoscript/test_request_adapters.py +295 -0
  113. annoscript-0.1.0/annoscript/test_requirements.py +228 -0
  114. annoscript-0.1.0/annoscript/test_rule_dedup.py +67 -0
  115. annoscript-0.1.0/annoscript/test_runtime.py +314 -0
  116. annoscript-0.1.0/annoscript/test_runtime_client.py +133 -0
  117. annoscript-0.1.0/annoscript/test_source_fields.py +111 -0
  118. annoscript-0.1.0/annoscript/test_speculation.py +458 -0
  119. annoscript-0.1.0/annoscript/test_substring_backend.py +81 -0
  120. annoscript-0.1.0/annoscript/test_synthetic_example.py +61 -0
  121. annoscript-0.1.0/annoscript_examples/__init__.py +1 -0
  122. annoscript-0.1.0/annoscript_examples/fixtures.py +3 -0
  123. annoscript-0.1.0/annoscript_examples/request.execution_plan.json +3017 -0
  124. annoscript-0.1.0/annoscript_examples/request.json +858 -0
  125. annoscript-0.1.0/annoscript_examples/request.py +37 -0
  126. annoscript-0.1.0/annoscript_examples/request_preview.decision.json +23 -0
  127. annoscript-0.1.0/annoscript_examples/request_preview.generation.json +20 -0
  128. annoscript-0.1.0/annoscript_examples/run.py +24 -0
  129. annoscript-0.1.0/design_api.md +38 -0
  130. annoscript-0.1.0/job_protocol.md +348 -0
  131. annoscript-0.1.0/motivation.md +18 -0
  132. annoscript-0.1.0/pyproject.toml +43 -0
  133. annoscript-0.1.0/remote_protocol.md +105 -0
  134. annoscript-0.1.0/requirements-p0-tokenizer.txt +6 -0
  135. annoscript-0.1.0/requirements-server.txt +3 -0
  136. annoscript-0.1.0/requirements-test-grammar.txt +3 -0
  137. annoscript-0.1.0/requirements.txt +1 -0
  138. annoscript-0.1.0/third_party/xgrammar/README.md +53 -0
  139. annoscript-0.1.0/third_party/xgrammar/pin.json +12 -0
  140. annoscript-0.1.0/third_party/xgrammar/substring-nonempty.patch +293 -0
  141. annoscript-0.1.0/third_party/xgrammar/tests/utf8_regression.py +118 -0
  142. annoscript-0.1.0/third_party/xgrammar/utf8-strict.patch +535 -0
@@ -0,0 +1,13 @@
1
+ .credential
2
+ __pycache__/
3
+ *.py[cod]
4
+ .venv/
5
+ lev-live-report*.json
6
+ .env
7
+ .env.*
8
+ *.pem
9
+ *.key
10
+ credential.sh
11
+ dist/
12
+ build/
13
+ *.egg-info/
@@ -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