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