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
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
"""Test-only dependency executor. No backend, retries, batching or solver.
|
|
2
|
+
|
|
3
|
+
Consumes the serialized Plan directly. asyncio futures model in-flight Infer
|
|
4
|
+
requests; fake providers may return either ordinary responses or awaitables.
|
|
5
|
+
Different launch orders exercise readiness rather than source-order execution.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import inspect
|
|
10
|
+
import json
|
|
11
|
+
import operator
|
|
12
|
+
import random
|
|
13
|
+
from copy import deepcopy
|
|
14
|
+
|
|
15
|
+
from annoscript import ir
|
|
16
|
+
from annoscript.plan import ExecutionPlan
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
_SKIP = object()
|
|
20
|
+
_COMPARE = {
|
|
21
|
+
"eq": operator.eq, "ne": operator.ne, "lt": operator.lt, "le": operator.le,
|
|
22
|
+
"gt": operator.gt, "ge": operator.ge, "in": lambda a, b: a in b,
|
|
23
|
+
"not_in": lambda a, b: a not in b, "is": operator.is_, "is_not": operator.is_not,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _expr(e, env, state):
|
|
28
|
+
kind = e["kind"]
|
|
29
|
+
ev = lambda x: _expr(x, env, state)
|
|
30
|
+
if kind == "literal":
|
|
31
|
+
return e["value"]
|
|
32
|
+
if kind == "var":
|
|
33
|
+
return env[e["name"]]
|
|
34
|
+
if kind == "state":
|
|
35
|
+
return state
|
|
36
|
+
if kind == "get_item":
|
|
37
|
+
return ev(e["value"])[ev(e["index"])]
|
|
38
|
+
if kind == "field":
|
|
39
|
+
return getattr(ev(e["value"]), e["name"])
|
|
40
|
+
if kind in ("list", "tuple"):
|
|
41
|
+
result = [ev(x) for x in e["items"]]
|
|
42
|
+
return tuple(result) if kind == "tuple" else result
|
|
43
|
+
if kind == "dict":
|
|
44
|
+
return {ev(k): ev(v) for k, v in e["entries"]}
|
|
45
|
+
if kind == "unary":
|
|
46
|
+
return {"not": operator.not_, "negative": operator.neg, "positive": operator.pos}[e["op"]](ev(e["value"]))
|
|
47
|
+
if kind == "binary":
|
|
48
|
+
return ev(e["left"]) + ev(e["right"])
|
|
49
|
+
if kind == "compare":
|
|
50
|
+
left = ev(e["left"])
|
|
51
|
+
for op, node in zip(e["ops"], e["comparators"]):
|
|
52
|
+
right = ev(node)
|
|
53
|
+
if not _COMPARE[op](left, right):
|
|
54
|
+
return False
|
|
55
|
+
left = right
|
|
56
|
+
return True
|
|
57
|
+
if kind == "bool_op":
|
|
58
|
+
value = ev(e["values"][0])
|
|
59
|
+
for x in e["values"][1:]:
|
|
60
|
+
if (e["op"] == "and" and not value) or (e["op"] == "or" and value):
|
|
61
|
+
break
|
|
62
|
+
value = ev(x)
|
|
63
|
+
return value
|
|
64
|
+
if kind == "intrinsic":
|
|
65
|
+
funcs = {"len": len, "int": int, "range": range, "enumerate": enumerate,
|
|
66
|
+
"str.strip": str.strip, "json.dumps": json.dumps,
|
|
67
|
+
"example": lambda state, answer: deepcopy({"state": state, "answer": answer})}
|
|
68
|
+
return funcs[e["name"]](*[ev(x) for x in e["args"]], **{k: ev(v) for k, v in e["kwargs"]})
|
|
69
|
+
if kind == "format_value":
|
|
70
|
+
value = ev(e["value"])
|
|
71
|
+
spec = ev(e["spec"]) if e["spec"] is not None else ""
|
|
72
|
+
if e["conversion"] != "none":
|
|
73
|
+
value = {"s": str, "r": repr, "a": ascii}[e["conversion"]](value)
|
|
74
|
+
return format(value, spec)
|
|
75
|
+
if kind == "format_string":
|
|
76
|
+
return "".join(ev(x) for x in e["parts"])
|
|
77
|
+
raise AssertionError(kind)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _unpack(target, value):
|
|
81
|
+
if target["kind"] == "name_target":
|
|
82
|
+
return [value]
|
|
83
|
+
values = list(value)
|
|
84
|
+
if len(values) != len(target["items"]):
|
|
85
|
+
raise ValueError("Unpack length mismatch")
|
|
86
|
+
result = []
|
|
87
|
+
for t, v in zip(target["items"], values):
|
|
88
|
+
result.extend(_unpack(t, v))
|
|
89
|
+
return result
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class _Executor:
|
|
93
|
+
def __init__(self, plan, anno, mode, order):
|
|
94
|
+
decoded = ExecutionPlan.from_dict(plan)
|
|
95
|
+
self.plan = decoded.to_dict()
|
|
96
|
+
self.dependencies = [ir._encode(d) for d in decoded.dependencies_for(mode)]
|
|
97
|
+
self.mode, self.order, self.anno = mode, order, anno
|
|
98
|
+
self.tasks = {t["id"]: t for t in self.plan["tasks"]}
|
|
99
|
+
self.regions = {r["id"]: r for r in self.plan["regions"]}
|
|
100
|
+
self.values = {v["id"]: v for v in self.plan["values"]}
|
|
101
|
+
self.requests = {r["id"]: r for r in self.plan["requests"]}
|
|
102
|
+
self.futures, self.outputs, self.region_runs = {}, {}, {}
|
|
103
|
+
self.iterations, self.items, self.entered = {}, {}, {}
|
|
104
|
+
self.cache, self.returns = {}, []
|
|
105
|
+
self.failed = None
|
|
106
|
+
self.random = random.Random(7)
|
|
107
|
+
self.paths = {}
|
|
108
|
+
for t in self.tasks.values():
|
|
109
|
+
path, region = [], self.regions[t["region"]]
|
|
110
|
+
while region["parent"] is not None:
|
|
111
|
+
if region["kind"] == "loop":
|
|
112
|
+
path.append(region["owner"])
|
|
113
|
+
region = self.regions[region["parent"]]
|
|
114
|
+
self.paths[t["id"]] = tuple(reversed(path))
|
|
115
|
+
self.incoming = {t: [] for t in self.tasks}
|
|
116
|
+
for d in self.dependencies:
|
|
117
|
+
self.incoming[d["target"]].append(d)
|
|
118
|
+
|
|
119
|
+
def launch(self, region, ctx):
|
|
120
|
+
ids = list(self.regions[region]["tasks"])
|
|
121
|
+
if self.order == "reverse":
|
|
122
|
+
ids.reverse()
|
|
123
|
+
elif self.order == "random":
|
|
124
|
+
self.random.shuffle(ids)
|
|
125
|
+
keys = []
|
|
126
|
+
for tid in ids:
|
|
127
|
+
key = (tid, ctx)
|
|
128
|
+
self.entered[key] = asyncio.Event()
|
|
129
|
+
self.futures[key] = asyncio.create_task(self.run_task(tid, ctx))
|
|
130
|
+
keys.append(key)
|
|
131
|
+
self.region_runs[region, ctx] = keys
|
|
132
|
+
|
|
133
|
+
async def ensure(self, tid, ctx):
|
|
134
|
+
ctx = ctx[:len(self.paths[tid])]
|
|
135
|
+
key = tid, ctx
|
|
136
|
+
if key not in self.futures:
|
|
137
|
+
region = self.regions[self.tasks[tid]["region"]]
|
|
138
|
+
owner = region["owner"]
|
|
139
|
+
if region["kind"] in ("then", "else"):
|
|
140
|
+
selected = await self.ensure(owner, ctx)
|
|
141
|
+
if selected is _SKIP or bool(selected) != (region["kind"] == "then"):
|
|
142
|
+
return _SKIP
|
|
143
|
+
else:
|
|
144
|
+
raise AssertionError(f"uninstantiated task {key}")
|
|
145
|
+
return await self.futures[key]
|
|
146
|
+
|
|
147
|
+
async def port(self, tid, ctx, port):
|
|
148
|
+
if port == "enter":
|
|
149
|
+
key = tid, ctx[:len(self.paths[tid])]
|
|
150
|
+
await self.entered[key].wait()
|
|
151
|
+
return True
|
|
152
|
+
result = await self.ensure(tid, ctx)
|
|
153
|
+
if result is _SKIP:
|
|
154
|
+
return False
|
|
155
|
+
if port == "false":
|
|
156
|
+
return not result
|
|
157
|
+
if port in ("true", "continue"):
|
|
158
|
+
return bool(result)
|
|
159
|
+
return True
|
|
160
|
+
|
|
161
|
+
async def dependency(self, d, ctx):
|
|
162
|
+
if d["condition"]:
|
|
163
|
+
c = d["condition"]
|
|
164
|
+
if not await self.port(c["task"], ctx, c["port"]):
|
|
165
|
+
return
|
|
166
|
+
m = d["mapping"]
|
|
167
|
+
source, target = m["source_loops"], m["target_loops"]
|
|
168
|
+
count = 0
|
|
169
|
+
for a, b in zip(source, target):
|
|
170
|
+
if a != b:
|
|
171
|
+
break
|
|
172
|
+
count += 1
|
|
173
|
+
if m["carried_loop"] is not None:
|
|
174
|
+
count = min(count, target.index(m["carried_loop"]) + 1)
|
|
175
|
+
prefix = list(ctx[:count])
|
|
176
|
+
if m["carried_loop"] is not None:
|
|
177
|
+
i = target.index(m["carried_loop"])
|
|
178
|
+
prefix[i] -= m["distance"]
|
|
179
|
+
if prefix[i] < 0:
|
|
180
|
+
return
|
|
181
|
+
contexts = [tuple(prefix)]
|
|
182
|
+
for loop in source[count:]:
|
|
183
|
+
expanded = []
|
|
184
|
+
for parent in contexts:
|
|
185
|
+
result = await self.ensure(loop, parent)
|
|
186
|
+
if result is not _SKIP:
|
|
187
|
+
expanded.extend(parent + (i,) for i in self.iterations[loop, parent])
|
|
188
|
+
contexts = expanded
|
|
189
|
+
for context in contexts:
|
|
190
|
+
await self.port(d["source"], context, d["port"])
|
|
191
|
+
|
|
192
|
+
async def value(self, value, ctx):
|
|
193
|
+
producer = self.values[value]["producer"]
|
|
194
|
+
ctx = ctx[:len(self.paths[producer])]
|
|
195
|
+
result = await self.ensure(producer, ctx)
|
|
196
|
+
if result is _SKIP:
|
|
197
|
+
raise AssertionError(f"read of inactive value {value}")
|
|
198
|
+
return self.outputs[value, ctx]
|
|
199
|
+
|
|
200
|
+
async def expression(self, bound, ctx):
|
|
201
|
+
env = {b["name"]: await self.value(b["value"], ctx) for b in bound["bindings"]}
|
|
202
|
+
return _expr(bound["expression"], env, self.anno.state)
|
|
203
|
+
|
|
204
|
+
async def complete_region(self, region, ctx):
|
|
205
|
+
results = await asyncio.gather(*(self.futures[k] for k in self.region_runs[region, ctx]))
|
|
206
|
+
return any(self.tasks[k[0]]["kind"] == "return" and result is not _SKIP or
|
|
207
|
+
self.tasks[k[0]]["kind"] == "join" and result is False
|
|
208
|
+
for k, result in zip(self.region_runs[region, ctx], results))
|
|
209
|
+
|
|
210
|
+
async def invoke(self, operation, args, kwargs):
|
|
211
|
+
if self.failed is not None:
|
|
212
|
+
raise self.failed
|
|
213
|
+
try:
|
|
214
|
+
result = getattr(self.anno, operation)(*args, **kwargs)
|
|
215
|
+
return await result if inspect.isawaitable(result) else result
|
|
216
|
+
except Exception as exc:
|
|
217
|
+
if self.failed is None:
|
|
218
|
+
self.failed = exc
|
|
219
|
+
raise
|
|
220
|
+
|
|
221
|
+
async def run_task(self, tid, ctx):
|
|
222
|
+
try:
|
|
223
|
+
return await self.task_body(tid, ctx)
|
|
224
|
+
except Exception as exc:
|
|
225
|
+
if self.failed is None:
|
|
226
|
+
self.failed = exc
|
|
227
|
+
raise
|
|
228
|
+
|
|
229
|
+
async def task_body(self, tid, ctx):
|
|
230
|
+
t = self.tasks[tid]
|
|
231
|
+
for gate in t["gates"]:
|
|
232
|
+
if not await self.port(gate["task"], ctx, gate["port"]):
|
|
233
|
+
return _SKIP
|
|
234
|
+
for d in self.incoming[tid]:
|
|
235
|
+
await self.dependency(d, ctx)
|
|
236
|
+
if self.failed:
|
|
237
|
+
raise self.failed
|
|
238
|
+
kind, values, result = t["kind"], [], None
|
|
239
|
+
if kind == "eval":
|
|
240
|
+
values = [await self.expression(t["expression"], ctx)]
|
|
241
|
+
elif kind == "unpack":
|
|
242
|
+
values = _unpack(t["target"], await self.expression(t["expression"], ctx))
|
|
243
|
+
elif kind == "store":
|
|
244
|
+
obj = await self.expression(t["container"], ctx)
|
|
245
|
+
index = await self.expression(t["index"], ctx)
|
|
246
|
+
item = await self.expression(t["item"], ctx)
|
|
247
|
+
obj[index] = item
|
|
248
|
+
elif kind == "infer_site":
|
|
249
|
+
args, kwvalues = await self.value(t["arguments"], ctx)
|
|
250
|
+
request = self.requests[t["request"]]
|
|
251
|
+
key = request["id"], ctx[:len(request["scope"])]
|
|
252
|
+
if not request["reusable"]:
|
|
253
|
+
key += (tid, ctx)
|
|
254
|
+
if key not in self.cache:
|
|
255
|
+
self.cache[key] = asyncio.create_task(self.invoke(request["operation"], args, dict(zip(t["keywords"], kwvalues))))
|
|
256
|
+
values = [await self.cache[key]]
|
|
257
|
+
elif kind == "branch":
|
|
258
|
+
result = bool(await self.expression(t["condition"], ctx))
|
|
259
|
+
self.launch(t["then_region"] if result else t["else_region"], ctx)
|
|
260
|
+
elif kind == "join":
|
|
261
|
+
branch = self.tasks[t["branch"]]
|
|
262
|
+
selected = await self.ensure(t["branch"], ctx)
|
|
263
|
+
result = not await self.complete_region(branch["then_region"] if selected else branch["else_region"], ctx)
|
|
264
|
+
elif kind == "merge":
|
|
265
|
+
selected = await self.ensure(t["branch"], ctx)
|
|
266
|
+
value = t["then_value"] if selected else t["else_value"]
|
|
267
|
+
if value is None:
|
|
268
|
+
return _SKIP
|
|
269
|
+
values = [await self.value(value, ctx)]
|
|
270
|
+
elif kind == "carry":
|
|
271
|
+
values = [await self.value(t["initial"], ctx[:-1]) if ctx[-1] == 0 else
|
|
272
|
+
await self.value(t["update"], ctx[:-1] + (ctx[-1]-1,))]
|
|
273
|
+
elif kind == "iteration":
|
|
274
|
+
values = _unpack(t["target"], self.items[t["loop"], ctx])
|
|
275
|
+
elif kind == "loop":
|
|
276
|
+
iterable = await self.expression(t["iterable"], ctx)
|
|
277
|
+
iterator = iter(iterable)
|
|
278
|
+
self.iterations[tid, ctx] = []
|
|
279
|
+
self.entered[tid, ctx].set()
|
|
280
|
+
index = 0
|
|
281
|
+
while True:
|
|
282
|
+
if index and self.mode in t["serial_modes"]:
|
|
283
|
+
await self.complete_region(t["body"], ctx + (index-1,))
|
|
284
|
+
try:
|
|
285
|
+
item = next(iterator)
|
|
286
|
+
except StopIteration:
|
|
287
|
+
break
|
|
288
|
+
self.items[tid, ctx + (index,)] = item
|
|
289
|
+
self.iterations[tid, ctx].append(index)
|
|
290
|
+
self.launch(t["body"], ctx + (index,))
|
|
291
|
+
index += 1
|
|
292
|
+
for i in range(index):
|
|
293
|
+
await self.complete_region(t["body"], ctx + (i,))
|
|
294
|
+
values = [await self.value(e["update"], ctx + (index-1,)) if index else
|
|
295
|
+
await self.value(e["initial"], ctx) for e in t["exports"]]
|
|
296
|
+
elif kind == "check":
|
|
297
|
+
valid = await self.expression(t["condition"], ctx)
|
|
298
|
+
message = await self.expression(t["message"], ctx) if t["message"] is not None else None
|
|
299
|
+
if not valid:
|
|
300
|
+
raise ValueError(message or "Constraint failed")
|
|
301
|
+
elif kind == "return":
|
|
302
|
+
result = await self.expression(t["expression"], ctx)
|
|
303
|
+
self.returns.append(result)
|
|
304
|
+
else:
|
|
305
|
+
raise AssertionError(kind)
|
|
306
|
+
for output, value in zip(t["outputs"], values):
|
|
307
|
+
self.outputs[output, ctx] = value
|
|
308
|
+
return result
|
|
309
|
+
|
|
310
|
+
async def run(self):
|
|
311
|
+
root = next(r["id"] for r in self.regions.values() if r["kind"] == "root")
|
|
312
|
+
self.launch(root, ())
|
|
313
|
+
try:
|
|
314
|
+
await self.complete_region(root, ())
|
|
315
|
+
assert len(self.returns) == 1, self.returns
|
|
316
|
+
return self.returns[0]
|
|
317
|
+
except Exception as exc:
|
|
318
|
+
# A nested check may fail before another root task propagates its
|
|
319
|
+
# own exception. Report observation order, not region-join order.
|
|
320
|
+
raise self.failed if self.failed is not None else exc
|
|
321
|
+
finally:
|
|
322
|
+
pending = set(self.futures.values()) | set(self.cache.values())
|
|
323
|
+
for task in pending:
|
|
324
|
+
if not task.done():
|
|
325
|
+
task.cancel()
|
|
326
|
+
await asyncio.gather(*pending, return_exceptions=True)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def evaluate(plan, anno, *, mode="eager", order="forward"):
|
|
330
|
+
"""Run a serialized Plan against a deterministic fake provider."""
|
|
331
|
+
return asyncio.run(asyncio.wait_for(_Executor(plan, anno, mode, order).run(), timeout=5))
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Python 3.10 side of the offline cross-process IR acceptance test."""
|
|
2
|
+
|
|
3
|
+
import importlib.abc
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ImportGuard(importlib.abc.MetaPathFinder):
|
|
10
|
+
def find_spec(self, fullname, path=None, target=None):
|
|
11
|
+
if fullname in ("annoscript.runtime", "annoscript.plan", "annoscript.plan_lowering") or fullname.startswith("annoscript.backends"):
|
|
12
|
+
raise AssertionError("client loaded execution code: " + fullname)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def main():
|
|
16
|
+
if sys.version_info[:2] != (3, 10):
|
|
17
|
+
raise RuntimeError("test client requires Python 3.10")
|
|
18
|
+
sys.meta_path.insert(0, ImportGuard())
|
|
19
|
+
from annoscript_examples.request import annotate
|
|
20
|
+
from annoscript.remote import AnnoRemoteClient
|
|
21
|
+
|
|
22
|
+
fixture = json.loads(Path(sys.argv[2]).read_text(encoding="utf-8"))
|
|
23
|
+
for mode in ("strict", "eager", "speculative"):
|
|
24
|
+
for optimize in (False, True):
|
|
25
|
+
for ordered in (False, True):
|
|
26
|
+
with AnnoRemoteClient(sys.argv[1], mode=mode, optimize=optimize, timeout=30) as client:
|
|
27
|
+
results = list(client.submit(annotate, fixture["inputs"], ordered=ordered))
|
|
28
|
+
if ordered and [r.index for r in results] != list(range(len(fixture["inputs"]))):
|
|
29
|
+
raise AssertionError("ordered results lost input indices")
|
|
30
|
+
indexed = {r.index: r for r in results}
|
|
31
|
+
if len(results) != len(indexed) or len(indexed) != len(fixture["expected"]):
|
|
32
|
+
raise AssertionError("missing or duplicate results")
|
|
33
|
+
for index, expected in enumerate(fixture["expected"]):
|
|
34
|
+
result = indexed[index]
|
|
35
|
+
if result.status != expected["status"]:
|
|
36
|
+
raise AssertionError((mode, optimize, index, result, expected))
|
|
37
|
+
if result.status == "success":
|
|
38
|
+
if result.value != expected["value"]:
|
|
39
|
+
raise AssertionError((index, result.value, expected["value"]))
|
|
40
|
+
elif result.error.code != expected["code"] or result.value is not None:
|
|
41
|
+
raise AssertionError((index, result, expected))
|
|
42
|
+
print(f"mode={mode}, optimize={optimize}, ordered={ordered}: {len(results)} indexed outcomes OK", flush=True)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
if __name__ == "__main__":
|
|
46
|
+
main()
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Loopback-only HTTP fixture for cross-version acceptance. Never a production server."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from copy import deepcopy
|
|
5
|
+
from dataclasses import asdict
|
|
6
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
7
|
+
import json
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
from annoscript._test_synthetic import CannedBackend, cases, key
|
|
12
|
+
from annoscript.ir import Program
|
|
13
|
+
from annoscript.passes import optimize_program
|
|
14
|
+
from annoscript.plan_lowering import lower_to_plan
|
|
15
|
+
from annoscript.remote import ir_sha256
|
|
16
|
+
from annoscript.runtime import AnnoRuntime, RunInput
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def main():
|
|
20
|
+
if sys.version_info[:2] != (3, 12):
|
|
21
|
+
raise RuntimeError("test server requires Python 3.12")
|
|
22
|
+
scenarios, inputs, expected, failures = cases()
|
|
23
|
+
Path(sys.argv[1]).write_text(json.dumps({"inputs": inputs, "expected": expected}, ensure_ascii=False), encoding="utf-8")
|
|
24
|
+
# Record fixtures before accepting requests; the real Runtime performs all
|
|
25
|
+
# constraint checks and model-response validation during execution.
|
|
26
|
+
fixture_backend = CannedBackend(scenarios, failures, speculative=True)
|
|
27
|
+
|
|
28
|
+
class Handler(BaseHTTPRequestHandler):
|
|
29
|
+
def log_message(self, *args):
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
def frame(self, value):
|
|
33
|
+
self.wfile.write((json.dumps(value, ensure_ascii=False, allow_nan=False) + "\n").encode())
|
|
34
|
+
self.wfile.flush()
|
|
35
|
+
|
|
36
|
+
def do_POST(self):
|
|
37
|
+
if self.path != "/v1/runs:batch":
|
|
38
|
+
self.send_error(404)
|
|
39
|
+
return
|
|
40
|
+
payload = json.loads(self.rfile.read(int(self.headers["Content-Length"])))
|
|
41
|
+
if payload["protocol_version"] != 1 or ir_sha256(payload["ir"]) != payload["ir_sha256"]:
|
|
42
|
+
self.send_error(400)
|
|
43
|
+
return
|
|
44
|
+
# Deserialization, optimization and lowering occur in this process.
|
|
45
|
+
program = Program.from_json(json.dumps(payload["ir"], ensure_ascii=False, allow_nan=False))
|
|
46
|
+
plan = lower_to_plan(optimize_program(program))
|
|
47
|
+
received = payload["inputs"]
|
|
48
|
+
if [item["index"] for item in received] != list(range(1, len(inputs))):
|
|
49
|
+
self.send_error(400, "client did not preserve validated input indices")
|
|
50
|
+
return
|
|
51
|
+
self.send_response(200)
|
|
52
|
+
self.send_header("Content-Type", "application/x-ndjson")
|
|
53
|
+
self.end_headers()
|
|
54
|
+
self.frame({"type": "accepted", "protocol_version": 1, "ir_sha256": payload["ir_sha256"]})
|
|
55
|
+
|
|
56
|
+
async def execute():
|
|
57
|
+
backend = deepcopy(fixture_backend)
|
|
58
|
+
async with AnnoRuntime(backend, backend) as runtime:
|
|
59
|
+
stream = await runtime.submit(plan, [RunInput(item["index"], item["state"]) for item in received],
|
|
60
|
+
mode=payload["mode"])
|
|
61
|
+
async for result in stream:
|
|
62
|
+
self.frame({"type": "result", **asdict(result)})
|
|
63
|
+
if key(inputs[0]) in {state for state, _, _ in backend.calls}:
|
|
64
|
+
raise AssertionError("invalid input reached a backend")
|
|
65
|
+
|
|
66
|
+
asyncio.run(execute())
|
|
67
|
+
self.frame({"type": "done"})
|
|
68
|
+
|
|
69
|
+
with HTTPServer(("127.0.0.1", 0), Handler) as server:
|
|
70
|
+
print(json.dumps({"url": f"http://127.0.0.1:{server.server_port}", "python": "3.12"}), flush=True)
|
|
71
|
+
server.serve_forever()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
if __name__ == "__main__":
|
|
75
|
+
main()
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Deterministic response tables, independent of instruction parsing."""
|
|
2
|
+
from copy import deepcopy
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
from annoscript._data import thaw
|
|
6
|
+
from annoscript.backends import BatchResult, InferenceError, InferenceOutput
|
|
7
|
+
from annoscript.anno import (ChoiceResponse, ExtractResponse, GenerateListResponse,
|
|
8
|
+
GenerateResponse, NoulResponse, ScoreResponse)
|
|
9
|
+
|
|
10
|
+
REVIEW_INPUT = {"text": "示例条目"}
|
|
11
|
+
LAUNCH_INPUT = {"text": "示例在广州发布"}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def key(value):
|
|
15
|
+
return json.dumps(thaw(value), ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class CannedBackend:
|
|
19
|
+
def __init__(self, scenarios=(), failures=None, *, speculative=False):
|
|
20
|
+
self.tables = {key(state): deepcopy(value) for state, value in scenarios}
|
|
21
|
+
self.failures, self.calls = failures or {}, []
|
|
22
|
+
|
|
23
|
+
def answer(self, context, call):
|
|
24
|
+
state_key = key(context.state)
|
|
25
|
+
field = call.arguments.get("field_name", call.id)
|
|
26
|
+
self.calls.append((state_key, call.operation, field))
|
|
27
|
+
if (state_key, field) in self.failures:
|
|
28
|
+
raise InferenceError("injected model failure", code=self.failures[state_key, field])
|
|
29
|
+
value = self.tables[state_key][field]
|
|
30
|
+
if call.operation == "noul":
|
|
31
|
+
return NoulResponse(0.9 if value else 0.1)
|
|
32
|
+
if call.operation == "choice":
|
|
33
|
+
return ChoiceResponse(value, 1.0, {k: float(k == value) for k in call.arguments["criteria"]})
|
|
34
|
+
if call.operation == "score":
|
|
35
|
+
return ScoreResponse({0: 0.0, 1: 1.0}, value, {0: "low", 1: "high"}, 1.0)
|
|
36
|
+
if call.operation == "generate":
|
|
37
|
+
return GenerateResponse(value)
|
|
38
|
+
if call.operation == "generate_list":
|
|
39
|
+
return GenerateListResponse(deepcopy(value))
|
|
40
|
+
if call.operation == "extract":
|
|
41
|
+
return ExtractResponse(deepcopy(value))
|
|
42
|
+
raise AssertionError(call.operation)
|
|
43
|
+
|
|
44
|
+
async def infer(self, context, call):
|
|
45
|
+
return InferenceOutput(self.answer(context, call))
|
|
46
|
+
|
|
47
|
+
async def infer_batch(self, context, calls):
|
|
48
|
+
return BatchResult({call.id: self.answer(context, call) for call in calls})
|
|
49
|
+
|
|
50
|
+
async def aclose(self):
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def cases():
|
|
55
|
+
scenarios = []
|
|
56
|
+
for index, text in enumerate(["alpha beta", "gamma delta", "示例甲 示例乙", "one two",
|
|
57
|
+
"three four", "five six", "seven eight", "nine ten"]):
|
|
58
|
+
state = {"text": text, "kind": "note"}
|
|
59
|
+
value = {"category": "note", "highlight": index != 1, "clarity": 1.0,
|
|
60
|
+
"title": None if index == 1 else "Synthetic title", "tags": ["sample"],
|
|
61
|
+
"spans": [text.split()[0]]}
|
|
62
|
+
scenarios.append((state, value))
|
|
63
|
+
inputs = [{"text": ""}] + [s for s, _ in scenarios]
|
|
64
|
+
expected = [{"status": "error", "code": "invalid_input"}]
|
|
65
|
+
expected += [{"status": "success", "value": value} for _, value in scenarios[:-1]]
|
|
66
|
+
expected += [{"status": "error", "code": "model_timeout"}]
|
|
67
|
+
return scenarios, inputs, expected, {(key(scenarios[-1][0]), "title"): "model_timeout"}
|