Add runtime output-language policy

Add RuntimeConfig with English default output policy, wire output language through runtime/frame selection/generation/CLI, preserve language metadata in mounted manifolds, and add runtime/CLI policy tests.
This commit is contained in:
Shay 2026-05-13 21:29:43 -07:00 committed by GitHub
parent 09c3664773
commit 30757ccc63
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 287 additions and 46 deletions

View file

@ -7,6 +7,7 @@ from collections.abc import Sequence
import numpy as np import numpy as np
from algebra.versor import versor_condition from algebra.versor import versor_condition
from core.config import DEFAULT_CONFIG, RuntimeConfig
from generate.dialogue import DialogueRole, classify_dialogue_blade, propose_dialogue from generate.dialogue import DialogueRole, classify_dialogue_blade, propose_dialogue
from generate.proposition import FrameRegistry, Proposition, propose from generate.proposition import FrameRegistry, Proposition, propose
from generate.stream import generate from generate.stream import generate
@ -15,7 +16,6 @@ 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 = { _SEED_ALIASES = {
"logos": "λόγος", "logos": "λόγος",
"dabar": "דבר", "dabar": "דבר",
@ -33,16 +33,34 @@ class ChatResponse:
proposition: Proposition proposition: Proposition
dialogue_role: DialogueRole dialogue_role: DialogueRole
versor_condition: float versor_condition: float
output_language: str
frame_pack: str
walk_surface: str
class ChatRuntime: class ChatRuntime:
def __init__( def __init__(
self, self,
pack_id: str | Sequence[str] = _DEFAULT_PACKS, pack_id: str | Sequence[str] | None = None,
*, *,
frame_pack: str | None = None, frame_pack: str | None = None,
config: RuntimeConfig = DEFAULT_CONFIG,
) -> None: ) -> None:
pack_ids = (pack_id,) if isinstance(pack_id, str) else tuple(pack_id) if pack_id is not None or frame_pack is not None:
pack_ids = (pack_id,) if isinstance(pack_id, str) else tuple(pack_id or config.input_packs)
resolved_config = RuntimeConfig(
input_packs=pack_ids,
output_language=config.output_language,
frame_pack=frame_pack or config.frame_pack,
max_tokens=config.max_tokens,
allow_cross_language_recall=config.allow_cross_language_recall,
allow_cross_language_generation=config.allow_cross_language_generation,
)
else:
resolved_config = config
pack_ids = tuple(config.input_packs)
self.config = resolved_config
manifests = [] manifests = []
manifolds = [] manifolds = []
entries = [] entries = []
@ -56,7 +74,7 @@ class ChatRuntime:
self._manifests = tuple(manifests) self._manifests = tuple(manifests)
self._context = SessionContext(manifold, persona=PersonaMotor.identity()) self._context = SessionContext(manifold, persona=PersonaMotor.identity())
self._frame_registry = FrameRegistry.from_pack( self._frame_registry = FrameRegistry.from_pack(
frame_pack or self._default_frame_pack(pack_ids), resolved_config.frame_pack,
self._context.vocab, 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}
@ -69,14 +87,6 @@ class ChatRuntime:
def session(self) -> SessionContext: def session(self) -> SessionContext:
return self._context 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):
@ -121,7 +131,7 @@ class ChatRuntime:
return None return None
return blade return blade
def chat(self, text: str, max_tokens: int = 32) -> ChatResponse: def chat(self, text: str, max_tokens: int | None = None) -> 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:
@ -129,7 +139,13 @@ class ChatRuntime:
field_state = self._context.ingest(filtered) field_state = self._context.ingest(filtered)
reference_blade = self._dialogue_reference() reference_blade = self._dialogue_reference()
base_proposition = propose(field_state, None, self._context.vocab, self._frame_registry) base_proposition = propose(
field_state,
None,
self._context.vocab,
self._frame_registry,
output_lang=self.config.output_language,
)
dialogue_role = classify_dialogue_blade( dialogue_role = classify_dialogue_blade(
base_proposition.relation, base_proposition.relation,
reference_blade, reference_blade,
@ -140,6 +156,7 @@ class ChatRuntime:
self._context.vocab, self._context.vocab,
self._frame_registry, self._frame_registry,
reference_blade, reference_blade,
output_lang=self.config.output_language,
) )
self._context.record_dialogue(proposition) self._context.record_dialogue(proposition)
@ -147,8 +164,11 @@ class ChatRuntime:
field_state, field_state,
self._context.vocab, self._context.vocab,
self._context.persona, self._context.persona,
max_tokens=max_tokens, max_tokens=self.config.max_tokens if max_tokens is None else max_tokens,
vault=self._context.vault, vault=self._context.vault,
recall_top_k=3 if self.config.allow_cross_language_recall else 0,
output_lang=self.config.output_language,
allow_cross_language_generation=self.config.allow_cross_language_generation,
) )
self._context.state = result.final_state self._context.state = result.final_state
self._context.vault.store( self._context.vault.store(
@ -157,15 +177,18 @@ class ChatRuntime:
) )
self._context.turn += 1 self._context.turn += 1
guarded = self._syntactic_guard(result.tokens) guarded = self._syntactic_guard(result.tokens)
surface = " ".join(guarded) walk_surface = " ".join(guarded)
return ChatResponse( return ChatResponse(
surface=surface, surface=proposition.surface,
proposition=proposition, proposition=proposition,
dialogue_role=dialogue_role, dialogue_role=dialogue_role,
versor_condition=versor_condition(result.final_state.F), versor_condition=versor_condition(result.final_state.F),
output_language=self.config.output_language,
frame_pack=self.config.frame_pack,
walk_surface=walk_surface,
) )
def respond(self, text: str, max_tokens: int = 32) -> str: def respond(self, text: str, max_tokens: int | None = None) -> str:
try: try:
return self.chat(text, max_tokens=max_tokens).surface return self.chat(text, max_tokens=max_tokens).surface
except ValueError: except ValueError:

