Add live chat runtime

This commit is contained in:
Shay 2026-05-13 20:40:56 -07:00
parent 9ba6abfa3e
commit 0780ca8166
5 changed files with 198 additions and 37 deletions

View file

@ -1,3 +1,3 @@
from .runtime import ChatRuntime from .runtime import ChatResponse, ChatRuntime
__all__ = ["ChatRuntime"] __all__ = ["ChatResponse", "ChatRuntime"]

39
chat/__main__.py Normal file
View file

@ -0,0 +1,39 @@
from __future__ import annotations
try:
import readline # noqa: F401
except ImportError: # pragma: no cover - platform optional
readline = None
from chat.runtime import ChatRuntime
_DIM = "\033[2m"
_RESET = "\033[0m"
def main() -> None:
runtime = ChatRuntime()
while True:
try:
text = input("> ").strip()
except EOFError:
print()
break
if text in {"quit", "exit"}:
break
if not text:
continue
try:
response = runtime.chat(text)
except (KeyError, ValueError) as exc:
print(f"{_DIM}[{exc}]{_RESET}")
continue
print(response.surface)
print(
f"{_DIM}[role={response.dialogue_role} "
f"versor_condition={response.versor_condition:.2e}]{_RESET}"
)
if __name__ == "__main__":
main()

View file

@ -1,25 +1,82 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass
import re import re
from collections.abc import Sequence
from language_packs import OOVPolicy, load_pack, load_pack_entries import numpy as np
from algebra.versor import versor_condition
from generate.dialogue import DialogueRole, classify_dialogue_blade, propose_dialogue
from generate.proposition import FrameRegistry, Proposition, propose
from generate.stream import generate
from language_packs import OOVPolicy, load_mounted_packs, load_pack, load_pack_entries
from persona.motor import PersonaMotor from persona.motor import PersonaMotor
from session.context import SessionContext from session.context import SessionContext
_TOKEN_RE = re.compile(r"\w+", re.UNICODE) _TOKEN_RE = re.compile(r"\w+", re.UNICODE)
_DEFAULT_PACKS = ("en_minimal_v1", "he_logos_micro_v1", "grc_logos_micro_v1")
_SEED_ALIASES = {
"logos": "λόγος",
"dabar": "דבר",
"or": "אור",
"phos": "φῶς",
"zoe": "ζωή",
"arche": "ἀρχή",
"aletheia": "ἀλήθεια",
}
@dataclass(frozen=True, slots=True)
class ChatResponse:
surface: str
proposition: Proposition
dialogue_role: DialogueRole
versor_condition: float
class ChatRuntime: class ChatRuntime:
def __init__(self, pack_id: str = "en_minimal_v1") -> None: def __init__(
manifest, manifold = load_pack(pack_id) self,
entries = load_pack_entries(pack_id) pack_id: str | Sequence[str] = _DEFAULT_PACKS,
self._manifest = manifest *,
frame_pack: str | None = None,
) -> None:
pack_ids = (pack_id,) if isinstance(pack_id, str) else tuple(pack_id)
manifests = []
manifolds = []
entries = []
for mounted_pack_id in pack_ids:
manifest, manifold = load_pack(mounted_pack_id)
manifests.append(manifest)
manifolds.append(manifold)
entries.extend(load_pack_entries(mounted_pack_id))
manifold = manifolds[0] if len(pack_ids) == 1 else load_mounted_packs(pack_ids)
self._manifests = tuple(manifests)
self._context = SessionContext(manifold, persona=PersonaMotor.identity()) self._context = SessionContext(manifold, persona=PersonaMotor.identity())
self._frame_registry = FrameRegistry.from_pack(
frame_pack or self._default_frame_pack(pack_ids),
self._context.vocab,
)
self._surface_by_fold = {e.surface.casefold(): e.surface for e in entries} self._surface_by_fold = {e.surface.casefold(): e.surface for e in entries}
self._surface_by_fold.update(_SEED_ALIASES)
self._pos_by_surface = { self._pos_by_surface = {
e.surface: (e.pos or e.part_of_speech or "X") for e in entries e.surface: (e.pos or e.part_of_speech or "X") for e in entries
} }
@property
def session(self) -> SessionContext:
return self._context
@staticmethod
def _default_frame_pack(pack_ids: tuple[str, ...]) -> str:
if any(pack_id.startswith("grc_") for pack_id in pack_ids):
return "grc"
if any(pack_id.startswith("he_") for pack_id in pack_ids):
return "he"
return "en"
def _tokenize(self, text: str) -> list[str]: def _tokenize(self, text: str) -> list[str]:
tokens: list[str] = [] tokens: list[str] = []
for match in _TOKEN_RE.finditer(text): for match in _TOKEN_RE.finditer(text):
@ -27,6 +84,9 @@ class ChatRuntime:
tokens.append(self._surface_by_fold.get(raw.casefold(), raw)) tokens.append(self._surface_by_fold.get(raw.casefold(), raw))
return tokens return tokens
def tokenize(self, text: str) -> list[str]:
return self._tokenize(text)
def _apply_oov_policy(self, tokens: list[str]) -> list[str]: def _apply_oov_policy(self, tokens: list[str]) -> list[str]:
kept: list[str] = [] kept: list[str] = []
for token in tokens: for token in tokens:
@ -34,9 +94,12 @@ class ChatRuntime:
self._context.vocab.get_versor(token) self._context.vocab.get_versor(token)
kept.append(token) kept.append(token)
except KeyError: except KeyError:
if self._manifest.oov_policy is OOVPolicy.FAIL_CLOSED: if all(manifest.oov_policy is OOVPolicy.FAIL_CLOSED for manifest in self._manifests):
raise raise
if self._manifest.oov_policy is OOVPolicy.PROPOSE_VOCAB_EXPANSION: if any(
manifest.oov_policy is OOVPolicy.PROPOSE_VOCAB_EXPANSION
for manifest in self._manifests
):
raise KeyError(f"OOV token requires vocab proposal: {token}") raise KeyError(f"OOV token requires vocab proposal: {token}")
return kept return kept
@ -51,12 +114,58 @@ class ChatRuntime:
prev_pos = pos prev_pos = pos
return out return out
def respond(self, text: str, max_tokens: int = 32) -> str: def _dialogue_reference(self) -> np.ndarray | None:
blade = self._context.last_dialogue_blade
if blade is None or float(np.linalg.norm(blade)) < 1e-8:
return None
return blade
def chat(self, text: str, max_tokens: int = 32) -> ChatResponse:
tokens = self._tokenize(text) tokens = self._tokenize(text)
filtered = self._apply_oov_policy(tokens) filtered = self._apply_oov_policy(tokens)
if not filtered: if not filtered:
return "" raise ValueError("ChatRuntime.chat() received no in-vocabulary tokens.")
self._context.ingest(filtered)
result = self._context.respond(max_tokens=max_tokens) field_state = self._context.ingest(filtered)
reference_blade = self._dialogue_reference()
base_proposition = propose(field_state, None, self._context.vocab, self._frame_registry)
dialogue_role = classify_dialogue_blade(
base_proposition.relation,
reference_blade,
)
proposition = propose_dialogue(
field_state,
None,
self._context.vocab,
self._frame_registry,
reference_blade,
)
self._context.record_dialogue(proposition)
result = generate(
field_state,
self._context.vocab,
self._context.persona,
max_tokens=max_tokens,
vault=None,
)
self._context.state = result.final_state
self._context.vault.store(
result.final_state.F,
{"turn": self._context.turn, "role": "assistant"},
)
self._context.turn += 1
guarded = self._syntactic_guard(result.tokens) guarded = self._syntactic_guard(result.tokens)
return " ".join(guarded) surface = " ".join(guarded)
return ChatResponse(
surface=surface,
proposition=proposition,
dialogue_role=dialogue_role,
versor_condition=versor_condition(result.final_state.F),
)
def respond(self, text: str, max_tokens: int = 32) -> str:
try:
return self.chat(text, max_tokens=max_tokens).surface
except ValueError:
return ""

