Fix SemIfEngine.generate: sample from the low-level llama.cpp backend

This commit is contained in:
Denton Social
2026-09-23 22:32:48 -05:00
parent 6c980efec0
commit fcefb971f8
2 changed files with 47 additions and 10 deletions
+41 -10
View File
@@ -95,23 +95,54 @@ class SemIfEngine:
def generate( def generate(
self, self,
messages: list[dict], messages: list[dict],
temperature: float = 0.2, temperature: float = 0.0,
max_tokens: int = 256, max_tokens: int = 256,
) -> str: ) -> str:
"""Drive the pinned decision model in the normal way: text generation. """Drive the pinned decision model in the normal way: text generation.
SemIf scoring reads option logits directly; this instead uses the SemIf scoring reads option logits directly; this instead autoregressively
underlying llama.cpp chat-completion endpoint on the same loaded model, samples from the same llama.cpp context, e.g. for skill-tree authoring.
e.g. for skill-tree authoring. Each call resets the KV cache by Generation decodes the chat template through the backend's low-level
default, so interleaving scoring and generation on one model is safe. context (there is no high-level chat-completion object on the CPU
backend), stopping at the tokenizer's eos token. The KV cache is cleared
at the start, so interleaving scoring and generation on one model is safe.
""" """
model, tokenizer, metadata = self._ensure_loaded() model, tokenizer, metadata = self._ensure_loaded()
try: try:
reply = model.create_chat_completion( import numpy
messages=messages,
temperature=temperature, engine = model.engine
max_tokens=max_tokens, prompt_ids = tokenizer.apply_chat_template(
messages,
tokenize=True,
add_generation_prompt=True,
enable_thinking=False,
) )
engine.clear()
logits = engine._decode(prompt_ids, 0, 0, True)
generated: list[int] = []
rng = numpy.random.default_rng()
for position in range(max_tokens):
token = _sample_token(logits, temperature, rng)
if token == tokenizer.eos_token_id:
break
generated.append(token)
logits = engine._decode([token], len(prompt_ids) + position, 0, True)
return tokenizer.decode(generated).strip()
except EngineUnavailable:
raise
except Exception as exc: except Exception as exc:
raise EngineUnavailable(f"generation failed: {exc}") from exc raise EngineUnavailable(f"generation failed: {exc}") from exc
return reply["choices"][0]["message"]["content"].strip()
def _sample_token(logits, temperature: float, rng) -> int:
"""Pick the next token from next-position logits: greedy or temperature."""
import numpy
if temperature <= 0.0:
return int(numpy.argmax(logits))
scaled = numpy.asarray(logits, dtype=numpy.float64) / max(temperature, 1e-6)
scaled = scaled - scaled.max()
probabilities = numpy.exp(scaled)
probabilities /= probabilities.sum()
return int(rng.choice(probabilities.size, p=probabilities))
+6
View File
@@ -84,6 +84,12 @@ def test_generate_category_without_engine_raises():
generate_category(engine, Request("anything"), {}) generate_category(engine, Request("anything"), {})
def test_generate_without_engine_raises():
engine = SemIfEngine(EngineConfig())
with pytest.raises(EngineUnavailable):
engine.generate([{"role": "user", "content": "hi"}])
def test_build_tree_includes_registry_stubs(tmp_path): def test_build_tree_includes_registry_stubs(tmp_path):
registry = CategoryRegistry(str(tmp_path / "categories.json")) registry = CategoryRegistry(str(tmp_path / "categories.json"))
registry.register("delivery", "Track and manage package deliveries.") registry.register("delivery", "Track and manage package deliveries.")