topoexploit 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.
@@ -0,0 +1,251 @@
1
+ """Constrained decoding for the TopoGPT3 OpenAI-compatible server.
2
+
3
+ DeepInfer exposes structured output through ``SamplingParams.so_args``
4
+ (``json`` / ``regex`` / ``ebnf``). This module mirrors that surface and also
5
+ understands OpenAI-style ``response_format`` payloads:
6
+
7
+ {"type": "text"}
8
+ {"type": "json_object"}
9
+ {"type": "json_schema", "json_schema": {"name": "...", "schema": {...}}}
10
+ {"type": "regex", "regex": "[A-Z]{2}-[0-9]{3}"}
11
+ {"type": "grammar", "grammar": "root ::= ..."}
12
+
13
+ ``xgrammar`` is imported lazily. The server keeps working without it; only
14
+ requests that ask for a constraint fail with a clear error.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import json
20
+ import threading
21
+ from dataclasses import dataclass
22
+ from typing import Any
23
+
24
+ MAX_CACHED_GRAMMARS = 64
25
+
26
+
27
+ class StructuredOutputError(ValueError):
28
+ """Invalid or unsupported structured-output request."""
29
+
30
+
31
+ @dataclass(frozen=True)
32
+ class GrammarSpec:
33
+ kind: str # json_object | json_schema | regex | ebnf
34
+ payload: Any = None
35
+
36
+ def cache_key(self) -> str:
37
+ if isinstance(self.payload, str):
38
+ body = self.payload
39
+ else:
40
+ body = json.dumps(
41
+ self.payload, sort_keys=True, separators=(",", ":"), default=str
42
+ )
43
+ return f"{self.kind}:{body}"
44
+
45
+
46
+ def _import_xgrammar():
47
+ try:
48
+ import xgrammar # type: ignore[import-not-found]
49
+ except ImportError as exc: # pragma: no cover - exercised without extra
50
+ raise StructuredOutputError(
51
+ "structured outputs require xgrammar: pip install 'topogpt3[structured]'"
52
+ ) from exc
53
+ return xgrammar
54
+
55
+
56
+ def parse_response_format(response_format: Any) -> GrammarSpec | None:
57
+ """Translate an OpenAI ``response_format`` payload into a GrammarSpec."""
58
+ if response_format is None:
59
+ return None
60
+ if not isinstance(response_format, dict):
61
+ raise StructuredOutputError("response_format must be an object")
62
+
63
+ kind = response_format.get("type")
64
+ if kind in (None, "text"):
65
+ return None
66
+ if kind == "json_object":
67
+ return GrammarSpec("json_object")
68
+ if kind == "json_schema":
69
+ body = response_format.get("json_schema")
70
+ if body is None:
71
+ body = response_format.get("schema")
72
+ if body is None:
73
+ raise StructuredOutputError("json_schema requires a schema field")
74
+ if isinstance(body, dict) and "schema" in body:
75
+ schema = body["schema"]
76
+ else:
77
+ schema = body
78
+ return GrammarSpec("json_schema", schema)
79
+ if kind == "regex":
80
+ pattern = response_format.get("regex", response_format.get("pattern"))
81
+ if not isinstance(pattern, str) or not pattern:
82
+ raise StructuredOutputError("regex response_format requires a pattern")
83
+ return GrammarSpec("regex", pattern)
84
+ if kind in ("grammar", "ebnf"):
85
+ grammar = response_format.get("grammar", response_format.get("ebnf"))
86
+ if not isinstance(grammar, str) or not grammar:
87
+ raise StructuredOutputError("grammar response_format requires a grammar")
88
+ return GrammarSpec("ebnf", grammar)
89
+
90
+ raise StructuredOutputError(f"unsupported response_format type: {kind!r}")
91
+
92
+
93
+ def parse_so_args(so_args: Any) -> GrammarSpec | None:
94
+ """Translate a DeepInfer-style ``so_args`` payload into a GrammarSpec."""
95
+ if so_args is None:
96
+ return None
97
+ if not isinstance(so_args, dict):
98
+ raise StructuredOutputError("so_args must be an object")
99
+
100
+ schema_type = so_args.get("schema_type")
101
+ schema = so_args.get("schema")
102
+ if schema_type in (None, "text"):
103
+ return None
104
+ if schema_type == "json_object":
105
+ return GrammarSpec("json_object")
106
+ if schema_type == "json":
107
+ if isinstance(schema, str):
108
+ try:
109
+ schema = json.loads(schema)
110
+ except json.JSONDecodeError as exc:
111
+ raise StructuredOutputError(f"invalid JSON schema: {exc}") from exc
112
+ if not isinstance(schema, dict):
113
+ raise StructuredOutputError("schema_type='json' requires a JSON schema")
114
+ return GrammarSpec("json_schema", schema)
115
+ if schema_type == "regex":
116
+ if not isinstance(schema, str) or not schema:
117
+ raise StructuredOutputError("schema_type='regex' requires a pattern")
118
+ return GrammarSpec("regex", schema)
119
+ if schema_type in ("ebnf", "grammar"):
120
+ if not isinstance(schema, str) or not schema:
121
+ raise StructuredOutputError("schema_type='ebnf' requires a grammar")
122
+ return GrammarSpec("ebnf", schema)
123
+
124
+ raise StructuredOutputError(f"unsupported schema_type: {schema_type!r}")
125
+
126
+
127
+ _COMPILER_LOCK = threading.Lock()
128
+ _COMPILERS: dict[tuple[str, int], tuple[Any, int]] = {}
129
+ _COMPILED: dict[tuple[tuple[str, int], str], Any] = {}
130
+
131
+
132
+ def _tokenizer_key(tokenizer: Any) -> tuple[str, int]:
133
+ enc = getattr(tokenizer, "enc", None)
134
+ if enc is None:
135
+ raise StructuredOutputError("tokenizer does not expose a tiktoken encoding")
136
+ return (getattr(enc, "name", "bpe"), int(enc.n_vocab))
137
+
138
+
139
+ def _get_compiler(tokenizer: Any) -> tuple[Any, int]:
140
+ xg = _import_xgrammar()
141
+ enc = tokenizer.enc
142
+ key = _tokenizer_key(tokenizer)
143
+
144
+ with _COMPILER_LOCK:
145
+ cached = _COMPILERS.get(key)
146
+ if cached is not None:
147
+ return cached
148
+
149
+ vocab = [enc.decode_single_token_bytes(i) for i in range(enc.n_vocab)]
150
+ eot = getattr(tokenizer, "eot_token", None)
151
+ stop_ids = [int(eot())] if callable(eot) else None
152
+ tokenizer_info = xg.TokenizerInfo(
153
+ vocab, vocab_size=enc.n_vocab, stop_token_ids=stop_ids
154
+ )
155
+ compiler = xg.GrammarCompiler(tokenizer_info)
156
+
157
+ with _COMPILER_LOCK:
158
+ _COMPILERS.setdefault(key, (compiler, enc.n_vocab))
159
+ return _COMPILERS[key]
160
+
161
+
162
+ def _compile(spec: GrammarSpec, compiler: Any) -> Any:
163
+ xg = _import_xgrammar()
164
+ try:
165
+ if spec.kind == "json_object":
166
+ grammar = xg.Grammar.builtin_json_grammar()
167
+ elif spec.kind == "json_schema":
168
+ grammar = xg.Grammar.from_json_schema(spec.payload)
169
+ elif spec.kind == "regex":
170
+ grammar = xg.Grammar.from_regex(spec.payload)
171
+ elif spec.kind == "ebnf":
172
+ grammar = xg.Grammar.from_ebnf(spec.payload)
173
+ else: # pragma: no cover - guarded by the parsers
174
+ raise StructuredOutputError(f"unsupported grammar kind: {spec.kind}")
175
+ return compiler.compile_grammar(grammar)
176
+ except StructuredOutputError:
177
+ raise
178
+ except Exception as exc:
179
+ raise StructuredOutputError(f"invalid {spec.kind} constraint: {exc}") from exc
180
+
181
+
182
+ class GrammarConstraint:
183
+ """Per-request xgrammar matcher that masks logits in place."""
184
+
185
+ def __init__(self, xg: Any, compiled: Any, vocab_size: int):
186
+ self._xg = xg
187
+ self._compiled = compiled
188
+ self.vocab_size = vocab_size
189
+ self._matcher = self._new_matcher()
190
+ self._bitmask = xg.allocate_token_bitmask(1, vocab_size)
191
+
192
+ def _new_matcher(self):
193
+ return self._xg.GrammarMatcher(
194
+ self._compiled, terminate_without_stop_token=True
195
+ )
196
+
197
+ def reset(self) -> None:
198
+ self._matcher = self._new_matcher()
199
+
200
+ @property
201
+ def is_terminated(self) -> bool:
202
+ return bool(self._matcher.is_terminated())
203
+
204
+ def apply(self, logits: Any) -> None:
205
+ """Mask disallowed tokens in ``logits`` with ``-inf`` in place."""
206
+ if self._matcher.is_terminated():
207
+ return
208
+ self._matcher.fill_next_token_bitmask(self._bitmask, 0)
209
+ self._xg.apply_token_bitmask_inplace(logits, self._bitmask)
210
+
211
+ def accept(self, token_id: int) -> bool:
212
+ """Feed an emitted token to the matcher. Returns False if it is invalid."""
213
+ if self._matcher.is_terminated():
214
+ return True
215
+ return bool(self._matcher.accept_token(int(token_id)))
216
+
217
+
218
+ def build_constraint(
219
+ tokenizer: Any, spec: GrammarSpec | None
220
+ ) -> GrammarConstraint | None:
221
+ """Compile ``spec`` (cached) and return a fresh matcher for one request."""
222
+ if spec is None:
223
+ return None
224
+
225
+ compiler, vocab_size = _get_compiler(tokenizer)
226
+ cache_key = (_tokenizer_key(tokenizer), spec.cache_key())
227
+
228
+ with _COMPILER_LOCK:
229
+ compiled = _COMPILED.get(cache_key)
230
+ if compiled is None:
231
+ compiled = _compile(spec, compiler)
232
+ with _COMPILER_LOCK:
233
+ if len(_COMPILED) >= MAX_CACHED_GRAMMARS:
234
+ _COMPILED.clear()
235
+ _COMPILED[cache_key] = compiled
236
+
237
+ return GrammarConstraint(_import_xgrammar(), compiled, vocab_size)
238
+
239
+
240
+ def constraint_from_request(
241
+ tokenizer: Any,
242
+ response_format: Any = None,
243
+ so_args: Any = None,
244
+ ) -> GrammarConstraint | None:
245
+ """Resolve either request surface into a ready-to-use constraint."""
246
+ if response_format is not None and so_args is not None:
247
+ raise StructuredOutputError("use either response_format or so_args, not both")
248
+ spec = parse_response_format(response_format)
249
+ if spec is None:
250
+ spec = parse_so_args(so_args)
251
+ return build_constraint(tokenizer, spec)