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,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()
|