annoscript 0.1.0__py3-none-any.whl
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/__init__.py +1 -0
- annoscript/_backend_profiles.py +17 -0
- annoscript/_client_common.py +287 -0
- annoscript/_data.py +37 -0
- annoscript/_dispatch_pool.py +47 -0
- annoscript/_env.py +19 -0
- annoscript/_examples.py +34 -0
- annoscript/_expressions.py +93 -0
- annoscript/_plan_viewer.html +953 -0
- annoscript/_remote_jobs.py +211 -0
- annoscript/_request_adapters.py +71 -0
- annoscript/_speculation.py +321 -0
- annoscript/_test_artifacts.py +33 -0
- annoscript/_test_evaluation_http.py +117 -0
- annoscript/_test_ir_runtime.py +138 -0
- annoscript/_test_job_process.py +166 -0
- annoscript/_test_plan_runtime.py +331 -0
- annoscript/_test_remote_client.py +46 -0
- annoscript/_test_remote_server.py +75 -0
- annoscript/_test_synthetic.py +67 -0
- annoscript/anno.py +270 -0
- annoscript/backends/__init__.py +13 -0
- annoscript/backends/_batch_wire.py +26 -0
- annoscript/backends/_bounded_grammar.py +97 -0
- annoscript/backends/_example_layout.py +23 -0
- annoscript/backends/_generation_policy.py +116 -0
- annoscript/backends/_grammar.py +83 -0
- annoscript/backends/_preview.py +13 -0
- annoscript/backends/_prompt.py +115 -0
- annoscript/backends/_shared_rules.py +38 -0
- annoscript/backends/adapters/__init__.py +17 -0
- annoscript/backends/adapters/chat.py +39 -0
- annoscript/backends/adapters/decision.py +119 -0
- annoscript/backends/base.py +427 -0
- annoscript/backends/config.py +67 -0
- annoscript/backends/decision.py +150 -0
- annoscript/backends/llm.py +338 -0
- annoscript/client.py +247 -0
- annoscript/client_tests.py +17 -0
- annoscript/compiler.py +708 -0
- annoscript/cross_version_tests.py +68 -0
- annoscript/fixtures/request-adapters-v1.json +4741 -0
- annoscript/formatter.py +171 -0
- annoscript/grammar_tests.py +176 -0
- annoscript/inspect.py +77 -0
- annoscript/ir.py +458 -0
- annoscript/job_cross_version_tests.py +118 -0
- annoscript/job_protocol.py +180 -0
- annoscript/jobs.py +255 -0
- annoscript/metrics.py +153 -0
- annoscript/p0_grammar_tests.py +147 -0
- annoscript/passes/__init__.py +40 -0
- annoscript/passes/_analysis.py +178 -0
- annoscript/passes/control_flow.py +68 -0
- annoscript/passes/dead_assignments.py +44 -0
- annoscript/passes/folding.py +104 -0
- annoscript/passes/propagation.py +53 -0
- annoscript/passes/temporaries.py +45 -0
- annoscript/plan.py +542 -0
- annoscript/plan_formatter.py +452 -0
- annoscript/plan_lowering.py +571 -0
- annoscript/preview.py +115 -0
- annoscript/remote.py +304 -0
- annoscript/runtime.py +861 -0
- annoscript/server/__init__.py +1 -0
- annoscript/server/__main__.py +49 -0
- annoscript/server/_plan_cache.py +79 -0
- annoscript/server/api.py +146 -0
- annoscript/server/metrics.py +140 -0
- annoscript/server/migrate.py +61 -0
- annoscript/server/releases.py +143 -0
- annoscript/server/store.py +429 -0
- annoscript/server/worker.py +290 -0
- annoscript/substring_tests.py +79 -0
- annoscript/test_advisory_lengths.py +115 -0
- annoscript/test_backends.py +181 -0
- annoscript/test_client.py +349 -0
- annoscript/test_compact_batch.py +95 -0
- annoscript/test_compiler.py +444 -0
- annoscript/test_decision_adapters.py +410 -0
- annoscript/test_decision_wire.py +190 -0
- annoscript/test_diagnostics.py +97 -0
- annoscript/test_engineering_p0.py +191 -0
- annoscript/test_environment.py +65 -0
- annoscript/test_example_backends.py +251 -0
- annoscript/test_examples.py +149 -0
- annoscript/test_field_names.py +221 -0
- annoscript/test_formatter.py +153 -0
- annoscript/test_fusion_scheduling.py +109 -0
- annoscript/test_generation_policy.py +180 -0
- annoscript/test_ir.py +148 -0
- annoscript/test_jobs.py +371 -0
- annoscript/test_jobs_server.py +511 -0
- annoscript/test_llm_batch.py +428 -0
- annoscript/test_metrics.py +59 -0
- annoscript/test_metrics_server.py +261 -0
- annoscript/test_minimal_fusion.py +59 -0
- annoscript/test_nullable_generate.py +169 -0
- annoscript/test_passes.py +408 -0
- annoscript/test_plan.py +522 -0
- annoscript/test_plan_constraints.py +305 -0
- annoscript/test_plan_formatter.py +272 -0
- annoscript/test_prompt_layout.py +140 -0
- annoscript/test_qwen_backend.py +181 -0
- annoscript/test_qwen_pilot_regression.py +180 -0
- annoscript/test_release_client.py +20 -0
- annoscript/test_remote.py +308 -0
- annoscript/test_request_adapters.py +295 -0
- annoscript/test_requirements.py +228 -0
- annoscript/test_rule_dedup.py +67 -0
- annoscript/test_runtime.py +314 -0
- annoscript/test_runtime_client.py +133 -0
- annoscript/test_source_fields.py +111 -0
- annoscript/test_speculation.py +458 -0
- annoscript/test_substring_backend.py +81 -0
- annoscript/test_synthetic_example.py +61 -0
- annoscript-0.1.0.dist-info/METADATA +145 -0
- annoscript-0.1.0.dist-info/RECORD +128 -0
- annoscript-0.1.0.dist-info/WHEEL +4 -0
- annoscript-0.1.0.dist-info/licenses/LICENSE +21 -0
- annoscript_examples/__init__.py +1 -0
- annoscript_examples/fixtures.py +3 -0
- annoscript_examples/request.execution_plan.json +3017 -0
- annoscript_examples/request.json +858 -0
- annoscript_examples/request.py +37 -0
- annoscript_examples/request_preview.decision.json +23 -0
- annoscript_examples/request_preview.generation.json +20 -0
- annoscript_examples/run.py +24 -0
annoscript/anno.py
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Any, List, Dict
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from annoscript._examples import Example
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def constrain(condition: bool, *, message: str | None = None) -> None:
|
|
9
|
+
"""Declare a hard constraint in a compiled AnnoScript eDSL program.
|
|
10
|
+
|
|
11
|
+
The compiler captures the pure ``condition`` expression and its dependencies
|
|
12
|
+
before Python evaluates the arguments. It may propagate the constraint to
|
|
13
|
+
related inference operations whose results are still adjustable within the
|
|
14
|
+
current solving scope. A constraint inside a branch applies only on that
|
|
15
|
+
execution path.
|
|
16
|
+
|
|
17
|
+
Successful execution past this call requires the condition to hold, as
|
|
18
|
+
established by proof or runtime validation. If a satisfying result cannot
|
|
19
|
+
be obtained, execution must report failure; retry and repair budgets are
|
|
20
|
+
execution-policy concerns. ``message`` optionally describes the constraint
|
|
21
|
+
for failure diagnostics. Constraints remain active in optimized execution.
|
|
22
|
+
|
|
23
|
+
This function is an eDSL intrinsic, not a standalone solver or validator.
|
|
24
|
+
Direct Python calls always raise ``RuntimeError``, even for a true condition:
|
|
25
|
+
the evaluated argument no longer carries the expression needed to propagate
|
|
26
|
+
constraints. The eDSL compiler must lower this call to a constraint operation.
|
|
27
|
+
"""
|
|
28
|
+
raise RuntimeError(
|
|
29
|
+
"constrain() requires a compiled AnnoScript eDSL program; "
|
|
30
|
+
"direct Python execution cannot propagate constraints."
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class NoulResponse:
|
|
36
|
+
noul: float
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class ChoiceResponse:
|
|
41
|
+
"""Choice result; the provider guarantees finite confidence in [0, 1]."""
|
|
42
|
+
|
|
43
|
+
choice: str
|
|
44
|
+
confidence: float
|
|
45
|
+
probabilities: Dict[str, float]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class ScoreResponse:
|
|
50
|
+
probabilities: Dict[int, float]
|
|
51
|
+
score: float
|
|
52
|
+
legend: Dict[int, str]
|
|
53
|
+
confidence: float
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class ExtractResponse:
|
|
58
|
+
items: List[str]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class GenerateResponse:
|
|
63
|
+
text: str | None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class GenerateListResponse:
|
|
68
|
+
items: List[str]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class Anno(ABC):
|
|
72
|
+
"""Decision, extraction, and generation operations sharing execution context.
|
|
73
|
+
|
|
74
|
+
The runtime supplies the program's system_prompt when constructing the
|
|
75
|
+
per-record instance. The LLM backend applies it alongside per-call task
|
|
76
|
+
instructions, including calls from helpers. Decision requests use only their
|
|
77
|
+
task instructions and requirements. Neither overrides API guarantees or hard
|
|
78
|
+
constraints.
|
|
79
|
+
|
|
80
|
+
All operations accept ``examples``: a list of Example values (or their
|
|
81
|
+
serialized state/answer objects). None and [] mean no examples. Examples
|
|
82
|
+
inherit this call's instructions, criteria and constraints, and are checked
|
|
83
|
+
before dispatch. They teach task semantics, not facts about the current state.
|
|
84
|
+
|
|
85
|
+
All operations accept ``requirements``: a list of nonblank strings describing
|
|
86
|
+
additional task instructions. None and [] mean no extra requirements. LLM
|
|
87
|
+
requests group identical strings and name only the tasks they apply to;
|
|
88
|
+
Decision questions include their own requirements in the task instructions.
|
|
89
|
+
Requirements do not replace API guarantees or hard constraints.
|
|
90
|
+
|
|
91
|
+
All operations accept ``field_name``: a nonblank string used as the
|
|
92
|
+
public task name, including question and batch answer keys. None preserves automatic
|
|
93
|
+
names. Names must be distinct within a physical request, including automatic
|
|
94
|
+
aliases; internal call IDs and response types remain unchanged.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
def __init__(
|
|
98
|
+
self,
|
|
99
|
+
state: str | Dict[str, Any] | List[Any],
|
|
100
|
+
model: str,
|
|
101
|
+
*,
|
|
102
|
+
system_prompt: str | None = None,
|
|
103
|
+
):
|
|
104
|
+
self.state = state
|
|
105
|
+
self.model = model
|
|
106
|
+
self.system_prompt = system_prompt
|
|
107
|
+
|
|
108
|
+
@abstractmethod
|
|
109
|
+
def noul(
|
|
110
|
+
self,
|
|
111
|
+
instructions: str,
|
|
112
|
+
criteria_false: str | None = None,
|
|
113
|
+
criteria_true: str | None = None,
|
|
114
|
+
*,
|
|
115
|
+
examples: list[Example] | None = None,
|
|
116
|
+
requirements: list[str] | None = None,
|
|
117
|
+
field_name: str | None = None,
|
|
118
|
+
) -> NoulResponse:
|
|
119
|
+
"""Evaluate a yes/no question using this instance's state and model."""
|
|
120
|
+
raise NotImplementedError
|
|
121
|
+
|
|
122
|
+
@abstractmethod
|
|
123
|
+
def choice(self, instructions: str, criteria: Dict[str, str], *,
|
|
124
|
+
examples: list[Example] | None = None,
|
|
125
|
+
requirements: list[str] | None = None,
|
|
126
|
+
field_name: str | None = None) -> ChoiceResponse:
|
|
127
|
+
"""Select an option using this instance's state and model.
|
|
128
|
+
|
|
129
|
+
A successful result must have ``result.choice in criteria`` and a finite
|
|
130
|
+
``result.confidence`` in the inclusive range [0, 1], excluding NaN and
|
|
131
|
+
infinities. Implementations must enforce these guarantees and report
|
|
132
|
+
failure if a valid result cannot be produced.
|
|
133
|
+
"""
|
|
134
|
+
raise NotImplementedError
|
|
135
|
+
|
|
136
|
+
@abstractmethod
|
|
137
|
+
def score(self, instructions: str, criteria: List[str], *,
|
|
138
|
+
examples: list[Example] | None = None,
|
|
139
|
+
requirements: list[str] | None = None,
|
|
140
|
+
field_name: str | None = None) -> ScoreResponse:
|
|
141
|
+
"""Score the state against ordered criteria using this instance's model."""
|
|
142
|
+
raise NotImplementedError
|
|
143
|
+
|
|
144
|
+
@abstractmethod
|
|
145
|
+
def extract(
|
|
146
|
+
self,
|
|
147
|
+
instructions: str,
|
|
148
|
+
*,
|
|
149
|
+
source_field: str | None = None,
|
|
150
|
+
min_items: int = 0,
|
|
151
|
+
max_items: int,
|
|
152
|
+
max_length_per_item: int,
|
|
153
|
+
max_num_tokens: int | None = None,
|
|
154
|
+
examples: list[Example] | None = None,
|
|
155
|
+
field_name: str | None = None,
|
|
156
|
+
requirements: list[str] | None = None,
|
|
157
|
+
) -> ExtractResponse:
|
|
158
|
+
"""Extract verbatim passages using this instance's state and model.
|
|
159
|
+
|
|
160
|
+
``source_field`` names a top-level string field in an object state,
|
|
161
|
+
such as ``source_field="text"``. It is a literal key, not a nested path.
|
|
162
|
+
String states require ``source_field=None`` and are extracted directly.
|
|
163
|
+
Object states require an explicit field; list states are unsupported.
|
|
164
|
+
Other state fields may provide context, but are not extraction sources.
|
|
165
|
+
|
|
166
|
+
Each item must be a nonempty, contiguous substring of the source, without
|
|
167
|
+
rewriting, normalization, or concatenation of separate passages. The
|
|
168
|
+
number of items must satisfy the inclusive ``min_items`` / ``max_items``
|
|
169
|
+
bounds. Required ``max_length_per_item`` is a suggested Unicode code-point
|
|
170
|
+
length, measured by Python's ``len``, and contributes to the token budget
|
|
171
|
+
unless a positive ``max_num_tokens`` overrides it (including format overhead).
|
|
172
|
+
In a batch this budget contributes to the shared limit.
|
|
173
|
+
Exceeding the suggestion does not invalidate an otherwise valid item.
|
|
174
|
+
Return an empty list when nothing matches and ``min_items`` is zero.
|
|
175
|
+
|
|
176
|
+
Implementations must raise ``ValueError`` for a missing, invalid or
|
|
177
|
+
non-string field, negative item bounds, an inverted item range, or a
|
|
178
|
+
nonpositive ``max_length_per_item``. If extraction cannot satisfy the
|
|
179
|
+
constraints, report failure rather than inventing items or returning a
|
|
180
|
+
successful result that violates the bounds. Matching the source does
|
|
181
|
+
not by itself establish that an item semantically supports a claim.
|
|
182
|
+
"""
|
|
183
|
+
raise NotImplementedError
|
|
184
|
+
|
|
185
|
+
@abstractmethod
|
|
186
|
+
def generate(
|
|
187
|
+
self,
|
|
188
|
+
instructions: str,
|
|
189
|
+
*,
|
|
190
|
+
min_length: int = 0,
|
|
191
|
+
max_length: int,
|
|
192
|
+
max_num_tokens: int | None = None,
|
|
193
|
+
examples: list[Example] | None = None,
|
|
194
|
+
field_name: str | None = None,
|
|
195
|
+
nullable: bool = False,
|
|
196
|
+
requirements: list[str] | None = None,
|
|
197
|
+
) -> GenerateResponse:
|
|
198
|
+
"""Generate text using this instance's state and model.
|
|
199
|
+
|
|
200
|
+
The result is a string by default. ``nullable=True`` additionally allows
|
|
201
|
+
``None`` (JSON null), without converting the literal string "null".
|
|
202
|
+
Nullable standalone calls and repairs return a JSON string or null scalar;
|
|
203
|
+
ordinary standalone calls remain raw text. Batches retain their answers
|
|
204
|
+
wrapper. The result may paraphrase or synthesize the state as directed by
|
|
205
|
+
``instructions``. ``min_length`` and
|
|
206
|
+
required ``max_length`` are suggested Unicode code-point lengths, measured by
|
|
207
|
+
Python's ``len``. A positive ``min_length`` requires nonempty non-null text,
|
|
208
|
+
but other deviations from the suggested range are not failures.
|
|
209
|
+
|
|
210
|
+
``max_num_tokens`` is an independent output limit for a standalone
|
|
211
|
+
generation. When Runtime batches ready calls, it contributes to a shared
|
|
212
|
+
budget and is not an enforced per-answer token limit. ``None`` uses the
|
|
213
|
+
client's assumed_avg_num_tokens_per_char (default 2) times max_length,
|
|
214
|
+
rounded up with a minimum of one token. Suggested lengths are not hard bounds.
|
|
215
|
+
Exhausting the budget must not silently turn an incomplete generation
|
|
216
|
+
into a successful response.
|
|
217
|
+
|
|
218
|
+
Implementations must raise ``ValueError`` for negative length bounds,
|
|
219
|
+
an inverted length range, a nonpositive ``max_num_tokens``, or a
|
|
220
|
+
non-boolean ``nullable``. If valid
|
|
221
|
+
output cannot be produced, report failure rather than returning a
|
|
222
|
+
successful result that violates the constraints. These constraints do
|
|
223
|
+
not guarantee factual accuracy. Branching and recovery remain the
|
|
224
|
+
caller's responsibility.
|
|
225
|
+
"""
|
|
226
|
+
raise NotImplementedError
|
|
227
|
+
|
|
228
|
+
@abstractmethod
|
|
229
|
+
def generate_list(
|
|
230
|
+
self,
|
|
231
|
+
instructions: str,
|
|
232
|
+
*,
|
|
233
|
+
min_items: int = 0,
|
|
234
|
+
max_items: int,
|
|
235
|
+
min_length_per_item: int = 0,
|
|
236
|
+
max_length_per_item: int,
|
|
237
|
+
max_num_tokens: int | None = None,
|
|
238
|
+
examples: list[Example] | None = None,
|
|
239
|
+
field_name: str | None = None,
|
|
240
|
+
requirements: list[str] | None = None,
|
|
241
|
+
) -> GenerateListResponse:
|
|
242
|
+
"""Generate a list of texts using this instance's state and model.
|
|
243
|
+
|
|
244
|
+
The result is always a list of strings, with no ``None`` items. Each
|
|
245
|
+
item may paraphrase or synthesize the state as directed by
|
|
246
|
+
``instructions``. The item count must satisfy the inclusive
|
|
247
|
+
``min_items`` / ``max_items`` bounds; an empty list is allowed when
|
|
248
|
+
``min_items=0``.
|
|
249
|
+
|
|
250
|
+
``min_length_per_item`` and required ``max_length_per_item`` are suggested
|
|
251
|
+
Unicode code-point lengths for each string, measured by Python's ``len``.
|
|
252
|
+
A positive minimum still requires nonempty items. Required ``max_items``
|
|
253
|
+
is a hard count limit. Neither required maximum accepts None.
|
|
254
|
+
|
|
255
|
+
``max_num_tokens`` is a generation budget for the entire list,
|
|
256
|
+
including formatting, in the model's tokens. In a Runtime batch it
|
|
257
|
+
contributes to a shared budget, without an independent per-list token
|
|
258
|
+
limit. ``None`` estimates ceil(assumed_avg_num_tokens_per_char * max_items *
|
|
259
|
+
max_length_per_item) + 32 + 8 * max_items. Extraction uses the same
|
|
260
|
+
formula. Item count and nonempty requirements remain strict. Exhausting the budget must not silently
|
|
261
|
+
turn an incomplete list or item into a successful response.
|
|
262
|
+
|
|
263
|
+
Implementations must raise ``ValueError`` for negative item or length
|
|
264
|
+
bounds, inverted ranges, or a nonpositive ``max_num_tokens``. If valid
|
|
265
|
+
output cannot be produced, report failure rather than returning a
|
|
266
|
+
successful result that violates the constraints. These constraints do
|
|
267
|
+
not guarantee factual accuracy. Branching and recovery remain the
|
|
268
|
+
caller's responsibility.
|
|
269
|
+
"""
|
|
270
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""API adapters. Batching and dependency scheduling belong to AnnoRuntime."""
|
|
2
|
+
|
|
3
|
+
from .base import (
|
|
4
|
+
BatchResult, LLMBatchPlan, DecisionBatchPlan, InferenceCall, InferenceContext, InferenceOutput,
|
|
5
|
+
InferenceError, BackendFatalError,
|
|
6
|
+
)
|
|
7
|
+
from .decision import DecisionAPIBackend
|
|
8
|
+
from .llm import LLMAPIBackend
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"BatchResult", "LLMBatchPlan", "DecisionBatchPlan", "InferenceCall", "InferenceContext", "InferenceOutput",
|
|
12
|
+
"InferenceError", "BackendFatalError", "DecisionAPIBackend", "LLMAPIBackend",
|
|
13
|
+
]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Request-local names for model APIs and compact LLM batch responses."""
|
|
2
|
+
|
|
3
|
+
FORMAT = "short_ids_values_v1"
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def wire_name(call, fallback):
|
|
7
|
+
"""Resolve an optional public name without replacing runtime identity."""
|
|
8
|
+
name = call.arguments.get("field_name")
|
|
9
|
+
if name is None:
|
|
10
|
+
return fallback
|
|
11
|
+
if type(name) is not str or not name.strip():
|
|
12
|
+
raise ValueError("field_name must be a nonblank string or None")
|
|
13
|
+
return name
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def batch_calls(calls):
|
|
17
|
+
"""Map wire IDs to unchanged calls; aliases never enter runtime identity."""
|
|
18
|
+
if len({call.id for call in calls}) != len(calls):
|
|
19
|
+
raise ValueError("a batch requires distinct calls")
|
|
20
|
+
identified = {}
|
|
21
|
+
for index, call in enumerate(calls, 1):
|
|
22
|
+
name = wire_name(call, f"q{index}")
|
|
23
|
+
if name in identified:
|
|
24
|
+
raise ValueError(f"duplicate model-visible field name: {name!r}")
|
|
25
|
+
identified[name] = call
|
|
26
|
+
return identified
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Hard structure/count/nonempty constraints; advisory code-point lengths, with source-isolated Substring rules."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
from .base import InferenceError
|
|
6
|
+
from ._batch_wire import batch_calls
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# Keep a complete JSON code point in one lexical FSM: a repeated independent
|
|
10
|
+
# json-char rule makes every text token context-sensitive in XGrammar.
|
|
11
|
+
JSON_ATOM = r'(?:[^"\\\x00-\x1f]|\\["\\/bfnrt]|\\u(?:[0-9a-cA-Ce-fE-F][0-9a-fA-F]{3}|[dD][0-7][0-9a-fA-F]{2})|\\u[dD][89aAbB][0-9a-fA-F]{2}\\u[dD][c-fC-F][0-9a-fA-F]{2})'
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def json_body(nonempty):
|
|
15
|
+
return "Regex(" + json.dumps(JSON_ATOM + ("+" if nonempty else "*")) + ")"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def literal(value):
|
|
19
|
+
return json.dumps(value, ensure_ascii=False)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def repeat(rule, minimum, maximum):
|
|
23
|
+
if maximum == 0:
|
|
24
|
+
return '""'
|
|
25
|
+
return rule + ('{' + str(minimum) + ',' + ('' if maximum is None else str(maximum)) + '}')
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def ensure_satisfiable(call):
|
|
29
|
+
a = call.arguments
|
|
30
|
+
if call.operation == "extract" and not call.extraction_source and a.get("min_items", 0) > 0:
|
|
31
|
+
raise InferenceError("empty extraction source cannot satisfy min_items",
|
|
32
|
+
code="unsatisfiable_constraints", call_id=call.id,
|
|
33
|
+
metadata={"dispatch_status": "not_sent"})
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def grammar(calls, *, batch=False, extract_mode="validated"):
|
|
37
|
+
definitions = []
|
|
38
|
+
cache = {}
|
|
39
|
+
|
|
40
|
+
def value_rule(call):
|
|
41
|
+
ensure_satisfiable(call)
|
|
42
|
+
a = call.arguments
|
|
43
|
+
key = (call.operation, call.extraction_source, a.get("min_items", 0), a.get("max_items"),
|
|
44
|
+
bool(a.get("min_length", 0)), bool(a.get("min_length_per_item", 0)),
|
|
45
|
+
bool(a.get("nullable", False)))
|
|
46
|
+
if key in cache:
|
|
47
|
+
return cache[key]
|
|
48
|
+
prefix = 'value-' + str(len(cache))
|
|
49
|
+
cache[key] = prefix
|
|
50
|
+
if call.operation == "generate":
|
|
51
|
+
body = json_body(bool(a.get('min_length', 0)))
|
|
52
|
+
string = '"\\\"" ' + body + ' "\\\""'
|
|
53
|
+
value = '(' + string + ' | "null")' if a.get("nullable", False) else string
|
|
54
|
+
definitions.append(prefix + ' ::= ' + value)
|
|
55
|
+
return prefix
|
|
56
|
+
if call.operation == "extract" and not call.extraction_source:
|
|
57
|
+
definitions.append(prefix + ' ::= "[]"')
|
|
58
|
+
return prefix
|
|
59
|
+
item = prefix + '-string'
|
|
60
|
+
if call.operation == "extract" and extract_mode == "substring_chars":
|
|
61
|
+
chunks = [literal(json.dumps(c, ensure_ascii=False)[1:-1]) for c in call.extraction_source]
|
|
62
|
+
options = ', allow_empty=false'
|
|
63
|
+
body = prefix + '-body'
|
|
64
|
+
definitions.append(body + ' ::= Substring(' + ', '.join(chunks) + options + ')')
|
|
65
|
+
else:
|
|
66
|
+
minimum = 1 if call.operation == "extract" else a.get('min_length_per_item', 0)
|
|
67
|
+
body = json_body(bool(minimum))
|
|
68
|
+
definitions.append(item + ' ::= "\\\"" ' + body + ' "\\\""')
|
|
69
|
+
low, high = a.get('min_items', 0), a.get('max_items')
|
|
70
|
+
if high == 0:
|
|
71
|
+
interior = '""'
|
|
72
|
+
else:
|
|
73
|
+
tail = repeat('(\",\" ' + item + ')', max(0, low - 1), None if high is None else high - 1)
|
|
74
|
+
interior = item + ' ' + tail
|
|
75
|
+
if low == 0:
|
|
76
|
+
interior = '(' + interior + ')?'
|
|
77
|
+
definitions.append(prefix + ' ::= "[" ' + interior + ' "]"')
|
|
78
|
+
return prefix
|
|
79
|
+
|
|
80
|
+
if batch:
|
|
81
|
+
parts = [literal('{"answers":{')]
|
|
82
|
+
for i, (identity, call) in enumerate(batch_calls(calls).items()):
|
|
83
|
+
prefix = (',' if i else '') + json.dumps(identity) + ':'
|
|
84
|
+
parts.extend([literal(prefix), value_rule(call)])
|
|
85
|
+
parts.append(literal('}}'))
|
|
86
|
+
root = ' '.join(parts)
|
|
87
|
+
else:
|
|
88
|
+
call, = calls
|
|
89
|
+
ensure_satisfiable(call)
|
|
90
|
+
if call.operation == 'generate' and not call.arguments.get("nullable", False):
|
|
91
|
+
# Standalone generate remains raw text, with no JSON wrapper.
|
|
92
|
+
root = 'Regex(' + json.dumps(r'[\s\S]' + ('+' if call.arguments.get('min_length', 0) else '*')) + ')'
|
|
93
|
+
elif call.operation == 'generate':
|
|
94
|
+
root = value_rule(call)
|
|
95
|
+
else:
|
|
96
|
+
root = literal('{"items":') + ' ' + value_rule(call) + ' ' + literal('}')
|
|
97
|
+
return 'root ::= ' + root + '\n' + '\n'.join(definitions)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Request-local example sharing; never modifies the current inference state."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
EXAMPLE_CONTRACT = (
|
|
7
|
+
"examples 是任务的输入与正确答案示范,只用于理解规则,不是当前 state 的事实。"
|
|
8
|
+
"answer 是该算子的值,不包含置信度;当前输出仍遵守指定格式。"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def group_examples(tasks):
|
|
13
|
+
"""Group inputs by first occurrence; retain every answer, including repeats."""
|
|
14
|
+
groups = {}
|
|
15
|
+
for identity, task in tasks.items():
|
|
16
|
+
for example in task["task"].get("examples", []):
|
|
17
|
+
state = example["state"]
|
|
18
|
+
key = json.dumps(state, ensure_ascii=False, allow_nan=False,
|
|
19
|
+
sort_keys=True, separators=(",", ":"))
|
|
20
|
+
if key not in groups:
|
|
21
|
+
groups[key] = {"state": state, "answers": []}
|
|
22
|
+
groups[key]["answers"].append((identity, example["answer"]))
|
|
23
|
+
return list(groups.values())
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Opt-in, release-bound Qwen request limits. No network or credential access."""
|
|
2
|
+
|
|
3
|
+
from functools import lru_cache
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
import math
|
|
7
|
+
import os
|
|
8
|
+
from annoscript._env import env_value
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from .base import BackendFatalError
|
|
12
|
+
from annoscript._data import thaw
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
SAMPLING = {"temperature": 0, "top_p": 1, "top_k": -1, "min_p": 0,
|
|
16
|
+
"repetition_penalty": 1, "presence_penalty": 0, "frequency_penalty": 0}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def validate_policy(value):
|
|
20
|
+
if value is None:
|
|
21
|
+
return None
|
|
22
|
+
keys = {"sampling", "default_max_tokens", "context_window", "tokenizer", "bounded_outputs"}
|
|
23
|
+
if type(value) is not dict or set(value) != keys:
|
|
24
|
+
raise ValueError("invalid generation request_policy")
|
|
25
|
+
sampling = value["sampling"]
|
|
26
|
+
if (type(sampling) is not dict or set(sampling) != set(SAMPLING)
|
|
27
|
+
or any(type(v) not in (int, float) or not math.isfinite(v) for v in sampling.values())
|
|
28
|
+
or sampling != SAMPLING):
|
|
29
|
+
raise ValueError("P0 requires explicit greedy sampling without additional penalties")
|
|
30
|
+
for name in ("default_max_tokens", "context_window"):
|
|
31
|
+
if type(value[name]) is not int or value[name] < 1:
|
|
32
|
+
raise ValueError("generation limits must be positive integers")
|
|
33
|
+
if value["default_max_tokens"] > value["context_window"] or value["bounded_outputs"] is not True:
|
|
34
|
+
raise ValueError("invalid bounded generation policy")
|
|
35
|
+
binding = value["tokenizer"]
|
|
36
|
+
if (type(binding) is not dict or set(binding) != {"revision", "files"}
|
|
37
|
+
or type(binding["revision"]) is not str or not binding["revision"]
|
|
38
|
+
or type(binding["files"]) is not dict
|
|
39
|
+
or not {"tokenizer.json", "tokenizer_config.json", "chat_template.jinja"} <= set(binding["files"])):
|
|
40
|
+
raise ValueError("tokenizer needs an immutable revision and file digests")
|
|
41
|
+
for name, digest in binding["files"].items():
|
|
42
|
+
if (type(name) is not str or Path(name).name != name or name in (".", "..")
|
|
43
|
+
or type(digest) is not str or len(digest) != 64
|
|
44
|
+
or any(c not in "0123456789abcdef" for c in digest)):
|
|
45
|
+
raise ValueError("invalid tokenizer file binding")
|
|
46
|
+
return json.loads(json.dumps(value))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@lru_cache(maxsize=4)
|
|
50
|
+
def _load_tokenizer(directory, serialized_binding):
|
|
51
|
+
binding = json.loads(serialized_binding)
|
|
52
|
+
root = Path(directory)
|
|
53
|
+
for name, digest in binding["files"].items():
|
|
54
|
+
if hashlib.sha256((root / name).read_bytes()).hexdigest() != digest:
|
|
55
|
+
raise ValueError("tokenizer artifact digest mismatch")
|
|
56
|
+
from transformers import AutoTokenizer
|
|
57
|
+
tokenizer = AutoTokenizer.from_pretrained(root, local_files_only=True, trust_remote_code=False)
|
|
58
|
+
if tokenizer.chat_template != (root / "chat_template.jinja").read_text():
|
|
59
|
+
raise ValueError("loaded chat template differs from bound artifact")
|
|
60
|
+
# Validate the template engine at startup, before any task can send HTTP.
|
|
61
|
+
tokenizer.apply_chat_template([{"role": "system", "content": ""},
|
|
62
|
+
{"role": "user", "content": "startup check"}],
|
|
63
|
+
tokenize=True, return_dict=False, add_generation_prompt=True, enable_thinking=False)
|
|
64
|
+
return tokenizer
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class PinnedTokenCounter:
|
|
68
|
+
def __init__(self, binding):
|
|
69
|
+
try:
|
|
70
|
+
directory = env_value("ANNO_GENERATE_TOKENIZER_DIR")
|
|
71
|
+
self.tokenizer = _load_tokenizer(directory, json.dumps(binding, sort_keys=True))
|
|
72
|
+
except Exception:
|
|
73
|
+
raise BackendFatalError("pinned tokenizer is unavailable or mismatched",
|
|
74
|
+
code="configuration_failed") from None
|
|
75
|
+
|
|
76
|
+
def __call__(self, payload):
|
|
77
|
+
try:
|
|
78
|
+
ids = self.tokenizer.apply_chat_template(
|
|
79
|
+
payload["messages"], tokenize=True, return_dict=False, add_generation_prompt=True,
|
|
80
|
+
**payload.get("chat_template_kwargs", {}))
|
|
81
|
+
if type(ids) is not list or any(type(token) is not int for token in ids):
|
|
82
|
+
raise ValueError("template must return one token ID sequence")
|
|
83
|
+
return len(ids)
|
|
84
|
+
except Exception:
|
|
85
|
+
raise BackendFatalError("bound chat template could not tokenize request",
|
|
86
|
+
code="configuration_failed") from None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def validate_token_ratio(value):
|
|
90
|
+
if type(value) not in (int, float) or not math.isfinite(value) or value <= 0:
|
|
91
|
+
raise ValueError("assumed_avg_num_tokens_per_char must be finite and positive")
|
|
92
|
+
return value
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def effective_budget(call, assumed_avg_num_tokens_per_char=2, override=None):
|
|
96
|
+
"""One rule for direct, runtime, batch and repair budgets; no hidden cap."""
|
|
97
|
+
ratio = validate_token_ratio(assumed_avg_num_tokens_per_char)
|
|
98
|
+
a = call.arguments
|
|
99
|
+
# Direct backend callers also pass through the public argument validator.
|
|
100
|
+
from .base import normalize_arguments
|
|
101
|
+
normalize_arguments(call.operation, (), thaw(a))
|
|
102
|
+
if call.operation == "extract" and type(call.extraction_source) is not str:
|
|
103
|
+
raise ValueError("extract call requires a normalized source_field binding")
|
|
104
|
+
explicit = a.get("max_num_tokens")
|
|
105
|
+
if explicit is not None:
|
|
106
|
+
budget = explicit
|
|
107
|
+
elif call.operation == "generate":
|
|
108
|
+
budget = max(1, math.ceil(ratio * a["max_length"]))
|
|
109
|
+
else:
|
|
110
|
+
count = a["max_items"]
|
|
111
|
+
budget = math.ceil(ratio * count * a["max_length_per_item"]) + 32 + 8 * count
|
|
112
|
+
if override is not None:
|
|
113
|
+
if type(override) is not int or override < 1:
|
|
114
|
+
raise ValueError("invalid token override")
|
|
115
|
+
budget = min(budget, override)
|
|
116
|
+
return budget
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Compact JSON structure for list operations, independent of business contracts.
|
|
2
|
+
|
|
3
|
+
JSON syntax and batch ID/value type mapping are constrained here. Runtime validates
|
|
4
|
+
decoded strings, item counts, nonempty requirements and verbatim extraction.
|
|
5
|
+
The grammar is source, not generated from the car example or a third-party repo.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
from ._batch_wire import batch_calls
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# Keep the string continuation in the same right-recursive rule. Repeating a
|
|
13
|
+
# separate `character` rule is language-equivalent but makes xgrammar 0.2.3
|
|
14
|
+
# classify tokens against the surrounding parser stack at every decode step
|
|
15
|
+
# with the Qwen3.5 vocabulary. It can dominate decoding and cause HTTP timeouts.
|
|
16
|
+
# This spelling preserves JSON escapes, Unicode and empty strings; it neither
|
|
17
|
+
# narrows extraction semantics nor changes any business/token budget.
|
|
18
|
+
STRING_RULES = r'''
|
|
19
|
+
string ::= "\"" string-body
|
|
20
|
+
string-body ::= "\"" | [^"\\\x00-\x1f] string-body | "\\" escape string-body
|
|
21
|
+
escape ::= ["\\/bfnrt] | "u" hex hex hex hex
|
|
22
|
+
hex ::= [0-9a-fA-F]
|
|
23
|
+
'''.strip()
|
|
24
|
+
|
|
25
|
+
ITEMS_GRAMMAR = r'''root ::= "{\"items\":[" (string ("," string)*)? "]}"''' + "\n" + STRING_RULES
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
BATCH_GRAMMAR = r'''
|
|
29
|
+
root ::= "{\"answers\":{" (answer ("," answer)*)? "}}"
|
|
30
|
+
answer ::= string ":" (string | items)
|
|
31
|
+
items ::= "[" (string ("," string)*)? "]"
|
|
32
|
+
'''.strip() + "\n" + STRING_RULES
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _extract_rules(source, prefix):
|
|
36
|
+
"""Atomic JSON encodings preserve code-point boundaries after JSON decoding.
|
|
37
|
+
|
|
38
|
+
Requires the pinned XGrammar allow_empty extension. No Unicode normalization
|
|
39
|
+
or union of sources is permitted. Runtime still checks counts and lengths.
|
|
40
|
+
"""
|
|
41
|
+
items, body = prefix + "-items", prefix + "-body"
|
|
42
|
+
if not source:
|
|
43
|
+
return items, [items + ' ::= "[]"']
|
|
44
|
+
chunks = [json.dumps(json.dumps(char, ensure_ascii=False)[1:-1], ensure_ascii=False)
|
|
45
|
+
for char in source]
|
|
46
|
+
string = prefix + "-string"
|
|
47
|
+
return items, [
|
|
48
|
+
body + " ::= Substring(" + ", ".join(chunks) + ", allow_empty=false)",
|
|
49
|
+
string + ' ::= "\\\"" ' + body + ' "\\\""',
|
|
50
|
+
items + ' ::= "[" (' + string + ' ("," ' + string + ')*)? "]"',
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def extract_grammar(source):
|
|
55
|
+
items, rules = _extract_rules(source, "extract")
|
|
56
|
+
return 'root ::= "{\\\"items\\\":" ' + items + ' "}"\n' + "\n".join(rules)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def batch_grammar(calls, *, extract_mode="validated"):
|
|
60
|
+
"""Bind every requested ID to its operation, once, in request order.
|
|
61
|
+
|
|
62
|
+
A generic union allows an extract ID to produce a string, even though
|
|
63
|
+
that answer is valid JSON. The response parser still validates arbitrary
|
|
64
|
+
object ordering and all constraints when the provider ignores the grammar.
|
|
65
|
+
"""
|
|
66
|
+
parts = [json.dumps('{"answers":{')]
|
|
67
|
+
sources, rules = {}, []
|
|
68
|
+
for index, (identity, call) in enumerate(batch_calls(calls).items()):
|
|
69
|
+
prefix = ("," if index else "") + json.dumps(identity) + ':'
|
|
70
|
+
value_rule = "string" if call.operation == "generate" else "items"
|
|
71
|
+
if call.operation == "generate" and call.arguments.get("nullable", False):
|
|
72
|
+
value_rule = '(string | "null")'
|
|
73
|
+
if call.operation == "extract" and extract_mode == "substring_chars":
|
|
74
|
+
source = call.extraction_source
|
|
75
|
+
if source not in sources:
|
|
76
|
+
rule, definitions = _extract_rules(source, f"source-{len(sources)}")
|
|
77
|
+
sources[source] = rule
|
|
78
|
+
rules.extend(definitions)
|
|
79
|
+
value_rule = sources[source]
|
|
80
|
+
parts.extend((json.dumps(prefix, ensure_ascii=False), value_rule))
|
|
81
|
+
parts.append(json.dumps("}}"))
|
|
82
|
+
grammar = "root ::= " + " ".join(parts) + '\nitems ::= "[" (string ("," string)*)? "]"\n' + STRING_RULES
|
|
83
|
+
return grammar + ("\n" + "\n".join(rules) if rules else "")
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Detached inspection of the exact HTTP body; never includes transport headers."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
|
|
5
|
+
from .base import encode_payload
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def request_preview(payload, *, stage, adapter, url, evidence=None):
|
|
9
|
+
body = encode_payload(payload)
|
|
10
|
+
return {"format": "lev-request-preview", "version": 1, "stage": stage,
|
|
11
|
+
"request_adapter": adapter, "method": "POST", "url": url,
|
|
12
|
+
"body_sha256": hashlib.sha256(body).hexdigest(), "body_bytes": len(body),
|
|
13
|
+
"payload": payload, "evidence": evidence or {}}
|