moth-112m / code /moth /backends /engine.py
darioooooo0o's picture
moth 0.3.0
5cf6791 verified
Raw History Blame Contribute Delete
3.53 kB
"""moth's conversation engine (moth.serve.Moth: history, reactions, sessions, the home tools, the phrase memory) on a
stateless backend (transformers, openai). Moth keeps tiny-agent's KV cache across turns and feeds only new text;
here every turn renders the whole (trimmed) chat and asks the backend for the next assistant turn. Everything above
the model is Moth's own code, unchanged: the same rollback, budget trimming, agent cards, one turn per session.
"""
from __future__ import annotations
from moth.backends.manifest import apply_args as apply_manifest
from moth.norepeat import PhraseMemory
from moth.serve import Moth
READY = "ready" # stands in for the logits / cache of the stateful path: there is nothing to keep between turns
class GenericMoth(Moth):
def __init__(self, a, manifest=None, backend=None):
from moth.backends import load_backend, resolve
self.manifest = manifest or getattr(a, "manifest", None) or resolve(a.model)
a = apply_manifest(a, self.manifest)
self.backend = backend or load_backend(self.manifest, a)
self.tok = self.backend.tokenizer()
self.model = self._kv = None
self.quant = "none"
self._reacting = False
self.setup(a)
if not self.backend.phrase_ban: # no token-level sampling (openai): no phrase ban, no carried-over words
self.phrases = PhraseMemory(0)
# --- what the stateful path does with a KV cache ------------------------------------------------------------
def _new_cache(self):
return READY
def _feed(self, ids):
return READY # the next _turn renders the whole chat; the budget is checked there
def _prefill_all(self):
"""Drop the oldest turns until the chat fits CTX_KEEP-worth of tokens (as Moth._prefill_all)."""
while len(self.messages) > 2 and self.backend.count(self._view()) >= self.keep:
i = next((k for k in range(2, len(self.messages)) if self.messages[k]["role"] == "user"), None)
if i is None:
break
self.messages = self.messages[:1] + self.messages[i:]
self.cache = READY
return READY
def _turn(self, logits, talk_only=False) -> str:
if self.backend.count(self._view()) + self.a.max_turn_tokens > self.budget:
self._prefill_all()
return self.backend.generate(
self._view(), max_tokens=self.a.max_turn_tokens, temperature=self.a.temperature, top_p=self.a.top_p,
rep_penalty=getattr(self.a, "rep_penalty", 1.0), pen_ids=self._pen_ids(), ban_tools=talk_only,
logits_hook=self._hook, stop=self.manifest.stop)
def _view(self) -> list[dict]:
"""The chat as the model sees it this turn: moth's bookkeeping keys out, and on a reaction the manifest's
reaction nudge after the agent card (this turn only; history keeps the plain card)."""
msgs = [{k: v for k, v in m.items() if k != "session"} for m in self.messages]
if self._reacting and self.manifest.reaction:
for m in reversed(msgs):
if m["role"] == "user":
m["content"] = f"{m['content']}\n\n{self.manifest.reaction}"
break
return msgs
def react(self, text, hold_for_human=False, item=None):
with self.lock:
self._reacting = True
try:
return super().react(text, hold_for_human=hold_for_human, item=item)
finally:
self._reacting = False