View file

@ -1,30 +1,20 @@
from __future__ import annotations from __future__ import annotations
from language_packs import load_mounted_packs from chat.runtime import ChatRuntime
_TRILINGUAL_PACKS = ("en_minimal_v1", "he_logos_micro_v1", "grc_logos_micro_v1")
_SEED_ALIASES = {
"logos": "λόγος",
"dabar": "דבר",
"or": "אור",
"phos": "φῶς",
"zoe": "ζωή",
"arche": "ἀρχή",
"aletheia": "ἀλήθεια",
}
def field_walk(seed: str, steps: int = 4) -> list[str]: def field_walk(seed: str, steps: int = 4) -> list[str]:
vocab = load_mounted_packs(_TRILINGUAL_PACKS) runtime = ChatRuntime()
surface = _SEED_ALIASES.get(seed.casefold(), seed) injected_terms = runtime.tokenize(seed)
F = vocab.get_versor(surface) response = runtime.chat(seed, max_tokens=max(1, steps))
walk = [seed] if surface == seed else [seed, surface] proposition_terms = [
idx = vocab.index_of(surface) response.proposition.subject,
for _ in range(max(0, steps - len(walk))): response.proposition.predicate,
word, idx = vocab.nearest(F, exclude_idx=idx) ]
walk.append(word) if response.proposition.object_ is not None:
F = vocab.get_versor(word) proposition_terms.append(response.proposition.object_)
return walk walk = [seed, *injected_terms, *proposition_terms, *response.surface.split()]
return walk[: max(1, steps)]
def main() -> None: def main() -> None:
@ -37,7 +27,7 @@ def main() -> None:
break break
try: try:
chain = field_walk(text) chain = field_walk(text)
print(f"[field walk: {' '.join(chain)}]") print(f"[field walk: {' -> '.join(chain)}]")
except KeyError: except KeyError:
print("[unknown token]") print("[unknown token]")

View file

@ -0,0 +1,23 @@
from __future__ import annotations
import numpy as np
from chat.runtime import ChatResponse, ChatRuntime
def test_chat_runtime_keeps_live_session_across_two_turns() -> None:
runtime = ChatRuntime()
first = runtime.chat("light logos", max_tokens=8)
first_field = runtime.session.state.F.copy()
second = runtime.chat("light truth", max_tokens=8)
second_field = runtime.session.state.F.copy()
assert isinstance(first, ChatResponse)
assert first.surface.strip()
assert second.surface.strip()
assert first.versor_condition < 1e-6
assert second.versor_condition < 1e-6
assert second.dialogue_role in {"elaborate", "assert"}
assert not np.array_equal(second_field, first_field)