traceforge-toolkit 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.
- traceforge/__init__.py +72 -0
- traceforge/__main__.py +5 -0
- traceforge/_generated.py +254 -0
- traceforge/adapters/__init__.py +12 -0
- traceforge/adapters/base.py +82 -0
- traceforge/adapters/genai_otel.py +164 -0
- traceforge/adapters/mapped_json.py +297 -0
- traceforge/adapters/otel.py +220 -0
- traceforge/boundary/__init__.py +46 -0
- traceforge/boundary/data/boundary-model.joblib +0 -0
- traceforge/boundary/decode.py +87 -0
- traceforge/boundary/features.py +137 -0
- traceforge/boundary/inference.py +267 -0
- traceforge/boundary/inferencer.py +146 -0
- traceforge/classify/__init__.py +95 -0
- traceforge/classify/cmd.py +109 -0
- traceforge/classify/coding.py +213 -0
- traceforge/classify/config.py +554 -0
- traceforge/classify/core.py +266 -0
- traceforge/classify/data/binary_info.yaml +358 -0
- traceforge/classify/data/canonical_tools.yaml +251 -0
- traceforge/classify/data/effect_overrides.yaml +480 -0
- traceforge/classify/data/mcp_profiles.yaml +711 -0
- traceforge/classify/data/recommendation_rules.yaml +111 -0
- traceforge/classify/data/risk.yaml +534 -0
- traceforge/classify/data/shell_defaults.yaml +95 -0
- traceforge/classify/data/shell_rules.yaml +1192 -0
- traceforge/classify/data/tool_classifications.yaml +215 -0
- traceforge/classify/data/verb_inference.yaml +218 -0
- traceforge/classify/mcp.py +181 -0
- traceforge/classify/phases.py +75 -0
- traceforge/classify/powershell.py +93 -0
- traceforge/classify/registry.py +138 -0
- traceforge/classify/risk.py +553 -0
- traceforge/classify/rules.py +215 -0
- traceforge/classify/schema.yaml +282 -0
- traceforge/classify/shell.py +503 -0
- traceforge/classify/tools.py +91 -0
- traceforge/classify/workflow.py +32 -0
- traceforge/cli/__init__.py +42 -0
- traceforge/cli/config_cmd.py +130 -0
- traceforge/cli/detect.py +46 -0
- traceforge/cli/download_cmd.py +74 -0
- traceforge/cli/factory.py +60 -0
- traceforge/cli/gate_cmd.py +30 -0
- traceforge/cli/init_cmd.py +86 -0
- traceforge/cli/replay.py +130 -0
- traceforge/cli/runner.py +181 -0
- traceforge/cli/score.py +217 -0
- traceforge/cli/status.py +65 -0
- traceforge/cli/watch.py +297 -0
- traceforge/config/__init__.py +80 -0
- traceforge/config/defaults.py +149 -0
- traceforge/config/loader.py +239 -0
- traceforge/config/mappings.py +104 -0
- traceforge/config/models.py +579 -0
- traceforge/enricher.py +576 -0
- traceforge/formatting/__init__.py +12 -0
- traceforge/formatting/budget.py +92 -0
- traceforge/formatting/density.py +154 -0
- traceforge/gate/__init__.py +17 -0
- traceforge/gate/client.py +145 -0
- traceforge/gate/external.py +305 -0
- traceforge/gate/registry.py +108 -0
- traceforge/gate/server.py +174 -0
- traceforge/gates/__init__.py +8 -0
- traceforge/gates/pii.py +352 -0
- traceforge/gates/pii_patterns.yaml +228 -0
- traceforge/governance/__init__.py +121 -0
- traceforge/governance/assessor.py +441 -0
- traceforge/governance/budget.py +59 -0
- traceforge/governance/canonical.py +56 -0
- traceforge/governance/codec.py +414 -0
- traceforge/governance/context.py +249 -0
- traceforge/governance/drift.py +196 -0
- traceforge/governance/emitter.py +234 -0
- traceforge/governance/envelope.py +138 -0
- traceforge/governance/ifc.py +364 -0
- traceforge/governance/integrity.py +162 -0
- traceforge/governance/labeler.py +250 -0
- traceforge/governance/mcp_drift.py +328 -0
- traceforge/governance/monitor.py +539 -0
- traceforge/governance/observer.py +276 -0
- traceforge/governance/persistence.py +401 -0
- traceforge/governance/phase1.py +77 -0
- traceforge/governance/pii.py +276 -0
- traceforge/governance/pipeline.py +840 -0
- traceforge/governance/registry.py +110 -0
- traceforge/governance/results.py +183 -0
- traceforge/governance/risk_wrapper.py +113 -0
- traceforge/governance/rules.py +368 -0
- traceforge/governance/scorer.py +262 -0
- traceforge/governance/shield.py +318 -0
- traceforge/governance/state.py +466 -0
- traceforge/governance/types.py +176 -0
- traceforge/mappings/__init__.py +1 -0
- traceforge/mappings/aider.yaml +216 -0
- traceforge/mappings/aider_markdown.yaml +113 -0
- traceforge/mappings/amazonq.yaml +56 -0
- traceforge/mappings/antigravity.yaml +88 -0
- traceforge/mappings/claude.yaml +93 -0
- traceforge/mappings/cline.yaml +286 -0
- traceforge/mappings/codex.yaml +158 -0
- traceforge/mappings/continue_dev.yaml +57 -0
- traceforge/mappings/copilot.yaml +266 -0
- traceforge/mappings/copilot_markdown.yaml +72 -0
- traceforge/mappings/copilot_vscode.yaml +181 -0
- traceforge/mappings/crewai.yaml +592 -0
- traceforge/mappings/goose.yaml +128 -0
- traceforge/mappings/langgraph.yaml +165 -0
- traceforge/mappings/maf.yaml +90 -0
- traceforge/mappings/maf_transcript.yaml +99 -0
- traceforge/mappings/openai_agents.yaml +144 -0
- traceforge/mappings/opencode.yaml +473 -0
- traceforge/mappings/openhands.yaml +320 -0
- traceforge/mappings/pydantic_ai.yaml +183 -0
- traceforge/mappings/smolagents.yaml +89 -0
- traceforge/mappings/sweagent.yaml +82 -0
- traceforge/migrations/__init__.py +1 -0
- traceforge/migrations/env.py +60 -0
- traceforge/migrations/models.py +145 -0
- traceforge/migrations/runner.py +44 -0
- traceforge/migrations/script.py.mako +25 -0
- traceforge/migrations/versions/0001_initial.py +176 -0
- traceforge/migrations/versions/__init__.py +4 -0
- traceforge/models.py +29 -0
- traceforge/parsers/__init__.py +21 -0
- traceforge/parsers/aider.py +416 -0
- traceforge/parsers/base.py +226 -0
- traceforge/parsers/copilot.py +314 -0
- traceforge/phase/__init__.py +50 -0
- traceforge/phase/data/phase-model.joblib +0 -0
- traceforge/phase/data/potion-base-8M/README.md +99 -0
- traceforge/phase/data/potion-base-8M/config.json +13 -0
- traceforge/phase/data/potion-base-8M/model.safetensors +0 -0
- traceforge/phase/data/potion-base-8M/modules.json +14 -0
- traceforge/phase/data/potion-base-8M/tokenizer.json +1 -0
- traceforge/phase/event_rows.py +107 -0
- traceforge/phase/features.py +468 -0
- traceforge/phase/inference.py +279 -0
- traceforge/phase/inferencer.py +171 -0
- traceforge/phase/segmentation.py +258 -0
- traceforge/pipeline.py +891 -0
- traceforge/preprocessors/__init__.py +34 -0
- traceforge/preprocessors/amazonq.py +224 -0
- traceforge/preprocessors/antigravity.py +116 -0
- traceforge/preprocessors/claude.py +95 -0
- traceforge/preprocessors/cline.py +36 -0
- traceforge/preprocessors/codex.py +311 -0
- traceforge/preprocessors/continue_dev.py +119 -0
- traceforge/preprocessors/copilot_vscode.py +171 -0
- traceforge/preprocessors/goose.py +156 -0
- traceforge/preprocessors/maf_transcript.py +84 -0
- traceforge/preprocessors/openai_agents.py +86 -0
- traceforge/preprocessors/opencode.py +85 -0
- traceforge/preprocessors/openhands.py +36 -0
- traceforge/preprocessors/pydantic_ai.py +62 -0
- traceforge/preprocessors/registry.py +24 -0
- traceforge/preprocessors/smolagents.py +90 -0
- traceforge/py.typed +0 -0
- traceforge/sdk/__init__.py +59 -0
- traceforge/sdk/gate_policy.py +63 -0
- traceforge/sdk/gate_types.py +140 -0
- traceforge/sdk/pipeline.py +265 -0
- traceforge/sdk/verdict.py +81 -0
- traceforge/sinks/__init__.py +23 -0
- traceforge/sinks/base.py +95 -0
- traceforge/sinks/callback.py +132 -0
- traceforge/sinks/console.py +112 -0
- traceforge/sinks/factory.py +94 -0
- traceforge/sinks/jsonl.py +125 -0
- traceforge/sinks/otel_exporter.py +212 -0
- traceforge/sinks/parquet.py +260 -0
- traceforge/sinks/s3.py +206 -0
- traceforge/sinks/sqlite_output.py +234 -0
- traceforge/sinks/webhook.py +136 -0
- traceforge/sources/__init__.py +18 -0
- traceforge/sources/auto_detect.py +173 -0
- traceforge/sources/base.py +45 -0
- traceforge/sources/file_poll.py +136 -0
- traceforge/sources/file_watch.py +221 -0
- traceforge/sources/http_poll.py +141 -0
- traceforge/sources/replay.py +63 -0
- traceforge/sources/sqlite.py +187 -0
- traceforge/sources/sse.py +198 -0
- traceforge/telemetry/__init__.py +192 -0
- traceforge/title/__init__.py +23 -0
- traceforge/title/_resolve.py +79 -0
- traceforge/title/context.py +295 -0
- traceforge/title/data/boilerplate_files.json +14 -0
- traceforge/title/heuristics.py +429 -0
- traceforge/title/hygiene.py +90 -0
- traceforge/title/inference.py +314 -0
- traceforge/title/inferencer.py +477 -0
- traceforge/title/naming.py +398 -0
- traceforge/trace.py +291 -0
- traceforge/tracking/__init__.py +30 -0
- traceforge/tracking/models.py +115 -0
- traceforge/tracking/phase_tracker.py +288 -0
- traceforge/types.py +315 -0
- traceforge_toolkit-0.1.0.dist-info/METADATA +188 -0
- traceforge_toolkit-0.1.0.dist-info/RECORD +205 -0
- traceforge_toolkit-0.1.0.dist-info/WHEEL +4 -0
- traceforge_toolkit-0.1.0.dist-info/entry_points.txt +2 -0
- traceforge_toolkit-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
"""Torch-free, CPU-only title generation for activity/step spans.
|
|
2
|
+
|
|
3
|
+
Serves the tiny T5 titler via :mod:`onnxruntime` + :mod:`tokenizers` + numpy
|
|
4
|
+
only -- no torch / transformers at inference (RSS ~250MB vs ~1GB with torch).
|
|
5
|
+
Single-pass beam search: the model emits ``num_beams`` candidates per span and
|
|
6
|
+
:func:`traceforge.title.hygiene.best_of` picks the first non-degenerate one.
|
|
7
|
+
|
|
8
|
+
The candidate-fusion 2-pass variant was evaluated and dropped: it overfit the
|
|
9
|
+
torch beam-search candidate distribution and degraded on the ORT generator, so
|
|
10
|
+
the robust shipped path is single-pass (see research notes / experiments).
|
|
11
|
+
|
|
12
|
+
Inputs are *distilled context* strings (intent / actions / files / symbols /
|
|
13
|
+
notes slots) produced upstream by the span feature builder. Generation params
|
|
14
|
+
mirror the training/eval config (no_repeat_ngram_size=2, repetition_penalty=1.3,
|
|
15
|
+
length_penalty=0.8) so served titles match offline evaluation.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import os
|
|
21
|
+
import re
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
import numpy as np
|
|
25
|
+
|
|
26
|
+
from .hygiene import best_of
|
|
27
|
+
|
|
28
|
+
#: T5 special ids: pad doubles as the decoder start token; eos == </s>.
|
|
29
|
+
_PAD, _EOS = 0, 1
|
|
30
|
+
#: Task prefix the titler was fine-tuned with (learned; must match training).
|
|
31
|
+
_PREFIX = "summarize agent step: "
|
|
32
|
+
#: Encoder truncation; the saved tokenizer.json otherwise bakes in max_length=20.
|
|
33
|
+
_MAX_SRC = 512
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _logsoftmax(x: np.ndarray) -> np.ndarray:
|
|
37
|
+
x = x - x.max(-1, keepdims=True)
|
|
38
|
+
return x - np.log(np.exp(x).sum(-1, keepdims=True))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
#: Surface-level grounding. A tiny seq2seq model under noisy contexts invents
|
|
42
|
+
#: identifier-shaped tokens that the span never mentions (e.g. ``_init_admissment``,
|
|
43
|
+
#: ``github-mcp-server-ample``). These are, by definition, ungrounded: a faithful
|
|
44
|
+
#: title can only name identifiers that appear in its own distilled context. The
|
|
45
|
+
#: rule is structural (no source tags, no thresholds, no tuned magic): a content
|
|
46
|
+
#: word carrying an identifier signal -- snake/path separator, hyphenation, an
|
|
47
|
+
#: internal capital, a digit, or a dotted extension -- must occur verbatim in the
|
|
48
|
+
#: context or the candidate that emitted it is demoted below grounded siblings.
|
|
49
|
+
#: Gated by ``TITLE_GROUND`` (default on) purely so the effect can be A/B measured.
|
|
50
|
+
_GROUND_DEFAULT = os.environ.get("TITLE_GROUND", "1") != "0"
|
|
51
|
+
#: Beam width / candidate pool. Wider gives the grounding gate more grounded
|
|
52
|
+
#: specifics to promote over a collapsed hallucination, at linear decode cost;
|
|
53
|
+
#: env-overridable purely so the footprint/quality trade can be measured.
|
|
54
|
+
_BEAMS_DEFAULT = int(os.environ.get("TITLE_BEAMS", "5"))
|
|
55
|
+
#: Adaptive escalation ceiling. When *every* base-width beam invents an
|
|
56
|
+
#: out-of-context identifier (a decode collapse), and only then, the pool is
|
|
57
|
+
#: re-decoded at this width so grounding has a faithful candidate to promote.
|
|
58
|
+
#: The grounded majority never pays it. Equal to base width disables escalation.
|
|
59
|
+
_BEAMS_MAX = int(os.environ.get("TITLE_BEAMS_MAX", str(2 * _BEAMS_DEFAULT)))
|
|
60
|
+
_WORD_RE = re.compile(r"[A-Za-z0-9_./\\-]+")
|
|
61
|
+
_ID_RE = re.compile(r"[_/\\-]|[a-z][A-Z]|\d|\.[A-Za-z]")
|
|
62
|
+
_GROUND_STOP = frozenset(
|
|
63
|
+
{
|
|
64
|
+
"the",
|
|
65
|
+
"a",
|
|
66
|
+
"an",
|
|
67
|
+
"to",
|
|
68
|
+
"of",
|
|
69
|
+
"in",
|
|
70
|
+
"on",
|
|
71
|
+
"for",
|
|
72
|
+
"and",
|
|
73
|
+
"or",
|
|
74
|
+
"with",
|
|
75
|
+
"from",
|
|
76
|
+
"into",
|
|
77
|
+
"by",
|
|
78
|
+
"at",
|
|
79
|
+
"as",
|
|
80
|
+
"its",
|
|
81
|
+
"their",
|
|
82
|
+
"this",
|
|
83
|
+
"that",
|
|
84
|
+
"these",
|
|
85
|
+
"those",
|
|
86
|
+
}
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _identifier_words(title: str) -> list[str]:
|
|
91
|
+
"""Identifier-shaped content words of a title (skipping the leading verb)."""
|
|
92
|
+
ws = _WORD_RE.findall(title)
|
|
93
|
+
return [w for w in ws[1:] if w.lower() not in _GROUND_STOP and _ID_RE.search(w)]
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _is_grounded(title: str, ctx_lower: str) -> bool:
|
|
97
|
+
"""True iff every identifier-shaped content word appears in the context."""
|
|
98
|
+
return all(w.lower() in ctx_lower for w in _identifier_words(title))
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _ground_order(cands: list[str], context: str) -> list[str]:
|
|
102
|
+
"""Stable-reorder candidates so context-grounded ones come first.
|
|
103
|
+
|
|
104
|
+
Ungrounded candidates are appended rather than dropped, so selection never
|
|
105
|
+
returns empty when *every* beam hallucinates -- the best-scoring (still
|
|
106
|
+
ungrounded) title is then surfaced, and downstream hygiene cleans it.
|
|
107
|
+
"""
|
|
108
|
+
ctx_lower = context.lower()
|
|
109
|
+
grounded: list[str] = []
|
|
110
|
+
ungrounded: list[str] = []
|
|
111
|
+
for c in cands:
|
|
112
|
+
(grounded if _is_grounded(c, ctx_lower) else ungrounded).append(c)
|
|
113
|
+
return grounded + ungrounded
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class TitleModel:
|
|
117
|
+
"""Loaded ORT titler. Construct via :meth:`load`; call :meth:`title`."""
|
|
118
|
+
|
|
119
|
+
def __init__(self, enc, dec, tok, prefix: str = _PREFIX) -> None:
|
|
120
|
+
self._enc = enc
|
|
121
|
+
self._dec = dec
|
|
122
|
+
self._tok = tok
|
|
123
|
+
self._prefix = prefix
|
|
124
|
+
|
|
125
|
+
# ----------------------------------------------------------------- loading
|
|
126
|
+
@classmethod
|
|
127
|
+
def load(
|
|
128
|
+
cls, model_dir: str | os.PathLike[str] | None = None, threads: int = 1
|
|
129
|
+
) -> "TitleModel":
|
|
130
|
+
"""Load the packaged int8 titler (or a custom ``model_dir``).
|
|
131
|
+
|
|
132
|
+
``threads`` caps onnxruntime intra-op threads to keep the live CPU
|
|
133
|
+
footprint near-zero (fan-spin / RAM starvation is a hard failure). The
|
|
134
|
+
default of ``1`` matches what the live :class:`TitleInferencer` serves, so
|
|
135
|
+
direct callers and footprint benchmarks measure the shipped config.
|
|
136
|
+
"""
|
|
137
|
+
import onnxruntime as ort
|
|
138
|
+
from tokenizers import Tokenizer
|
|
139
|
+
|
|
140
|
+
from ._resolve import INSTALL_HINT, span_dir
|
|
141
|
+
|
|
142
|
+
if model_dir is not None:
|
|
143
|
+
d = Path(model_dir)
|
|
144
|
+
else:
|
|
145
|
+
resolved = span_dir()
|
|
146
|
+
if resolved is None:
|
|
147
|
+
raise FileNotFoundError(INSTALL_HINT)
|
|
148
|
+
d = resolved
|
|
149
|
+
so = ort.SessionOptions()
|
|
150
|
+
so.intra_op_num_threads = threads
|
|
151
|
+
so.inter_op_num_threads = 1
|
|
152
|
+
providers = ["CPUExecutionProvider"]
|
|
153
|
+
enc = ort.InferenceSession(str(d / "encoder.onnx"), so, providers=providers)
|
|
154
|
+
dec = ort.InferenceSession(str(d / "decoder.onnx"), so, providers=providers)
|
|
155
|
+
tok = Tokenizer.from_file(str(d / "tokenizer.json"))
|
|
156
|
+
tok.enable_truncation(max_length=_MAX_SRC)
|
|
157
|
+
return cls(enc, dec, tok)
|
|
158
|
+
|
|
159
|
+
# -------------------------------------------------------------- generation
|
|
160
|
+
def _encode(self, text: str):
|
|
161
|
+
e = self._tok.encode(self._prefix + text)
|
|
162
|
+
ids = np.asarray([e.ids], dtype=np.int64)
|
|
163
|
+
mask = np.asarray([e.attention_mask], dtype=np.int64)
|
|
164
|
+
h = self._enc.run(None, {"input_ids": ids, "attention_mask": mask})[0]
|
|
165
|
+
return h, mask
|
|
166
|
+
|
|
167
|
+
def _beams(
|
|
168
|
+
self,
|
|
169
|
+
text: str,
|
|
170
|
+
num_beams: int,
|
|
171
|
+
num_return: int,
|
|
172
|
+
max_new: int,
|
|
173
|
+
no_repeat: int,
|
|
174
|
+
rep_pen: float,
|
|
175
|
+
len_pen: float,
|
|
176
|
+
) -> list[str]:
|
|
177
|
+
h, mask = self._encode(text)
|
|
178
|
+
B = num_beams
|
|
179
|
+
hB = np.repeat(h, B, axis=0)
|
|
180
|
+
mB = np.repeat(mask, B, axis=0)
|
|
181
|
+
beams: list[list[int]] = [[_PAD]]
|
|
182
|
+
scores = np.zeros(1, dtype=np.float64)
|
|
183
|
+
done: list[tuple[float, list[int]]] = []
|
|
184
|
+
for _ in range(max_new):
|
|
185
|
+
n = len(beams)
|
|
186
|
+
ids = np.asarray(beams, dtype=np.int64)
|
|
187
|
+
raw = self._dec.run(
|
|
188
|
+
None,
|
|
189
|
+
{
|
|
190
|
+
"input_ids": ids,
|
|
191
|
+
"encoder_hidden_states": hB[:n],
|
|
192
|
+
"encoder_attention_mask": mB[:n],
|
|
193
|
+
},
|
|
194
|
+
)[0][:, -1, :].astype(np.float64)
|
|
195
|
+
for i, toks in enumerate(beams):
|
|
196
|
+
for t in set(toks): # HF repetition penalty on raw logits
|
|
197
|
+
raw[i, t] = raw[i, t] / rep_pen if raw[i, t] > 0 else raw[i, t] * rep_pen
|
|
198
|
+
if no_repeat and len(toks) >= no_repeat: # ban repeated n-grams
|
|
199
|
+
pref = tuple(toks[-(no_repeat - 1) :]) if no_repeat > 1 else ()
|
|
200
|
+
for k in range(len(toks) - no_repeat + 1):
|
|
201
|
+
if tuple(toks[k : k + no_repeat - 1]) == pref:
|
|
202
|
+
raw[i, toks[k + no_repeat - 1]] = -1e9
|
|
203
|
+
lp = _logsoftmax(raw)
|
|
204
|
+
cand = (scores[:n, None] + lp).reshape(-1)
|
|
205
|
+
V = lp.shape[1]
|
|
206
|
+
order = np.argpartition(cand, -2 * B)[-2 * B :]
|
|
207
|
+
order = order[np.argsort(cand[order])[::-1]]
|
|
208
|
+
new_beams: list[list[int]] = []
|
|
209
|
+
new_scores: list[float] = []
|
|
210
|
+
for idx in order:
|
|
211
|
+
bi, tok = idx // V, int(idx % V)
|
|
212
|
+
toks = beams[bi] + [tok]
|
|
213
|
+
if tok == _EOS:
|
|
214
|
+
done.append((cand[idx] / (len(toks) ** len_pen), toks))
|
|
215
|
+
else:
|
|
216
|
+
new_beams.append(toks)
|
|
217
|
+
new_scores.append(cand[idx])
|
|
218
|
+
if len(new_beams) >= B:
|
|
219
|
+
break
|
|
220
|
+
if not new_beams:
|
|
221
|
+
break
|
|
222
|
+
beams, scores = new_beams, np.asarray(new_scores)
|
|
223
|
+
if len(done) >= B: # early_stopping=True: B finished hypotheses
|
|
224
|
+
break
|
|
225
|
+
# Prefer FINISHED hypotheses; only fall back to unfinished active beams
|
|
226
|
+
# if too few finished (avoids truncated titles).
|
|
227
|
+
done.sort(key=lambda x: x[0], reverse=True)
|
|
228
|
+
if len(done) < num_return:
|
|
229
|
+
done = done + sorted(
|
|
230
|
+
((s / (len(t) ** len_pen), t) for s, t in zip(scores, beams)),
|
|
231
|
+
key=lambda x: x[0],
|
|
232
|
+
reverse=True,
|
|
233
|
+
)
|
|
234
|
+
outs: list[str] = []
|
|
235
|
+
seen: set[str] = set()
|
|
236
|
+
for _score, toks in done:
|
|
237
|
+
t = self._tok.decode([x for x in toks if x not in (_PAD, _EOS)]).strip()
|
|
238
|
+
if t not in seen:
|
|
239
|
+
seen.add(t)
|
|
240
|
+
outs.append(t)
|
|
241
|
+
if len(outs) >= num_return:
|
|
242
|
+
break
|
|
243
|
+
return outs
|
|
244
|
+
|
|
245
|
+
def _decode_grounded(
|
|
246
|
+
self,
|
|
247
|
+
context: str,
|
|
248
|
+
num_beams: int,
|
|
249
|
+
max_new: int,
|
|
250
|
+
no_repeat: int,
|
|
251
|
+
rep_pen: float,
|
|
252
|
+
len_pen: float,
|
|
253
|
+
ground: bool,
|
|
254
|
+
) -> list[str]:
|
|
255
|
+
"""Decode candidates, grounding-ordered, with adaptive escalation.
|
|
256
|
+
|
|
257
|
+
Base-width decode first. Only if grounding is on and *no* base candidate
|
|
258
|
+
is grounded (every beam invented an out-of-context identifier) is the
|
|
259
|
+
pool re-decoded at :data:`_BEAMS_MAX` -- so the faithful majority keeps
|
|
260
|
+
the base footprint and only the collapsing minority pays for more beams.
|
|
261
|
+
"""
|
|
262
|
+
cands = self._beams(context, num_beams, num_beams, max_new, no_repeat, rep_pen, len_pen)
|
|
263
|
+
if not ground:
|
|
264
|
+
return cands
|
|
265
|
+
ctx_low = context.lower()
|
|
266
|
+
if not any(_is_grounded(c, ctx_low) for c in cands) and _BEAMS_MAX > num_beams:
|
|
267
|
+
cands = self._beams(
|
|
268
|
+
context, _BEAMS_MAX, _BEAMS_MAX, max_new, no_repeat, rep_pen, len_pen
|
|
269
|
+
)
|
|
270
|
+
return _ground_order(cands, context)
|
|
271
|
+
|
|
272
|
+
def title(
|
|
273
|
+
self,
|
|
274
|
+
context: str,
|
|
275
|
+
num_beams: int = _BEAMS_DEFAULT,
|
|
276
|
+
max_new: int = 32,
|
|
277
|
+
no_repeat: int = 2,
|
|
278
|
+
rep_pen: float = 1.3,
|
|
279
|
+
len_pen: float = 0.8,
|
|
280
|
+
ground: bool | None = None,
|
|
281
|
+
) -> str:
|
|
282
|
+
"""Return a short imperative title for a distilled span ``context``.
|
|
283
|
+
|
|
284
|
+
Generates ``num_beams`` candidates and returns the first non-degenerate
|
|
285
|
+
one after decode hygiene. When ``ground`` (default :data:`_GROUND_DEFAULT`)
|
|
286
|
+
is on, candidates that name identifiers absent from ``context`` are
|
|
287
|
+
demoted below grounded ones first (with adaptive beam escalation on a
|
|
288
|
+
full collapse).
|
|
289
|
+
"""
|
|
290
|
+
if not context or not context.strip():
|
|
291
|
+
return ""
|
|
292
|
+
g = _GROUND_DEFAULT if ground is None else ground
|
|
293
|
+
cands = self._decode_grounded(context, num_beams, max_new, no_repeat, rep_pen, len_pen, g)
|
|
294
|
+
return best_of(cands)
|
|
295
|
+
|
|
296
|
+
def candidates(
|
|
297
|
+
self,
|
|
298
|
+
context: str,
|
|
299
|
+
num_beams: int = _BEAMS_DEFAULT,
|
|
300
|
+
max_new: int = 32,
|
|
301
|
+
no_repeat: int = 2,
|
|
302
|
+
rep_pen: float = 1.3,
|
|
303
|
+
len_pen: float = 0.8,
|
|
304
|
+
ground: bool | None = None,
|
|
305
|
+
) -> list[str]:
|
|
306
|
+
"""Return the cleaned beam candidates (for sibling de-dup via hygiene).
|
|
307
|
+
|
|
308
|
+
Grounded candidates are ordered first (see :meth:`title`) so both
|
|
309
|
+
:func:`best_of` and :func:`pick_distinct` prefer faithful titles.
|
|
310
|
+
"""
|
|
311
|
+
if not context or not context.strip():
|
|
312
|
+
return []
|
|
313
|
+
g = _GROUND_DEFAULT if ground is None else ground
|
|
314
|
+
return self._decode_grounded(context, num_beams, max_new, no_repeat, rep_pen, len_pen, g)
|