mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-13 17:10:59 +01:00
The gates read the toolbox through toolbox.load_safe (typed, fail-open) beside main's raw load(); the session log follows LEETCODE_COACH_SESSIONS_DIR, stamps LEETCODE_COACH_CLUSTER, and the card builder logs postmortem on delivery. leetcode report dispatches beside cluster_cli; client notes move to DOCS-clients.md to keep SKILL.md under the cap. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PjvF5DZG9JduSBYKxqyysi
130 lines
5.3 KiB
Python
Executable File
130 lines
5.3 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Coaching REPL for /leetcode that only ever shows the post-gate reply.
|
|
|
|
In the interactive TUI a Stop hook runs after the reply has streamed, so a
|
|
blocked reply (one that hands kuhy a pasteable line) is already on screen
|
|
before the rewrite appears. `claude -p` returns nothing until the hooks are
|
|
done, so this loop is the only client where the no-solution gate actually
|
|
withholds text from the human. Each turn is one `claude -p --resume` call.
|
|
|
|
Every block first passes the req-before-code phase gate (`phase.py`): code
|
|
sent before an accepted ``req:`` list is refused here and never reaches the
|
|
model. Replies lose their ``<!-- rung:Hn -->`` marker (logged) before printing.
|
|
|
|
When Claude answers with its usage-limit message (`claude_cli.is_limit_hit`)
|
|
the session continues on the local model (`local_session.py`) with the
|
|
conversation so far and the same gate; `--local` starts there.
|
|
|
|
Input: a block ends on an empty line (paste code, then press Enter twice).
|
|
`quit` or Ctrl-D leaves. `reveal` is passed through like any other message.
|
|
`leetcode report` prints the per-problem method report (`report.py`).
|
|
Started by the `leetcode` launcher as `python3 -m leetcode_coach.leetcode_repl`;
|
|
its prints ARE the session (T201 exemption in pyproject.toml).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import functools
|
|
import logging
|
|
import os
|
|
import sys
|
|
|
|
from leetcode_coach import cluster_cli, local_session, phase, report
|
|
from leetcode_coach.claude_cli import ask, is_limit_hit
|
|
|
|
# Named, not __name__: under `-m` this module is `__main__`, which would escape
|
|
# the package logger's NullHandler and print the warning to the session.
|
|
_LOG = logging.getLogger("leetcode_coach.leetcode_repl")
|
|
|
|
|
|
def read_block() -> str | None:
|
|
"""Lines until an empty one; None on EOF with nothing typed."""
|
|
print("\nyou> ", end="", flush=True)
|
|
lines: list[str] = []
|
|
while True:
|
|
try:
|
|
line = input()
|
|
except EOFError:
|
|
# Ctrl-D is how kuhy leaves; benign, logged only to satisfy the
|
|
# repo's no-silent-except rule (the package logger is silent).
|
|
_LOG.warning("stdin closed (Ctrl-D); ending the block")
|
|
return "\n".join(lines) if lines else None
|
|
if not line.strip():
|
|
if lines:
|
|
return "\n".join(lines)
|
|
continue
|
|
lines.append(line)
|
|
|
|
|
|
def _screen(gate: phase.PhaseGate, block: str) -> str | None:
|
|
"""The block to send the coach, or None when the phase gate refuses it."""
|
|
sent, note = gate.screen(block)
|
|
if note:
|
|
print(note)
|
|
return sent
|
|
|
|
|
|
def run_claude(model: str, problem: list[str]) -> int:
|
|
"""The Claude session; hands over to the local model on a usage limit."""
|
|
gate = phase.PhaseGate.open(problem[0])
|
|
opener = local_session.opening_message(problem[1:])
|
|
first = ask(["--model", model], "/leetcode " + " ".join(problem))
|
|
if is_limit_hit(first):
|
|
print(f"[{first.get('result', 'usage limit')} -- switching to local model]")
|
|
return local_session.run(problem[0], [], opener, read_block, gate=gate)
|
|
session = first.get("session_id")
|
|
print(gate.after_reply(str(first.get("result", ""))))
|
|
if not session:
|
|
return 1
|
|
history = [
|
|
{"role": "user", "content": opener},
|
|
{"role": "assistant", "content": str(first.get("result", ""))},
|
|
]
|
|
while True:
|
|
block = read_block()
|
|
if block is None or local_session.is_quit(block):
|
|
return 0
|
|
if (sent := _screen(gate, block)) is None:
|
|
continue
|
|
print("\n...", flush=True)
|
|
reply = ask(["--model", model, "--resume", session], sent)
|
|
if is_limit_hit(reply):
|
|
print(f"[{reply.get('result', 'usage limit')} -- continuing locally]")
|
|
return local_session.run(problem[0], history, sent, read_block, gate=gate)
|
|
print(gate.after_reply(str(reply.get("result", ""))))
|
|
history += [
|
|
{"role": "user", "content": sent},
|
|
{"role": "assistant", "content": str(reply.get("result", ""))},
|
|
]
|
|
|
|
|
|
def start_session(problem: list[str], *, local: bool) -> int:
|
|
"""One coaching session for `<url-or-slug> [stuck on...]`."""
|
|
if local:
|
|
opener = local_session.opening_message(problem[1:])
|
|
gate = phase.PhaseGate.open(problem[0])
|
|
return local_session.run(problem[0], [], opener, read_block, gate=gate)
|
|
return run_claude(os.environ.get("LEETCODE_MODEL", "sonnet"), problem)
|
|
|
|
|
|
def main(argv: list[str]) -> int:
|
|
"""Parse `[--local] <url-or-slug> [stuck on...]` (or `report`) and run it."""
|
|
local = "--local" in argv
|
|
args = [a for a in argv if a != "--local"]
|
|
if args[:1] == [report.COMMAND]:
|
|
return report.main(args[1:])
|
|
if not args or args[0] in {"-h", "--help"}:
|
|
print("Usage: leetcode [--local] <url-or-slug> [what you are stuck on...]")
|
|
print(" LEETCODE_MODEL=opus leetcode ... to use a stronger model")
|
|
print(" --local coach on the local Ollama model (free, offline)")
|
|
print(" leetcode due | --cluster <name> | --retest <slug> (--dry-run)")
|
|
print(" leetcode report req-before-code, max rung, reveal per problem")
|
|
return 0
|
|
if args[0] in cluster_cli.COMMANDS:
|
|
return cluster_cli.main(args, functools.partial(start_session, local=local))
|
|
return start_session(args, local=local)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main(sys.argv[1:]))
|