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,444 @@
1
+ """Compiler behavior and differential tests against ordinary Python execution."""
2
+
3
+ from copy import deepcopy
4
+ import json
5
+ from pathlib import Path
6
+ import unittest
7
+ from unittest.mock import patch
8
+
9
+
10
+ from annoscript._test_artifacts import assert_saved_artifact
11
+ from annoscript.client import AnnoClient, precondition, system_prompt
12
+ from annoscript.compiler import CompileError, compile_program
13
+ from annoscript.anno import (
14
+ ChoiceResponse, ExtractResponse, GenerateListResponse, GenerateResponse,
15
+ NoulResponse, ScoreResponse, constrain,
16
+ )
17
+ from annoscript._test_ir_runtime import evaluate, walk_json
18
+
19
+
20
+ def _twice(value):
21
+ return value + value
22
+
23
+
24
+ def _title(anno, suffix="!"):
25
+ text = anno.generate("title", max_length=512).text
26
+ return text + suffix
27
+
28
+
29
+ def repeated_calls(anno):
30
+ text = "caller"
31
+ a = _twice(anno.generate("once", max_length=512).text)
32
+ b = _title(anno)
33
+ c = _title(anno, suffix="?")
34
+ return [a, b, c, text]
35
+
36
+
37
+ def _optional(value):
38
+ if not value:
39
+ return None, []
40
+ if value == "special":
41
+ return value, [value, value]
42
+ result = [value]
43
+ return value, result
44
+
45
+
46
+ def _nested(value):
47
+ main, related = _optional(value)
48
+ if main is None:
49
+ return related
50
+ return related + [main]
51
+
52
+
53
+ def loop_helpers(anno):
54
+ result = []
55
+ for value in anno.state:
56
+ result = result + [_nested(value)]
57
+ return result
58
+
59
+
60
+ def _change(data):
61
+ data["value"] = "after"
62
+ return "changed"
63
+
64
+
65
+ def expression_order(anno):
66
+ data = {"value": "before"}
67
+ result = [data["value"], _change(data), data["value"]]
68
+ a = 1
69
+ b = a = a + 1
70
+ a, b = b + 1, a
71
+ return [result, a, b]
72
+
73
+
74
+ def _pair(left, right="default"):
75
+ return [left, right]
76
+
77
+
78
+ def keyword_order(anno):
79
+ return _pair(right=anno.generate("right", max_length=512).text, left=anno.generate("left", max_length=512).text)
80
+
81
+
82
+ def short_circuit(anno):
83
+ a = False and anno.generate("unreachable", max_length=512).text
84
+ b = "present" or anno.generate("unreachable", max_length=512).text
85
+ c = 3 < 2 < int(anno.generate("unreachable", max_length=512).text)
86
+ d = True and anno.generate("reachable", max_length=512).text
87
+ e = "" or anno.generate("fallback", max_length=512).text
88
+ return [a, b, c, d, e]
89
+
90
+
91
+ def context_alias(anno):
92
+ engine = anno
93
+ return _title(engine)
94
+
95
+
96
+ def _falls_through(value):
97
+ if value:
98
+ return "yes"
99
+
100
+
101
+ def implicit_return(anno):
102
+ return _falls_through(anno.state)
103
+
104
+
105
+ def constrained(anno):
106
+ text = anno.generate("text", max_length=512).text
107
+ if anno.state:
108
+ constrain(len(text) > 0 and text.strip() != "", message="非空")
109
+ return text
110
+
111
+
112
+ def all_operations(anno):
113
+ a = anno.noul("binary").noul
114
+ b = anno.choice("choose", {"a": "first"}).choice
115
+ c = anno.score("rate", ["bad", "good"]).score
116
+ d = anno.extract("extract", source_field="text", max_items=8, max_length_per_item=58).items
117
+ e = anno.generate("generate", max_length=512).text
118
+ f = anno.generate_list("list", max_items=8, max_length_per_item=58).items
119
+ return [a, b, c, d, e, f]
120
+
121
+
122
+ def formatting(anno):
123
+ result = []
124
+ for index, value in enumerate(anno.state, start=2):
125
+ result = result + [f"第{index:03d}项:{value!r}"]
126
+ result = result + [json.dumps({"条目": result}, ensure_ascii=False)]
127
+ return result
128
+
129
+
130
+ def _never_check(data):
131
+ raise AssertionError("Preconditions must not execute while compiling")
132
+
133
+
134
+ @system_prompt("公共规则")
135
+ @precondition(_never_check)
136
+ def configured(anno):
137
+ return _title(anno)
138
+
139
+
140
+ _CAPTURE = {"names": ["初始"]}
141
+
142
+
143
+ def captured(anno):
144
+ return _CAPTURE
145
+
146
+
147
+ def recursive(anno):
148
+ return recursive(anno)
149
+
150
+
151
+ def _mutual_a(anno):
152
+ return _mutual_b(anno)
153
+
154
+
155
+ def _mutual_b(anno):
156
+ return _mutual_a(anno)
157
+
158
+
159
+ def mutual_recursion(anno):
160
+ return _mutual_a(anno)
161
+
162
+
163
+ def unsupported_while(anno):
164
+ while anno.state:
165
+ pass
166
+
167
+
168
+ def unsupported_comprehension(anno):
169
+ return [item for item in anno.state]
170
+
171
+
172
+ def unsupported_loop_return(anno):
173
+ for item in anno.state:
174
+ return item
175
+
176
+
177
+ def unsupported_break(anno):
178
+ for item in anno.state:
179
+ break
180
+
181
+
182
+ def unsupported_try(anno):
183
+ try:
184
+ return anno.state
185
+ except ValueError:
186
+ return None
187
+
188
+
189
+ def dynamic_call(anno):
190
+ function = _title
191
+ return function(anno)
192
+
193
+
194
+ def missing_name(anno):
195
+ return nonexistent_symbol
196
+
197
+
198
+ def unbound_name(anno):
199
+ if anno.state:
200
+ text = "set"
201
+ return text
202
+
203
+
204
+ def write_input(anno):
205
+ data = anno.state
206
+ data["key"] = "bad"
207
+ return data
208
+
209
+
210
+ def write_nested_input(anno):
211
+ data = {"input": anno.state}
212
+ alias = data["input"]
213
+ alias["key"] = "bad"
214
+ return data
215
+
216
+
217
+ def write_capture(anno):
218
+ data = _CAPTURE
219
+ data["names"] = []
220
+ return data
221
+
222
+
223
+ def write_loop_alias(anno):
224
+ data = {}
225
+ for index in range(2):
226
+ data["bad"] = index
227
+ data = anno.state
228
+ return data
229
+
230
+
231
+ def _bad_helper(data):
232
+ data["key"] = "bad"
233
+ return data
234
+
235
+
236
+ def write_helper_input(anno):
237
+ return _bad_helper(anno.state)
238
+
239
+
240
+ def invalid_model_arguments(anno):
241
+ return anno.generate("title", bogus=1, max_length=512).text
242
+
243
+
244
+ def effectful_constraint(anno):
245
+ constrain(anno.generate("title", max_length=512).text != "")
246
+ return None
247
+
248
+
249
+ def forbidden_python_call(anno):
250
+ print("must not run")
251
+ return None
252
+
253
+
254
+ def invalid_unpack(anno):
255
+ a, *b = anno.state
256
+ return a
257
+
258
+
259
+ def return_context(anno):
260
+ return anno
261
+
262
+
263
+ def invalid_signature(a, b):
264
+ return None
265
+
266
+
267
+ class Probe:
268
+ """A deterministic model double with an observable call sequence."""
269
+
270
+ def __init__(self, state=None, text=None):
271
+ self.state = state
272
+ self.calls = []
273
+ self.text = text
274
+
275
+ def generate(self, instructions, **kwargs):
276
+ self.calls.append(("generate", instructions, deepcopy(kwargs)))
277
+ text = self.text if self.text is not None else f"{instructions}:{len(self.calls)}"
278
+ return GenerateResponse(text)
279
+
280
+ def choice(self, instructions, criteria):
281
+ self.calls.append(("choice", instructions, deepcopy(criteria)))
282
+ return ChoiceResponse(next(iter(criteria)), 0.9, {key: 1.0 for key in criteria})
283
+
284
+ def noul(self, instructions):
285
+ self.calls.append(("noul", instructions))
286
+ return NoulResponse(0.8)
287
+
288
+ def score(self, instructions, criteria):
289
+ self.calls.append(("score", instructions, deepcopy(criteria)))
290
+ return ScoreResponse({0: 0.2, 1: 0.8}, 0.8, {0: "bad", 1: "good"}, 0.9)
291
+
292
+ def extract(self, instructions, **kwargs):
293
+ self.calls.append(("extract", instructions, deepcopy(kwargs)))
294
+ return ExtractResponse([self.state[kwargs["source_field"]]])
295
+
296
+ def generate_list(self, instructions, **kwargs):
297
+ self.calls.append(("generate_list", instructions, deepcopy(kwargs)))
298
+ return GenerateListResponse(["entry"])
299
+
300
+
301
+ class CompilerTests(unittest.TestCase):
302
+ def compare(self, function, state=None):
303
+ original = Probe(deepcopy(state))
304
+ expected = function(original)
305
+ for optimize in (False, True):
306
+ compiled = Probe(deepcopy(state))
307
+ program = compile_program(function, optimize=optimize)
308
+ actual = evaluate(json.loads(program.to_json()), compiled)
309
+ self.assertEqual(actual, expected)
310
+ self.assertEqual(compiled.calls, original.calls)
311
+ self.assertEqual(compiled.state, original.state)
312
+ return actual, compiled.calls, program.to_dict()
313
+
314
+ def test_repeated_calls_arguments_and_local_isolation(self):
315
+ actual, calls, data = self.compare(repeated_calls)
316
+ self.assertEqual(len(calls), 3)
317
+ self.assertEqual(actual, ["once:1once:1", "title:2!", "title:3?", "caller"])
318
+ self.assertFalse(any(n.get("kind") in ("call", "function") for n in walk_json(data)))
319
+
320
+ def test_nested_returns_and_finite_loops(self):
321
+ for state in ([], ["", "ordinary", "special", ""]):
322
+ with self.subTest(state=state):
323
+ self.compare(loop_helpers, state)
324
+
325
+ def test_expression_order_and_assignment(self):
326
+ self.assertEqual(self.compare(expression_order)[0], [["before", "changed", "after"], 3, 2])
327
+ self.compare(keyword_order)
328
+
329
+ def test_short_circuit_skips_inference(self):
330
+ _, calls, _ = self.compare(short_circuit)
331
+ self.assertEqual([c[1] for c in calls], ["reachable", "fallback"])
332
+
333
+ def test_injected_context_alias(self):
334
+ self.compare(context_alias)
335
+
336
+ def test_implicit_return(self):
337
+ self.assertIsNone(self.compare(implicit_return, False)[0])
338
+ self.assertEqual(self.compare(implicit_return, True)[0], "yes")
339
+
340
+ def test_all_six_operations_and_response_fields(self):
341
+ _, calls, data = self.compare(all_operations, {"text": "source"})
342
+ self.assertEqual(len(calls), 6)
343
+ self.assertEqual({n["operation"] for n in walk_json(data) if n.get("kind") == "infer"},
344
+ {"noul", "choice", "score", "extract", "generate", "generate_list"})
345
+
346
+ def test_fstrings_and_json_encoding(self):
347
+ self.compare(formatting, ["中文", 'a"b', "line\nbreak"])
348
+
349
+ def test_constraint_is_a_pure_expression_and_path_local(self):
350
+ data = compile_program(constrained).to_dict()
351
+ constraints = [n for n in walk_json(data) if n.get("kind") == "constrain"]
352
+ self.assertEqual(len(constraints), 1)
353
+ self.assertEqual(constraints[0]["condition"]["kind"], "bool_op")
354
+ self.assertIn("intrinsic", {n.get("kind") for n in walk_json(constraints[0])})
355
+ self.assertEqual(evaluate(data, Probe(False, text=" ")), " ")
356
+ with self.assertRaisesRegex(ValueError, "非空"):
357
+ evaluate(data, Probe(True, text=" "))
358
+ self.assertEqual(evaluate(data, Probe(True, text="ok")), "ok")
359
+
360
+ def test_config_and_preconditions_are_separate(self):
361
+ with patch.object(AnnoClient, "_validate_inputs", side_effect=AssertionError("must not run")):
362
+ data = compile_program(configured).to_dict()
363
+ self.assertEqual(data["config"], {"system_prompt": "公共规则"})
364
+ serialized = json.dumps(data)
365
+ self.assertNotIn("_never_check", serialized)
366
+ self.assertNotIn("precondition", serialized)
367
+
368
+ def test_captured_globals_are_detached_snapshots(self):
369
+ program = compile_program(captured)
370
+ _CAPTURE["names"].append("later")
371
+ try:
372
+ self.assertEqual(evaluate(program.to_dict(), Probe()), {"names": ["初始"]})
373
+ self.assertEqual(evaluate(compile_program(captured).to_dict(), Probe()), _CAPTURE)
374
+ finally:
375
+ _CAPTURE["names"].pop()
376
+
377
+ def test_compile_never_executes_the_function(self):
378
+ import sys
379
+ executed = []
380
+ watched = {repeated_calls.__code__, _title.__code__, _twice.__code__}
381
+
382
+ def trace(frame, event, arg):
383
+ if event == "call" and frame.f_code in watched:
384
+ executed.append(frame.f_code.co_name)
385
+
386
+ previous = sys.getprofile()
387
+ sys.setprofile(trace)
388
+ try:
389
+ compile_program(repeated_calls)
390
+ finally:
391
+ sys.setprofile(previous)
392
+ self.assertEqual(executed, [])
393
+
394
+ def test_determinism_and_inline_source_chain(self):
395
+ first = compile_program(loop_helpers)
396
+ second = compile_program(loop_helpers)
397
+ self.assertEqual(first.to_json(), second.to_json())
398
+ statements = [n for n in walk_json(first.to_dict()) if "source" in n and "id" in n]
399
+ self.assertEqual(len({n["id"] for n in statements}), len(statements))
400
+ self.assertTrue(any(len(n["source"]["calls"]) == 2 for n in statements))
401
+ self.assertNotIn(str(Path(__file__).resolve().parent), first.to_json())
402
+
403
+ def test_rejected_language_constructs_and_writes(self):
404
+ cases = {
405
+ recursive: "Recursive", mutual_recursion: "Recursive",
406
+ unsupported_while: "While", unsupported_comprehension: "ListComp",
407
+ unsupported_loop_return: "Returns inside loops", unsupported_break: "Break",
408
+ unsupported_try: "Try", dynamic_call: "Cannot capture function",
409
+ missing_name: "Unknown name", unbound_name: "may be unbound",
410
+ write_input: "read-only", write_nested_input: "read-only", write_capture: "read-only",
411
+ write_loop_alias: "read-only", write_helper_input: "read-only",
412
+ invalid_model_arguments: "Invalid call arguments", effectful_constraint: "must be pure",
413
+ forbidden_python_call: "Only known intrinsics", invalid_unpack: "only names",
414
+ return_context: "cannot be stored", invalid_signature: "exactly one",
415
+ }
416
+ for function, message in cases.items():
417
+ with self.subTest(function=function.__name__), self.assertRaisesRegex(CompileError, message):
418
+ compile_program(function)
419
+
420
+ def test_diagnostic_contains_source_and_inline_chain(self):
421
+ with self.assertRaises(CompileError) as caught:
422
+ compile_program(write_helper_input)
423
+ error = caught.exception
424
+ self.assertEqual(error.source.span.function, "_bad_helper")
425
+ self.assertEqual(error.source.calls[-1].function, "write_helper_input")
426
+ self.assertGreater(error.source.span.line, 0)
427
+ self.assertIn("inlined from", str(error))
428
+
429
+ def test_unavailable_source_and_closure(self):
430
+ with self.assertRaisesRegex(CompileError, "source is unavailable"):
431
+ compile_program(eval("lambda anno: None"))
432
+ data = "captured"
433
+
434
+ def closure(anno):
435
+ return data
436
+
437
+ with self.assertRaisesRegex(CompileError, "Closures"):
438
+ compile_program(closure)
439
+ with self.assertRaisesRegex(CompileError, "ordinary Python function"):
440
+ compile_program(len)
441
+
442
+
443
+ if __name__ == "__main__":
444
+ unittest.main()