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.
- topoexploit-0.1.0.dist-info/METADATA +523 -0
- topoexploit-0.1.0.dist-info/RECORD +23 -0
- topoexploit-0.1.0.dist-info/WHEEL +5 -0
- topoexploit-0.1.0.dist-info/entry_points.txt +6 -0
- topoexploit-0.1.0.dist-info/licenses/LICENSE +661 -0
- topoexploit-0.1.0.dist-info/top_level.txt +1 -0
- topogpt3/__init__.py +125 -0
- topogpt3/__main__.py +49 -0
- topogpt3/api_server.py +1178 -0
- topogpt3/continuation.py +114 -0
- topogpt3/ewc.py +369 -0
- topogpt3/exploitgym_config.py +130 -0
- topogpt3/exploitgym_loader.py +416 -0
- topogpt3/hodge_cm.py +160 -0
- topogpt3/inference.py +731 -0
- topogpt3/inference_hrm.py +1502 -0
- topogpt3/jlens.py +958 -0
- topogpt3/lens_model.py +389 -0
- topogpt3/merged_config.py +187 -0
- topogpt3/model.py +3824 -0
- topogpt3/structured_output.py +251 -0
- topogpt3/train.py +1939 -0
- topogpt3/vanilla_control.py +240 -0
|
@@ -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)
|