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,133 @@
1
+ """The concrete synchronous client and asynchronous runtime share ownership safely."""
2
+
3
+ import asyncio
4
+ import unittest
5
+ from unittest.mock import patch
6
+ from threading import Event
7
+
8
+ from annoscript.client import AnnoClient, precondition, require
9
+ from annoscript.runtime import AnnoRuntime
10
+ from annoscript.backends import InferenceOutput, BackendFatalError
11
+ from annoscript.anno import GenerateResponse
12
+ from annoscript.test_runtime import FakeBackend, mixed
13
+
14
+
15
+ def echo(anno):
16
+ return anno.generate(anno.state, max_length=512).text
17
+
18
+
19
+ def constant(anno):
20
+ return [None, "中文"]
21
+
22
+
23
+ def check(state):
24
+ require(state != "invalid", message="invalid record")
25
+
26
+
27
+ @precondition(check)
28
+ def checked(anno):
29
+ return anno.generate(anno.state, max_length=512).text
30
+
31
+
32
+ class ClientTests(unittest.TestCase):
33
+ def client(self, backend=None, **options):
34
+ backend = backend or FakeBackend()
35
+ return AnnoClient(AnnoRuntime(backend, backend, **options))
36
+
37
+ def test_results_preconditions_order_indices_and_repeated_submissions(self):
38
+ with self.client() as client:
39
+ for ordered in (False, True):
40
+ with client.submit(checked, ["a", "invalid", "b"], ordered=ordered) as stream:
41
+ result = list(stream)
42
+ by_index = {r.index: r for r in result}
43
+ self.assertEqual(by_index[0].value, "a")
44
+ self.assertEqual(by_index[1].error.code, "invalid_input")
45
+ self.assertEqual(by_index[2].value, "b")
46
+ if ordered:
47
+ self.assertEqual([r.index for r in result], [0, 1, 2])
48
+ self.assertEqual(list(client.submit(constant, [])), [])
49
+ self.assertEqual(list(client.submit(constant, ["x"]))[0].value, [None, "中文"])
50
+ self.assertEqual(list(client.submit(echo, [float("nan")]))[0].error.code, "invalid_input")
51
+ self.assertFalse(client._thread.is_alive())
52
+ self.assertTrue(client.runtime.closed)
53
+ client.close()
54
+
55
+ def test_checker_defect_prevents_all_model_calls(self):
56
+ seen = []
57
+ def broken(state):
58
+ seen.append(state)
59
+ if state == "bad":
60
+ raise RuntimeError("checker defect")
61
+ with self.client() as client, patch("annoscript._client_common._PRECONDITIONS", {echo: (broken,)}):
62
+ with self.assertRaisesRegex(RuntimeError, "checker defect"):
63
+ client.submit(echo, ["first", "bad"])
64
+ self.assertIsNone(client._thread)
65
+ self.assertEqual(client.runtime.llm_backend.calls, [])
66
+ self.assertEqual(seen, ["first", "bad"])
67
+
68
+ def test_per_input_failure_isolated_and_fatal_error_closes_stream(self):
69
+ with self.client(FakeBackend(fail=("bad",))) as client:
70
+ result = list(client.submit(echo, ["bad", "ok"], ordered=True))
71
+ self.assertEqual([r.status for r in result], ["error", "success"])
72
+ class Fatal(FakeBackend):
73
+ async def infer(self, context, call):
74
+ raise BackendFatalError("auth failed")
75
+ with self.client(Fatal()) as client:
76
+ stream = client.submit(echo, ["a", "b"])
77
+ with self.assertRaisesRegex(BackendFatalError, "auth failed"):
78
+ list(stream)
79
+ self.assertTrue(stream.closed)
80
+
81
+ def test_close_stops_queued_work_without_waiting_for_model(self):
82
+ class Controlled(FakeBackend):
83
+ async def infer(self, context, call):
84
+ self.count += 1
85
+ self.started.set()
86
+ try:
87
+ await asyncio.Event().wait()
88
+ finally:
89
+ self.cancelled.set()
90
+ backend = Controlled()
91
+ backend.started, backend.cancelled, backend.count = Event(), Event(), 0
92
+ with self.client(backend, max_llm_inflight=1) as client:
93
+ stream = client.submit(echo, ["a", "b", "c"])
94
+ self.assertTrue(backend.started.wait(2))
95
+ stream.close()
96
+ self.assertTrue(backend.cancelled.wait(2))
97
+ self.assertEqual(backend.count, 1)
98
+ self.assertFalse(client.runtime.closed)
99
+ self.assertEqual(list(stream), [])
100
+
101
+ def test_client_does_not_execute_program_function(self):
102
+ with self.client() as client, patch("annoscript.anno.Anno.__init__", side_effect=AssertionError("must not construct AnnoScript")):
103
+ result = list(client.submit(mixed, ["state"]))
104
+ self.assertEqual(result[0].value, [0.8, "a", 0])
105
+
106
+ def test_runtime_has_one_client_owner(self):
107
+ with self.client() as client:
108
+ with self.assertRaisesRegex(ValueError, "unstarted"):
109
+ AnnoClient(client.runtime)
110
+
111
+ def test_results_are_delivered_out_of_order_then_can_be_ordered(self):
112
+ class Ordered(FakeBackend):
113
+ async def infer(self, context, call):
114
+ if call.arguments["instructions"] == "slow":
115
+ if self.release is None:
116
+ self.release = asyncio.Event()
117
+ await self.release.wait()
118
+ await asyncio.sleep(0)
119
+ else:
120
+ if self.release is None:
121
+ self.release = asyncio.Event()
122
+ self.release.set()
123
+ return InferenceOutput(GenerateResponse(call.arguments["instructions"]))
124
+ for ordered in (False, True):
125
+ backend = Ordered()
126
+ backend.release = None
127
+ with self.client(backend) as client:
128
+ result = list(client.submit(echo, ["slow", "fast"], ordered=ordered))
129
+ self.assertEqual([r.index for r in result], [0, 1] if ordered else [1, 0])
130
+
131
+
132
+ if __name__ == "__main__":
133
+ unittest.main()
@@ -0,0 +1,111 @@
1
+ """Field-bound extraction and simplified car requests; no live services."""
2
+ import json
3
+ import unittest
4
+ from dataclasses import replace
5
+
6
+ import httpx
7
+
8
+
9
+ from annoscript.backends import InferenceContext, InferenceError
10
+ from annoscript._test_evaluation_http import FixtureLLMBackend as LLMAPIBackend
11
+ from annoscript.backends.base import normalize_call, validate_response
12
+ from annoscript.backends._bounded_grammar import grammar
13
+ from annoscript.backends._generation_policy import effective_budget
14
+ from annoscript.client import AnnoClient
15
+ from annoscript.compiler import compile_program, CompileError
16
+ from annoscript.anno import ExtractResponse
17
+ from annoscript.plan_lowering import lower_to_plan
18
+ from annoscript.runtime import AnnoRuntime, RunInput
19
+ from annoscript._test_evaluation_http import llm_content
20
+ from annoscript.test_generation_policy import policy
21
+ from annoscript.test_llm_batch import completion
22
+
23
+
24
+ def extract(context, **options):
25
+ return normalize_call("e", "extract", ("抽取原文",),
26
+ {"max_items": 2, "max_length_per_item": 300, **options}, context)
27
+
28
+
29
+ def obsolete_source(anno):
30
+ return anno.extract("原文", source="text", max_items=1, max_length_per_item=20).items
31
+
32
+
33
+ class SourceFieldTests(unittest.TestCase):
34
+ def test_obsolete_api_fails_before_configuration_or_execution(self):
35
+ with self.assertRaisesRegex(TypeError, "source_layout was removed"):
36
+ AnnoClient.from_env(source_layout="inline")
37
+ for optimize in (False, True):
38
+ with self.assertRaisesRegex(CompileError, "source"):
39
+ compile_program(obsolete_source, optimize=optimize)
40
+
41
+ def test_resolution_snapshot_literal_keys_and_invalid_combinations(self):
42
+ data = {"text": "甲乙", "other": "丙丁", "nested": {"text": "错误"}}
43
+ context = InferenceContext(data)
44
+ call = extract(context, source_field="other", max_num_tokens=100)
45
+ data["other"] = "已改变"
46
+ self.assertEqual(call.extraction_source, "丙丁")
47
+ self.assertNotIn("source", call.arguments)
48
+ self.assertEqual(effective_budget(call), 100)
49
+ self.assertEqual(validate_response(call, ExtractResponse(["丙"])).items, ["丙"])
50
+ with self.assertRaises(InferenceError):
51
+ validate_response(call, ExtractResponse(["甲"]))
52
+ self.assertEqual(extract(InferenceContext("甲乙")).extraction_source, "甲乙")
53
+ self.assertEqual(extract(InferenceContext({"a.b": "literal"}), source_field="a.b").extraction_source, "literal")
54
+ for state, options in [
55
+ (data, {}), (data, {"source_field": "missing"}), (data, {"source_field": "nested"}),
56
+ (data, {"source_field": "nested.text"}), ({"text": 1}, {"source_field": "text"}),
57
+ ("甲乙", {"source_field": "text"}), ([], {}), (data, {"source_field": True}),
58
+ (data, {"source": "甲乙"}),
59
+ ]:
60
+ with self.subTest(state=state, options=options), self.assertRaises(ValueError):
61
+ extract(InferenceContext(state), **options)
62
+ for budget in (0, -1, True, 1.5):
63
+ with self.assertRaises(ValueError):
64
+ extract(context, source_field="text", max_num_tokens=budget)
65
+
66
+ def test_source_isolation_in_grammar_and_prepared_cache(self):
67
+ backend = LLMAPIBackend("http://offline.invalid", "fixture", "fixture",
68
+ profile="qwen_vllm", extract_mode="substring_chars")
69
+ a, b = InferenceContext({"text": "甲乙"}), InferenceContext({"text": "丙丁"})
70
+ ca, cb = extract(a, source_field="text"), extract(b, source_field="text")
71
+ self.assertNotEqual(grammar([ca], extract_mode="substring_chars"),
72
+ grammar([cb], extract_mode="substring_chars"))
73
+ pa, pb = backend._prepare(a, [ca], 100), backend._prepare(b, [cb], 100)
74
+ self.assertNotEqual(pa.binding, pb.binding)
75
+ self.assertNotEqual(pa.body, pb.body)
76
+ with self.assertRaisesRegex(ValueError, "binding differs"):
77
+ backend._prepare(b, [ca], 100)
78
+ with self.assertRaises(ValueError):
79
+ effective_budget(replace(ca, extraction_source=None))
80
+
81
+
82
+ class CarWireTests(unittest.IsolatedAsyncioTestCase):
83
+
84
+
85
+ async def test_explicit_extract_budget_avoids_9760_but_long_input_fails_locally(self):
86
+ context = InferenceContext({"text": "原文"})
87
+ call = normalize_call("e", "extract", ("抽取",),
88
+ {"source_field": "text", "max_items": 16, "max_length_per_item": 300,
89
+ "max_num_tokens": 1440}, context)
90
+ self.assertEqual(effective_budget(call), 1440)
91
+ for input_tokens, expected_count in ((6752, 1), (6753, 0)):
92
+ sent = []
93
+
94
+ def handle(request):
95
+ sent.append(request)
96
+ return completion('{"items":["原文"]}')
97
+
98
+ backend = LLMAPIBackend("http://offline.invalid", "fixture", "fixture",
99
+ profile="qwen_vllm", request_policy=policy(),
100
+ _token_counter=lambda p: input_tokens,
101
+ transport=httpx.MockTransport(handle))
102
+ try:
103
+ if expected_count:
104
+ await backend.infer(context, call)
105
+ else:
106
+ with self.assertRaises(InferenceError) as caught:
107
+ await backend.infer(context, call)
108
+ self.assertEqual(caught.exception.code, "context_length_exceeded")
109
+ self.assertEqual(len(sent), expected_count)
110
+ finally:
111
+ await backend.aclose()