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 __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 ""
|
||||||
|
|
|
||||||
|
|
@ -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]")
|
||||||
|
|
||||||
|
|
|
||||||
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