Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/M1Design.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,9 @@ Accepted patterns:
- "I am using X"
- "I'm using X"

Polite prefixes such as `"please"` may be tolerated and ignored by the parser.
For example, `please use X` is treated the same as `use X`.

Produces:

FACT_SET(key="focus.primary", value=X)
Expand Down
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,6 @@ mypy_path = "src"
[tool.pytest.ini_options]
addopts = "-ra"
testpaths = ["tests"]

[tool.coverage.run]
source = ["context_compiler"]
63 changes: 61 additions & 2 deletions src/context_compiler/repl.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,76 @@
from . import create_engine
from .engine import Decision

_EXIT_TOKENS = {"exit", "quit"}
_HELP_TOKENS = {"help", "?"}


def format_decision(decision: Decision) -> str:
return json.dumps(decision, sort_keys=True, separators=(",", ":"))


def _is_interactive(in_stream: TextIO, out_stream: TextIO) -> bool:
return bool(in_stream.isatty() and out_stream.isatty())


def _print_interactive_help(out_stream: TextIO) -> None:
print("Commands: help/? exit/quit", file=out_stream)
print("Examples:", file=out_stream)
print(" use chamber ensemble", file=out_stream)
print(" don't use passive voice", file=out_stream)
print(" allow contractions", file=out_stream)
print(" actually piano reduction", file=out_stream)
print(" reset policies", file=out_stream)
print(" clear state", file=out_stream)


def _print_interactive_decision(decision: Decision, out_stream: TextIO) -> None:
kind = decision["kind"]
if kind == "passthrough":
print("passthrough", file=out_stream)
return
if kind == "clarify":
prompt = decision["prompt_to_user"] or ""
print(f"clarify: {prompt}", file=out_stream)
return

print("updated", file=out_stream)
state = decision["state"]
assert state is not None
focus_primary = state["facts"]["focus.primary"]
prohibit = state["policies"]["prohibit"]
print("state:", file=out_stream)
print(f" focus.primary: {json.dumps(focus_primary)}", file=out_stream)
print(f" prohibit: {json.dumps(prohibit)}", file=out_stream)


def run_repl(in_stream: TextIO, out_stream: TextIO) -> None:
engine = create_engine()

if _is_interactive(in_stream, out_stream):
print("Context Compiler REPL. Type help for commands.", file=out_stream)

while True:
line = in_stream.readline()
if line == "":
return
user_input = line.rstrip("\n")
token = user_input.strip().lower()
if not token:
continue
if token in _EXIT_TOKENS:
return
if token in _HELP_TOKENS:
_print_interactive_help(out_stream)
continue

decision = engine.step(user_input)
_print_interactive_decision(decision, out_stream)
return

for line in in_stream:
user_input = line.rstrip("\n")
if user_input.strip().lower() in {"exit", "quit"}:
if user_input.strip().lower() in _EXIT_TOKENS:
return
decision = engine.step(user_input)
print(format_decision(decision), file=out_stream)
Expand All @@ -26,5 +85,5 @@ def main() -> int:
return 0


if __name__ == "__main__":
if __name__ == "__main__": # pragma: no cover
raise SystemExit(main())
90 changes: 90 additions & 0 deletions tests/test_repl.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
from context_compiler.repl import run_repl


class _TTYStringIO(StringIO):
def isatty(self) -> bool:
return True


def _run_session(text: str) -> list[dict[str, object]]:
out = StringIO()
run_repl(StringIO(text), out)
Expand Down Expand Up @@ -55,3 +60,88 @@ def test_repl_exit_and_quit_terminate_session() -> None:

assert decisions_exit == []
assert decisions_quit == []


def test_repl_interactive_help_commands() -> None:
out = _TTYStringIO()
run_repl(_TTYStringIO("help\n?\nquit\n"), out)

lines = out.getvalue().splitlines()
expected_help = [
"Commands: help/? exit/quit",
"Examples:",
" use chamber ensemble",
" don't use passive voice",
" allow contractions",
" actually piano reduction",
" reset policies",
" clear state",
]
assert lines[0] == "Context Compiler REPL. Type help for commands."
assert lines[1:9] == expected_help
assert lines[9:17] == expected_help


def test_repl_interactive_ignores_blank_lines() -> None:
out = _TTYStringIO()
run_repl(_TTYStringIO("\n \n\t\nquit\n"), out)

assert out.getvalue().splitlines() == ["Context Compiler REPL. Type help for commands."]


def test_repl_interactive_formats_decisions_and_state_summary() -> None:
out = _TTYStringIO()
run_repl(_TTYStringIO("hello\nuse Nord Stage 4\nno use docker\nquit\n"), out)

lines = out.getvalue().splitlines()
assert lines[0] == "Context Compiler REPL. Type help for commands."
assert lines[1] == "passthrough"
assert lines[2] == "updated"
assert lines[3] == "state:"
assert lines[4] == ' focus.primary: "Nord Stage 4"'
assert lines[5] == " prohibit: []"
assert lines[6].startswith("clarify: ")


def test_repl_interactive_eof_exits_cleanly() -> None:
out = _TTYStringIO()
run_repl(_TTYStringIO("use Nord Stage 4\n"), out)

assert out.getvalue().splitlines() == [
"Context Compiler REPL. Type help for commands.",
"updated",
"state:",
' focus.primary: "Nord Stage 4"',
" prohibit: []",
]


def test_repl_interactive_reset_policies_output() -> None:
out = _TTYStringIO()
run_repl(_TTYStringIO("don't use passive voice\nreset policies\nquit\n"), out)

lines = out.getvalue().splitlines()
assert lines == [
"Context Compiler REPL. Type help for commands.",
"updated",
"state:",
" focus.primary: null",
' prohibit: ["passive voice"]',
"updated",
"state:",
" focus.primary: null",
" prohibit: []",
]


def test_repl_non_interactive_keeps_json_output() -> None:
out = StringIO()
run_repl(StringIO("use Nord Stage 4\nquit\n"), out)

lines = out.getvalue().splitlines()
expected = (
'{"kind":"update","prompt_to_user":null,'
'"state":{"facts":{"focus.primary":"Nord Stage 4"},'
'"policies":{"prohibit":[]},"version":1}}'
)
assert lines == [expected]
Loading