View file

@ -20,7 +20,7 @@ if str(_REPO_ROOT) not in sys.path:
DESCRIPTION = "CORE versor engine command suite." DESCRIPTION = "CORE versor engine command suite."
EPILOG = "Examples:\n core chat\n core trace \"word beginning truth\"\n core trace --pack en_minimal_v1 --json \"word beginning truth\"\n core oov covenant\n core pack list\n core pack verify en_minimal_v1\n core test tests/test_alignment_graph.py -q" EPILOG = "Examples:\n core chat\n core trace \"word beginning truth\"\n core trace --output-language grc --frame-pack grc --json \"logos\"\n core oov covenant\n core pack list\n core pack verify en_minimal_v1\n core test tests/test_alignment_graph.py -q"
def _run(*args: str, check: bool = False) -> int: def _run(*args: str, check: bool = False) -> int:
@ -42,9 +42,47 @@ def _print_runtime_import_hint(exc: BaseException) -> NoReturn:
) )
def _runtime_config_from_args(args: argparse.Namespace):
from core.config import DEFAULT_CONFIG, RuntimeConfig
output_language = args.output_language
frame_pack = args.frame_pack or output_language
input_packs = tuple(args.pack) if getattr(args, "pack", None) else DEFAULT_CONFIG.input_packs
return RuntimeConfig(
input_packs=input_packs,
output_language=output_language,
frame_pack=frame_pack,
max_tokens=args.max_tokens,
allow_cross_language_recall=not args.no_cross_language_recall,
allow_cross_language_generation=args.allow_cross_language_generation,
)
def cmd_chat(args: argparse.Namespace) -> int: def cmd_chat(args: argparse.Namespace) -> int:
"""Launch the readline REPL backed by ChatRuntime.""" """Launch a readline REPL backed by ChatRuntime."""
return _run(sys.executable, "-m", "chat", *args.args) try:
from chat.runtime import ChatRuntime
except Exception as exc: # pragma: no cover - exercised by CLI in broken envs
_print_runtime_import_hint(exc)
runtime = ChatRuntime(config=_runtime_config_from_args(args))
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"[{exc}]", file=sys.stderr)
continue
print(response.surface)
return 0
def cmd_test(args: argparse.Namespace) -> int: def cmd_test(args: argparse.Namespace) -> int:
@ -75,15 +113,13 @@ def cmd_check(args: argparse.Namespace) -> int:
return _run(sys.executable, "-m", "ruff", "check", *targets) return _run(sys.executable, "-m", "ruff", "check", *targets)
def _runtime_for_trace(pack: list[str] | None): def _runtime_for_trace(args: argparse.Namespace):
try: try:
from chat.runtime import ChatRuntime from chat.runtime import ChatRuntime
except Exception as exc: # pragma: no cover - exercised by CLI in broken envs except Exception as exc: # pragma: no cover - exercised by CLI in broken envs
_print_runtime_import_hint(exc) _print_runtime_import_hint(exc)
pack_arg: str | tuple[str, ...]
pack_arg = tuple(pack) if pack else ("en_minimal_v1", "he_logos_micro_v1", "grc_logos_micro_v1")
try: try:
return ChatRuntime(pack_arg) return ChatRuntime(config=_runtime_config_from_args(args))
except Exception as exc: except Exception as exc:
_die( _die(
"failed to initialize ChatRuntime. Check mounted language packs with " "failed to initialize ChatRuntime. Check mounted language packs with "
@ -100,6 +136,9 @@ def _trace_payload(text: str, resp: Any, runtime: Any) -> dict[str, Any]:
payload: dict[str, Any] = { payload: dict[str, Any] = {
"input": text, "input": text,
"surface": resp.surface, "surface": resp.surface,
"walk_surface": resp.walk_surface,
"output_language": resp.output_language,
"frame_pack": resp.frame_pack,
"dialogue_role": str(resp.dialogue_role), "dialogue_role": str(resp.dialogue_role),
"versor_condition": float(resp.versor_condition), "versor_condition": float(resp.versor_condition),
"proposition": { "proposition": {
@ -119,6 +158,9 @@ def _trace_payload(text: str, resp: Any, runtime: Any) -> dict[str, Any]:
def _print_trace(payload: dict[str, Any]) -> None: def _print_trace(payload: dict[str, Any]) -> None:
print(f"input : {payload['input']}") print(f"input : {payload['input']}")
print(f"surface : {payload['surface']}") print(f"surface : {payload['surface']}")
print(f"walk_surface : {payload['walk_surface']}")
print(f"output_language: {payload['output_language']}")
print(f"frame_pack : {payload['frame_pack']}")
print(f"dialogue_role : {payload['dialogue_role']}") print(f"dialogue_role : {payload['dialogue_role']}")
print(f"versor_cond : {payload['versor_condition']:.2e}") print(f"versor_cond : {payload['versor_condition']:.2e}")
proposition = payload["proposition"] proposition = payload["proposition"]
@ -143,7 +185,7 @@ def cmd_trace(args: argparse.Namespace) -> int:
if not text: if not text:
_die("trace requires input text. Try: core trace \"word beginning truth\"") _die("trace requires input text. Try: core trace \"word beginning truth\"")
runtime = _runtime_for_trace(args.pack) runtime = _runtime_for_trace(args)
try: try:
response = runtime.chat(text, max_tokens=args.max_tokens) response = runtime.chat(text, max_tokens=args.max_tokens)
except Exception as exc: except Exception as exc:
@ -165,7 +207,7 @@ def cmd_oov(args: argparse.Namespace) -> int:
except Exception as exc: # pragma: no cover - exercised by CLI in broken envs except Exception as exc: # pragma: no cover - exercised by CLI in broken envs
_print_runtime_import_hint(exc) _print_runtime_import_hint(exc)
runtime = ChatRuntime(tuple(args.pack) if args.pack else ("en_minimal_v1", "he_logos_micro_v1", "grc_logos_micro_v1")) runtime = ChatRuntime(config=_runtime_config_from_args(args))
vocab = runtime.session.vocab vocab = runtime.session.vocab
try: try:
@ -244,6 +286,23 @@ def cmd_doctor(args: argparse.Namespace) -> int:
return 0 if ok else 1 return 0 if ok else 1
def _add_runtime_policy_args(parser: argparse.ArgumentParser) -> None:
parser.add_argument("--pack", action="append", help="language pack to mount; repeat for multiple packs")
parser.add_argument("--output-language", default="en", help="target output language code; default: en")
parser.add_argument("--frame-pack", help="frame pack to use; defaults to output language")
parser.add_argument("--max-tokens", type=int, default=32, help="maximum generated tokens; default: 32")
parser.add_argument(
"--allow-cross-language-generation",
action="store_true",
help="allow generated walk tokens from any mounted language",
)
parser.add_argument(
"--no-cross-language-recall",
action="store_true",
help="disable vault recall during generation",
)
def build_parser() -> argparse.ArgumentParser: def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
prog="core", prog="core",
@ -255,7 +314,7 @@ def build_parser() -> argparse.ArgumentParser:
subparsers = parser.add_subparsers(dest="command", metavar="command") subparsers = parser.add_subparsers(dest="command", metavar="command")
chat = subparsers.add_parser("chat", help="start the interactive chat REPL") chat = subparsers.add_parser("chat", help="start the interactive chat REPL")
chat.add_argument("args", nargs=argparse.REMAINDER, help="arguments forwarded to python -m chat") _add_runtime_policy_args(chat)
chat.set_defaults(func=cmd_chat) chat.set_defaults(func=cmd_chat)
test = subparsers.add_parser("test", help="run pytest with sane defaults") test = subparsers.add_parser("test", help="run pytest with sane defaults")
@ -271,14 +330,13 @@ def build_parser() -> argparse.ArgumentParser:
help="trace one chat turn with field telemetry", help="trace one chat turn with field telemetry",
description="trace one chat turn with field telemetry", description="trace one chat turn with field telemetry",
) )
trace.add_argument("--pack", action="append", help="language pack to mount; repeat for multiple packs") _add_runtime_policy_args(trace)
trace.add_argument("--max-tokens", type=int, default=32, help="maximum generated tokens; default: 32")
trace.add_argument("--json", action="store_true", help="emit machine-readable JSON") trace.add_argument("--json", action="store_true", help="emit machine-readable JSON")
trace.add_argument("text", nargs=argparse.REMAINDER, help="input text to trace") trace.add_argument("text", nargs=argparse.REMAINDER, help="input text to trace")
trace.set_defaults(func=cmd_trace) trace.set_defaults(func=cmd_trace)
oov = subparsers.add_parser("oov", help="ground or inspect one token") oov = subparsers.add_parser("oov", help="ground or inspect one token")
oov.add_argument("--pack", action="append", help="language pack to mount; repeat for multiple packs") _add_runtime_policy_args(oov)
oov.add_argument("token", help="token to inspect or ground") oov.add_argument("token", help="token to inspect or ground")
oov.set_defaults(func=cmd_oov) oov.set_defaults(func=cmd_oov)

16
core/config.py Normal file
View file

@ -0,0 +1,16 @@
from __future__ import annotations
from dataclasses import dataclass
@dataclass(frozen=True, slots=True)
class RuntimeConfig:
input_packs: tuple[str, ...] = ("en_minimal_v1", "he_logos_micro_v1", "grc_logos_micro_v1")
output_language: str = "en"
frame_pack: str = "en"
max_tokens: int = 32
allow_cross_language_recall: bool = True
allow_cross_language_generation: bool = False
DEFAULT_CONFIG = RuntimeConfig()

View file

@ -94,13 +94,14 @@ def propose_dialogue(
vocab, vocab,
frame_registry: FrameRegistry, frame_registry: FrameRegistry,
reference_blade: np.ndarray | None = None, reference_blade: np.ndarray | None = None,
output_lang: str | None = None,
) -> Proposition: ) -> Proposition:
""" """
Generate a proposition through a dialogue-role constrained frame choice. Generate a proposition through a dialogue-role constrained frame choice.
""" """
base = propose(field_state, None, vocab, frame_registry) base = propose(field_state, None, vocab, frame_registry, output_lang=output_lang)
role = classify_dialogue_blade(base.relation, reference_blade) role = classify_dialogue_blade(base.relation, reference_blade)
frame = frame_registry.select_dialogue(base.relation, role) frame = frame_registry.select_dialogue(base.relation, role)
role_registry = FrameRegistry((frame,)) role_registry = FrameRegistry((frame,))
proposition = propose(field_state, vault, vocab, role_registry) proposition = propose(field_state, vault, vocab, role_registry, output_lang=output_lang)
return proposition return proposition

View file

@ -128,7 +128,25 @@ class FrameRegistry:
return len(self._frames) return len(self._frames)
def propose(field_state: FieldState, vault, vocab, frame_registry: FrameRegistry) -> Proposition: def _candidate_indices_for_language(vocab, output_lang: str | None) -> np.ndarray | None:
if output_lang is None:
return None
indices_for_language = getattr(vocab, "indices_for_language", None)
if indices_for_language is None:
return None
indices = indices_for_language(output_lang)
if len(indices) == 0:
raise ValueError(f"No proposition candidates for output language {output_lang!r}.")
return indices
def propose(
field_state: FieldState,
vault,
vocab,
frame_registry: FrameRegistry,
output_lang: str | None = None,
) -> Proposition:
""" """
Generate one structured proposition from the live field. Generate one structured proposition from the live field.
@ -141,17 +159,20 @@ def propose(field_state: FieldState, vault, vocab, frame_registry: FrameRegistry
prompt = _prompt_versor(field_state) prompt = _prompt_versor(field_state)
relation = outer_product(prompt, field_state.F) relation = outer_product(prompt, field_state.F)
frame = frame_registry.select(relation) frame = frame_registry.select(relation)
candidate_indices = _candidate_indices_for_language(vocab, output_lang)
subject_word, subject_idx = _nearest_content_word( subject_word, subject_idx = _nearest_content_word(
vocab, vocab,
prompt, prompt,
exclude_indices=frozenset(), exclude_indices=frozenset(),
preferred_pos=frozenset({"noun", "pronoun"}), preferred_pos=frozenset({"noun", "pronoun"}),
candidate_indices=candidate_indices,
) )
predicate_word, predicate_idx = _nearest_content_word( predicate_word, predicate_idx = _nearest_content_word(
vocab, vocab,
field_state.F, field_state.F,
exclude_indices=frozenset({subject_idx}), exclude_indices=frozenset({subject_idx}),
candidate_indices=candidate_indices,
) )
object_word: str | None = None object_word: str | None = None
@ -162,6 +183,7 @@ def propose(field_state: FieldState, vault, vocab, frame_registry: FrameRegistry
relation, relation,
exclude_indices=frozenset({subject_idx, predicate_idx}), exclude_indices=frozenset({subject_idx, predicate_idx}),
preferred_pos=frozenset({"noun", "pronoun"}), preferred_pos=frozenset({"noun", "pronoun"}),
candidate_indices=candidate_indices,
) )
object_versor = vocab.get_versor_at(object_idx) object_versor = vocab.get_versor_at(object_idx)
@ -273,6 +295,7 @@ def _nearest_content_word(
query: np.ndarray, query: np.ndarray,
exclude_indices: frozenset[int], exclude_indices: frozenset[int],
preferred_pos: frozenset[str] = frozenset(), preferred_pos: frozenset[str] = frozenset(),
candidate_indices: np.ndarray | None = None,
) -> tuple[str, int]: ) -> tuple[str, int]:
stop_indices = { stop_indices = {
vocab.index_of(surface) vocab.index_of(surface)
@ -281,13 +304,13 @@ def _nearest_content_word(
} }
blocked = set(exclude_indices) | stop_indices blocked = set(exclude_indices) | stop_indices
if preferred_pos: if preferred_pos:
selected = _nearest_by_pos(vocab, query, blocked, preferred_pos) selected = _nearest_by_pos(vocab, query, blocked, preferred_pos, candidate_indices)
if selected is not None: if selected is not None:
return selected return selected
try: try:
return vocab.nearest(query, exclude_indices=blocked) return vocab.nearest(query, exclude_indices=blocked, candidate_indices=candidate_indices)
except ValueError: except ValueError:
return vocab.nearest(query, exclude_indices=set(exclude_indices)) return vocab.nearest(query, exclude_indices=set(exclude_indices), candidate_indices=candidate_indices)
def _nearest_by_pos( def _nearest_by_pos(
@ -295,10 +318,12 @@ def _nearest_by_pos(
query: np.ndarray, query: np.ndarray,
blocked: set[int], blocked: set[int],
preferred_pos: frozenset[str], preferred_pos: frozenset[str],
candidate_indices: np.ndarray | None = None,
) -> tuple[str, int] | None: ) -> tuple[str, int] | None:
best_score = -np.inf best_score = -np.inf
best: tuple[str, int] | None = None best: tuple[str, int] | None = None
for idx in range(len(vocab)): candidates = range(len(vocab)) if candidate_indices is None else [int(idx) for idx in candidate_indices]
for idx in candidates:
if idx in blocked: if idx in blocked:
continue continue
word = vocab.get_word_at(idx) word = vocab.get_word_at(idx)

View file

@ -18,6 +18,8 @@ F is always on the manifold. nearest() is exact.
from __future__ import annotations from __future__ import annotations
from collections import deque from collections import deque
import numpy as np
from field.state import FieldState from field.state import FieldState
from field.propagate import propagate_step from field.propagate import propagate_step
from algebra.rotor import word_transition_rotor from algebra.rotor import word_transition_rotor
@ -48,6 +50,7 @@ def _nearest_next(
current_node: int, current_node: int,
recent_nodes: tuple[int, ...] = (), recent_nodes: tuple[int, ...] = (),
stop_nodes: frozenset[int] = frozenset(), stop_nodes: frozenset[int] = frozenset(),
candidate_indices: np.ndarray | None = None,
) -> tuple[str, int]: ) -> tuple[str, int]:
""" """
Select the nearest vocabulary point while avoiding short loops. Select the nearest vocabulary point while avoiding short loops.
@ -59,7 +62,7 @@ def _nearest_next(
informative neighbors are available. informative neighbors are available.
""" """
if len(vocab) <= 1: if len(vocab) <= 1:
return vocab.nearest(F_voiced) return vocab.nearest(F_voiced, candidate_indices=candidate_indices)
recent = set(recent_nodes) recent = set(recent_nodes)
stop = set(stop_nodes) stop = set(stop_nodes)
@ -71,10 +74,15 @@ def _nearest_next(
) )
for extra in fallback_orders: for extra in fallback_orders:
try: try:
return vocab.nearest(F_voiced, exclude_idx=current_node, exclude_indices=extra) return vocab.nearest(
F_voiced,
exclude_idx=current_node,
exclude_indices=extra,
candidate_indices=candidate_indices,
)
except ValueError: except ValueError:
continue continue
return vocab.nearest(F_voiced, exclude_idx=current_node) return vocab.nearest(F_voiced, exclude_idx=current_node, candidate_indices=candidate_indices)
def _voiced_state(state: FieldState, persona) -> FieldState: def _voiced_state(state: FieldState, persona) -> FieldState:
@ -111,6 +119,18 @@ def _recall_state(state: FieldState, vault, top_k: int) -> FieldState:
return current return current
def _candidate_indices_for_language(vocab, output_lang: str | None) -> np.ndarray | None:
if output_lang is None:
return None
indices_for_language = getattr(vocab, "indices_for_language", None)
if indices_for_language is None:
return None
indices = indices_for_language(output_lang)
if len(indices) == 0:
raise ValueError(f"No generation candidates for output language {output_lang!r}.")
return indices
def generate( def generate(
state: FieldState, state: FieldState,
vocab, vocab,
@ -119,6 +139,8 @@ def generate(
record_trajectory: bool = False, record_trajectory: bool = False,
vault=None, vault=None,
recall_top_k: int = 3, recall_top_k: int = 3,
output_lang: str | None = None,
allow_cross_language_generation: bool = True,
) -> GenerationResult: ) -> GenerationResult:
""" """
Generate a token sequence from an initial FieldState. Generate a token sequence from an initial FieldState.
@ -140,6 +162,7 @@ def generate(
trajectory = [] if record_trajectory else None trajectory = [] if record_trajectory else None
current = state current = state
recent_nodes = deque([state.node], maxlen=_RECENT_WINDOW) recent_nodes = deque([state.node], maxlen=_RECENT_WINDOW)
candidate_indices = None if allow_cross_language_generation else _candidate_indices_for_language(vocab, output_lang)
stop_nodes = frozenset( stop_nodes = frozenset(
vocab.index_of(token) vocab.index_of(token)
for token in _STOP_TOKENS for token in _STOP_TOKENS
@ -154,6 +177,7 @@ def generate(
current.node, current.node,
recent_nodes=tuple(recent_nodes), recent_nodes=tuple(recent_nodes),
stop_nodes=stop_nodes, stop_nodes=stop_nodes,
candidate_indices=candidate_indices,
) )
tokens.append(_articulate(vocab, word)) tokens.append(_articulate(vocab, word))

View file

@ -268,7 +268,7 @@ def compile_entries_to_manifold(entries: list[LexicalEntry], morphology_registry
for entry in entries: for entry in entries:
morphology = _resolved_morphology(entry, morphology_registry) morphology = _resolved_morphology(entry, morphology_registry)
versor = _entry_to_coordinate(entry, morphology) versor = _entry_to_coordinate(entry, morphology)
manifold.add(entry.surface, versor, morphology=morphology) manifold.add(entry.surface, versor, morphology=morphology, language=entry.language)
entry_id_to_surface[entry.entry_id] = entry.surface entry_id_to_surface[entry.entry_id] = entry.surface
if morphology_registry is not None: if morphology_registry is not None:
@ -394,12 +394,13 @@ def load_mounted_packs(pack_ids: tuple[str, ...] | list[str]) -> VocabManifold:
surface = manifold.get_word_at(idx) surface = manifold.get_word_at(idx)
if surface in seen: if surface in seen:
continue continue
entry = entry_by_surface.get(surface)
mounted.add( mounted.add(
surface, surface,
manifold.get_versor_at(idx), manifold.get_versor_at(idx),
morphology=manifold.morphology_for_word(surface), morphology=manifold.morphology_for_word(surface),
language=None if entry is None else entry.language,
) )
entry = entry_by_surface.get(surface)
if entry is not None and entry.semantic_domains: if entry is not None and entry.semantic_domains:
primary_groups.setdefault(entry.semantic_domains[0].lower(), []).append( primary_groups.setdefault(entry.semantic_domains[0].lower(), []).append(
(entry.language, surface) (entry.language, surface)

View file

@ -21,6 +21,8 @@ def test_trace_help_exits_without_runtime_import(capsys: pytest.CaptureFixture[s
out = capsys.readouterr().out out = capsys.readouterr().out
assert "trace one chat turn" in out assert "trace one chat turn" in out
assert "--pack" in out assert "--pack" in out
assert "--output-language" in out
assert "--frame-pack" in out
assert "--json" in out assert "--json" in out
@ -51,6 +53,8 @@ def test_trace_formats_real_runtime_payload(capsys: pytest.CaptureFixture[str])
assert main(["trace", "--pack", "en_minimal_v1", "word", "beginning", "truth"]) == 0 assert main(["trace", "--pack", "en_minimal_v1", "word", "beginning", "truth"]) == 0
out = capsys.readouterr().out out = capsys.readouterr().out
assert "input : word beginning truth" in out assert "input : word beginning truth" in out
assert "output_language: en" in out
assert "frame_pack : en" in out
assert "proposition" in out assert "proposition" in out
assert "subject" in out assert "subject" in out
assert "predicate" in out assert "predicate" in out
@ -60,6 +64,8 @@ def test_trace_json_formats_real_runtime_payload(capsys: pytest.CaptureFixture[s
assert main(["trace", "--pack", "en_minimal_v1", "--json", "word", "beginning", "truth"]) == 0 assert main(["trace", "--pack", "en_minimal_v1", "--json", "word", "beginning", "truth"]) == 0
out = capsys.readouterr().out out = capsys.readouterr().out
assert '"input": "word beginning truth"' in out assert '"input": "word beginning truth"' in out
assert '"output_language": "en"' in out
assert '"frame_pack": "en"' in out
assert '"proposition"' in out assert '"proposition"' in out
assert '"subject"' in out assert '"subject"' in out
assert '"predicate"' in out assert '"predicate"' in out

View file

@ -0,0 +1,55 @@
from __future__ import annotations
import re
from chat.runtime import ChatRuntime
from core.config import RuntimeConfig
_GREEK_RE = re.compile(r"[\u0370-\u03ff\u1f00-\u1fff]")
_HEBREW_RE = re.compile(r"[\u0590-\u05ff]")
def _is_english_surface(text: str) -> bool:
return not _GREEK_RE.search(text) and not _HEBREW_RE.search(text)
def test_default_runtime_emits_english_surface() -> None:
runtime = ChatRuntime()
response = runtime.chat("word beginning truth")
assert response.output_language == "en"
assert response.frame_pack == "en"
assert _is_english_surface(response.surface)
def test_trilanguage_mounted_runtime_still_emits_english_surface_by_default() -> None:
runtime = ChatRuntime(
config=RuntimeConfig(
input_packs=("en_minimal_v1", "he_logos_micro_v1", "grc_logos_micro_v1"),
output_language="en",
frame_pack="en",
)
)
response = runtime.chat("word beginning truth")
assert _is_english_surface(response.surface)
assert response.proposition.frame_id.startswith("en:")
def test_greek_output_language_emits_greek_surface() -> None:
runtime = ChatRuntime(
config=RuntimeConfig(
input_packs=("en_minimal_v1", "he_logos_micro_v1", "grc_logos_micro_v1"),
output_language="grc",
frame_pack="grc",
)
)
response = runtime.chat("logos arche aletheia")
assert response.output_language == "grc"
assert response.frame_pack == "grc"
assert _GREEK_RE.search(response.surface)
assert response.proposition.frame_id.startswith(("grc:", "el:"))
def test_chat_response_surface_uses_proposition_when_relation_nonzero() -> None:
runtime = ChatRuntime(config=RuntimeConfig(output_language="en", frame_pack="en"))
response = runtime.chat("word beginning truth")
assert response.surface == response.proposition.surface

View file

@ -38,10 +38,17 @@ class VocabManifold:
self._words: list[str] = [] self._words: list[str] = []
self._versors: list[np.ndarray] = [] # each shape (32,), grade-normed to ±1 self._versors: list[np.ndarray] = [] # each shape (32,), grade-normed to ±1
self._morphology_by_word: dict[str, MorphologyEntry] = {} self._morphology_by_word: dict[str, MorphologyEntry] = {}
self._language_by_word: dict[str, str] = {}
self._transient_words: set[str] = set() self._transient_words: set[str] = set()
self._unknown_token_log: list[dict[str, object]] = [] self._unknown_token_log: list[dict[str, object]] = []
def add(self, word: str, versor: np.ndarray, morphology: MorphologyEntry | None = None) -> None: def add(
self,
word: str,
versor: np.ndarray,
morphology: MorphologyEntry | None = None,
language: str | None = None,
) -> None:
""" """
Register a word-versor pair. Register a word-versor pair.
@ -68,6 +75,9 @@ class VocabManifold:
) )
self._words.append(word) self._words.append(word)
self._versors.append(v) self._versors.append(v)
resolved_language = language or (morphology.language if morphology is not None else None)
if resolved_language:
self._language_by_word[word] = resolved_language
if morphology is not None: if morphology is not None:
self._morphology_by_word[word] = morphology self._morphology_by_word[word] = morphology
@ -175,11 +185,28 @@ class VocabManifold:
"""Return structured morphology for a stored surface, if the pack provided it.""" """Return structured morphology for a stored surface, if the pack provided it."""
return self._morphology_by_word.get(word) return self._morphology_by_word.get(word)
def language_for_word(self, word: str) -> str | None:
"""Return the language code for a stored surface, if known."""
morphology = self._morphology_by_word.get(word)
if morphology is not None:
return morphology.language
return self._language_by_word.get(word)
def indices_for_language(self, lang: str) -> np.ndarray:
"""Return manifold indices whose language matches lang."""
matches = [
idx
for idx, word in enumerate(self._words)
if self.language_for_word(word) == lang
]
return np.asarray(matches, dtype=np.int64)
def nearest( def nearest(
self, self,
F: np.ndarray, F: np.ndarray,
exclude_idx: int = -1, exclude_idx: int = -1,
exclude_indices: set[int] | frozenset[int] | None = None, exclude_indices: set[int] | frozenset[int] | None = None,
candidate_indices: np.ndarray | list[int] | tuple[int, ...] | None = None,
) -> tuple[str, int]: ) -> tuple[str, int]:
""" """
Find the word whose versor is closest to F by CGA inner product. Find the word whose versor is closest to F by CGA inner product.
@ -192,12 +219,17 @@ class VocabManifold:
if exclude_idx >= 0: if exclude_idx >= 0:
blocked.add(exclude_idx) blocked.add(exclude_idx)
if candidate_indices is None:
candidates = range(len(self._versors))
else:
candidates = [int(idx) for idx in candidate_indices]
best_score = -np.inf best_score = -np.inf
best_idx = -1 best_idx = -1
for i, v in enumerate(self._versors): for i in candidates:
if i in blocked: if i in blocked:
continue continue
score = cga_inner(F, v) score = cga_inner(F, self._versors[i])
if score > best_score: if score > best_score:
best_score = score best_score = score
best_idx = i best_idx = i