Add live chat runtime
This commit is contained in:
parent
9ba6abfa3e
commit
0780ca8166
5 changed files with 198 additions and 37 deletions
|
|
@ -1,3 +1,3 @@
|
|||
from .runtime import ChatRuntime
|
||||
from .runtime import ChatResponse, ChatRuntime
|
||||
|
||||
__all__ = ["ChatRuntime"]
|
||||
__all__ = ["ChatResponse", "ChatRuntime"]
|
||||
|
|
|
|||
39
chat/__main__.py
Normal file
39
chat/__main__.py
Normal 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()
|
||||
133
chat/runtime.py
133
chat/runtime.py
|
|
@ -1,25 +1,82 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
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 session.context import SessionContext
|
||||
|
||||
_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:
|
||||
def __init__(self, pack_id: str = "en_minimal_v1") -> None:
|
||||
manifest, manifold = load_pack(pack_id)
|
||||
entries = load_pack_entries(pack_id)
|
||||
self._manifest = manifest
|
||||
def __init__(
|
||||
self,
|
||||
pack_id: str | Sequence[str] = _DEFAULT_PACKS,
|
||||
*,
|
||||
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._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.update(_SEED_ALIASES)
|
||||
self._pos_by_surface = {
|
||||
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]:
|
||||
tokens: list[str] = []
|
||||
for match in _TOKEN_RE.finditer(text):
|
||||
|
|
@ -27,6 +84,9 @@ class ChatRuntime:
|
|||
tokens.append(self._surface_by_fold.get(raw.casefold(), raw))
|
||||
return tokens
|
||||
|
||||
def tokenize(self, text: str) -> list[str]:
|
||||
return self._tokenize(text)
|
||||
|
||||
def _apply_oov_policy(self, tokens: list[str]) -> list[str]:
|
||||
kept: list[str] = []
|
||||
for token in tokens:
|
||||
|
|
@ -34,9 +94,12 @@ class ChatRuntime:
|
|||
self._context.vocab.get_versor(token)
|
||||
kept.append(token)
|
||||
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
|
||||
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}")
|
||||
return kept
|
||||
|
||||
|
|
@ -51,12 +114,58 @@ class ChatRuntime:
|
|||
prev_pos = pos
|
||||
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)
|
||||
filtered = self._apply_oov_policy(tokens)
|
||||
if not filtered:
|
||||
return ""
|
||||
self._context.ingest(filtered)
|
||||
result = self._context.respond(max_tokens=max_tokens)
|
||||
raise ValueError("ChatRuntime.chat() received no in-vocabulary 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)
|
||||
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 ""
|
||||
|
|
|
|||
|
|
@ -1,30 +1,20 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from language_packs import load_mounted_packs
|
||||
|
||||
_TRILINGUAL_PACKS = ("en_minimal_v1", "he_logos_micro_v1", "grc_logos_micro_v1")
|
||||
_SEED_ALIASES = {
|
||||
"logos": "λόγος",
|
||||
"dabar": "דבר",
|
||||
"or": "אור",
|
||||
"phos": "φῶς",
|
||||
"zoe": "ζωή",
|
||||
"arche": "ἀρχή",
|
||||
"aletheia": "ἀλήθεια",
|
||||
}
|
||||
from chat.runtime import ChatRuntime
|
||||
|
||||
|
||||
def field_walk(seed: str, steps: int = 4) -> list[str]:
|
||||
vocab = load_mounted_packs(_TRILINGUAL_PACKS)
|
||||
surface = _SEED_ALIASES.get(seed.casefold(), seed)
|
||||
F = vocab.get_versor(surface)
|
||||
walk = [seed] if surface == seed else [seed, surface]
|
||||
idx = vocab.index_of(surface)
|
||||
for _ in range(max(0, steps - len(walk))):
|
||||
word, idx = vocab.nearest(F, exclude_idx=idx)
|
||||
walk.append(word)
|
||||
F = vocab.get_versor(word)
|
||||
return walk
|
||||
runtime = ChatRuntime()
|
||||
injected_terms = runtime.tokenize(seed)
|
||||
response = runtime.chat(seed, max_tokens=max(1, steps))
|
||||
proposition_terms = [
|
||||
response.proposition.subject,
|
||||
response.proposition.predicate,
|
||||
]
|
||||
if response.proposition.object_ is not None:
|
||||
proposition_terms.append(response.proposition.object_)
|
||||
walk = [seed, *injected_terms, *proposition_terms, *response.surface.split()]
|
||||
return walk[: max(1, steps)]
|
||||
|
||||
|
||||
def main() -> None:
|
||||
|
|
@ -37,7 +27,7 @@ def main() -> None:
|
|||
break
|
||||
try:
|
||||
chain = field_walk(text)
|
||||
print(f"[field walk: {' → '.join(chain)}]")
|
||||
print(f"[field walk: {' -> '.join(chain)}]")
|
||||
except KeyError:
|
||||
print("[unknown token]")
|
||||
|
||||
|
|
|
|||
23
tests/test_chat_runtime.py
Normal file
23
tests/test_chat_runtime.py
Normal 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)
|
||||
Loading…
Reference in a new issue