|
| 1 | +import { CodeBlock } from "@/components/CodeBlock"; |
| 2 | +import { Table } from "@/components/Table"; |
| 3 | + |
| 4 | +export default function IPythonPage() { |
| 5 | + return ( |
| 6 | + <div> |
| 7 | + <h1 className="text-3xl font-bold mb-4">IPythonREPL</h1> |
| 8 | + |
| 9 | + <p className="text-xl text-muted-foreground mb-6 leading-relaxed"> |
| 10 | + <strong className="text-foreground">IPythonREPL</strong> executes code inside a real{" "} |
| 11 | + <a href="https://ipython.org/" className="text-primary hover:underline font-medium">IPython</a>{" "} |
| 12 | + session instead of plain <code className="px-1.5 py-0.5 rounded bg-muted text-foreground text-sm font-semibold">exec()</code>. |
| 13 | + It supports two kernel modes — an <strong className="text-foreground">in-process</strong> shell that |
| 14 | + runs in the same Python process as the RLM (the default, fastest), and a{" "} |
| 15 | + <strong className="text-foreground">subprocess</strong> kernel that runs a real{" "} |
| 16 | + <code className="px-1.5 py-0.5 rounded bg-muted text-foreground text-sm font-semibold">ipykernel</code> in a separate |
| 17 | + Python process for hard cell timeouts and full namespace isolation from the RLM host. |
| 18 | + Both modes give the LM access to IPython's full surface (cell magics, rich |
| 19 | + repr, line tracebacks). |
| 20 | + </p> |
| 21 | + |
| 22 | + <p className="text-muted-foreground mb-4"> |
| 23 | + <strong>Prerequisite:</strong> install the optional extra: |
| 24 | + </p> |
| 25 | + <CodeBlock language="bash" code={`pip install 'rlms[ipython]' |
| 26 | +# or with uv: |
| 27 | +# uv pip install -e ".[ipython]"`} /> |
| 28 | + |
| 29 | + <CodeBlock code={`from rlm import RLM |
| 30 | +
|
| 31 | +# In-process (default kernel_mode): same process, fast. |
| 32 | +rlm = RLM( |
| 33 | + backend="openai", |
| 34 | + backend_kwargs={"model_name": "gpt-5-mini"}, |
| 35 | + environment="ipython", |
| 36 | + environment_kwargs={ |
| 37 | + "kernel_mode": "in_process", |
| 38 | + "cell_timeout": 30, # SIGALRM-based; Unix main thread only |
| 39 | + }, |
| 40 | +) |
| 41 | +
|
| 42 | +# Subprocess: separate Python process, hard timeouts, full isolation. |
| 43 | +rlm = RLM( |
| 44 | + backend="openai", |
| 45 | + backend_kwargs={"model_name": "gpt-5-mini"}, |
| 46 | + environment="ipython", |
| 47 | + environment_kwargs={ |
| 48 | + "kernel_mode": "subprocess", |
| 49 | + "cell_timeout": 30, # Hard guarantee via interrupt_kernel |
| 50 | + "startup_timeout": 60, |
| 51 | + "max_concurrent_subcalls": 4, |
| 52 | + }, |
| 53 | +)`} /> |
| 54 | + |
| 55 | + <hr className="my-8 border-border" /> |
| 56 | + |
| 57 | + <h2 className="text-2xl font-semibold mb-4">Arguments</h2> |
| 58 | + <Table |
| 59 | + headers={["Argument", "Type", "Default", "Description"]} |
| 60 | + rows={[ |
| 61 | + [<code key="1">kernel_mode</code>, <code key="2">"in_process" | "subprocess"</code>, <code key="3">"in_process"</code>, "Where the IPython session runs"], |
| 62 | + [<code key="4">cell_timeout</code>, <code key="5">float | None</code>, <code key="6">None</code>, "Per-cell timeout in seconds; None disables"], |
| 63 | + [<code key="7">startup_timeout</code>, <code key="8">float</code>, <code key="9">60.0</code>, "Subprocess kernel boot timeout"], |
| 64 | + [<code key="10">subcall_timeout</code>, <code key="11">float | None</code>, <code key="12">None</code>, "Per-request kernel→broker socket timeout (subprocess)"], |
| 65 | + [<code key="13">max_concurrent_subcalls</code>, <code key="14">int</code>, <code key="15">4</code>, "Global cap on concurrent subcall_fn invocations"], |
| 66 | + [<code key="16">setup_code</code>, <code key="17">str</code>, <code key="18">None</code>, "Code to run at initialization"], |
| 67 | + [<code key="19">custom_tools</code>, <code key="20">dict</code>, <code key="21">None</code>, "Functions / values injected into the namespace"], |
| 68 | + ]} |
| 69 | + /> |
| 70 | + |
| 71 | + <hr className="my-8 border-border" /> |
| 72 | + |
| 73 | + <h2 className="text-2xl font-semibold mb-4">In-process vs. subprocess</h2> |
| 74 | + <Table |
| 75 | + headers={["", "in_process", "subprocess"]} |
| 76 | + rows={[ |
| 77 | + ["Process", "Same as host", "Separate Python via ipykernel"], |
| 78 | + ["Subcall path", "Direct Python call", "TCP broker (4-byte length-prefixed JSON)"], |
| 79 | + [<><code key="t1">cell_timeout</code></>, "Best-effort SIGALRM (Unix, main thread)", <>Hard, via <code key="t2">interrupt_kernel</code></>], |
| 80 | + ["Cell magics (%%timeit, …)", "Yes", "Yes"], |
| 81 | + [<><code key="t3">input()</code></>, "Disabled (raises)", <>Disabled (<code key="t4">allow_stdin=False</code>)</>], |
| 82 | + ["Isolation from host", "Shares stdout/stderr/cwd/SIGALRM", "Full process isolation"], |
| 83 | + ["Custom tool injection", "Direct namespace inject", <>Pickled with <code key="t5">dill</code> over ZMQ</>], |
| 84 | + ]} |
| 85 | + /> |
| 86 | + |
| 87 | + <hr className="my-8 border-border" /> |
| 88 | + |
| 89 | + <h2 className="text-2xl font-semibold mb-4">How It Works</h2> |
| 90 | + <h3 className="text-lg font-medium mt-4 mb-2">In-process</h3> |
| 91 | + <ol className="list-decimal list-inside text-muted-foreground space-y-1 mb-4"> |
| 92 | + <li>Creates a fresh <code>InteractiveShell</code> with a per-instance user module (so multiple in-process REPLs don't share <code>sys.modules['__main__']</code>) and IPython's history database disabled.</li> |
| 93 | + <li>Injects scaffold helpers (<code>llm_query</code>, <code>rlm_query</code>, <code>FINAL_VAR</code>, <code>SHOW_VARS</code>) and a stubbed <code>input()</code> into <code>user_ns</code>.</li> |
| 94 | + <li><code>execute_code</code> runs each cell via <code>shell.run_cell</code> under an <code>RLock</code>; <code>cell_timeout</code> is enforced with <code>SIGALRM</code> + <code>setitimer</code>.</li> |
| 95 | + <li><code>rlm_query</code> calls <code>subcall_fn</code> directly, gated by a per-instance semaphore so kernel-side fan-out can't exceed <code>max_concurrent_subcalls</code>.</li> |
| 96 | + </ol> |
| 97 | + |
| 98 | + <h3 className="text-lg font-medium mt-4 mb-2">Subprocess</h3> |
| 99 | + <ol className="list-decimal list-inside text-muted-foreground space-y-1 mb-4"> |
| 100 | + <li>Starts a TCP broker on <code>127.0.0.1:0</code> (ephemeral port).</li> |
| 101 | + <li>Launches an <code>ipykernel</code> subprocess pinned to the host's <code>sys.executable</code> (so it inherits the same site-packages — important for <code>dill</code>, custom imports, etc.).</li> |
| 102 | + <li>Bootstraps kernel-side scaffold helpers that route <code>llm_query</code> to the LM Handler and <code>rlm_query</code> / <code>FINAL_VAR</code> to the broker over the 4-byte-prefixed JSON protocol.</li> |
| 103 | + <li>Each user cell first sets a unique <code>_RLM_CURRENT_CELL</code> cell-id in the kernel via a separate <code>execute_interactive</code> call (so cell magics still work). Every broker request carries this id.</li> |
| 104 | + <li><code>cell_timeout</code> is enforced by <code>kc.execute_interactive(timeout=…)</code> + <code>km.interrupt_kernel()</code>. Subcall completions whose <code>subcall_fn</code> finishes <em>after</em> the originating cell timed out are stored under that cell's id and discarded as stale on the next drain — they aren't misattributed to a later cell.</li> |
| 105 | + </ol> |
| 106 | + |
| 107 | + <pre className="text-sm">{`┌──────────────────────────────────────────┐ |
| 108 | +│ Host (RLM process) │ |
| 109 | +│ ┌─────────────┐ Socket ┌──────────────┐ │ |
| 110 | +│ │ Subcall │◄──────►│ LM Handler │ │ |
| 111 | +│ │ broker │ └──────────────┘ │ |
| 112 | +│ │ (TCP + JSON)│ │ |
| 113 | +│ └─────┬───────┘ │ |
| 114 | +└────────┼─────────────────────────────────┘ |
| 115 | + │ ZMQ (jupyter_client) |
| 116 | +┌────────┼─────────────────────────────────┐ |
| 117 | +│ ipykernel subprocess │ |
| 118 | +│ ┌─────▼────────┐ │ |
| 119 | +│ │ IPython │ rlm_query() / FINAL_VAR│ |
| 120 | +│ │ user_ns │ → broker over TCP │ |
| 121 | +│ │ (cell_id │ llm_query() → LM Handler│ |
| 122 | +│ │ tagged) │ │ |
| 123 | +│ └──────────────┘ │ |
| 124 | +└──────────────────────────────────────────┘`}</pre> |
| 125 | + |
| 126 | + <hr className="my-8 border-border" /> |
| 127 | + |
| 128 | + <h2 className="text-2xl font-semibold mb-4">Notable behavior</h2> |
| 129 | + <ul className="list-disc list-inside text-muted-foreground space-y-2"> |
| 130 | + <li><strong className="text-foreground">Per-instance serialization.</strong> <code>execute_code</code> takes an <code>RLock</code>, so concurrent calls from different threads are serialized within an instance.</li> |
| 131 | + <li><strong className="text-foreground">Global subcall cap.</strong> <code>max_concurrent_subcalls</code> bounds <em>total</em> in-flight <code>subcall_fn</code> invocations on the instance — even if user code spawns kernel-side threads that each fan out a batch.</li> |
| 132 | + <li><strong className="text-foreground">Reentry guard.</strong> If <code>subcall_fn</code> calls <code>execute_code</code> back on the parent REPL (or a cell traverses <code>rlm_query.__self__.execute_code(…)</code> in in-process mode), the call raises <code>RuntimeError</code> instead of deadlocking the cell lock or corrupting the in-flight cell's tracking. <code>subcall_fn</code> should spawn a child REPL.</li> |
| 133 | + <li><strong className="text-foreground">Cell-id attribution.</strong> Subcall completions are tagged with the originating cell's id so a slow <code>subcall_fn</code> that finishes after its cell timed out is never counted under a later cell. Long-lived kernel threads that call <code>rlm_query</code> after their spawning cell ends will, however, be tagged with whatever cell is active at call time.</li> |
| 134 | + <li><strong className="text-foreground">In-process is not isolated.</strong> Two in-process instances each get a unique <code>__main__</code> substitute, but they still share the host's stdout/stderr/cwd/SIGALRM. Use <code>subprocess</code> if you need true isolation.</li> |
| 135 | + </ul> |
| 136 | + |
| 137 | + <hr className="my-8 border-border" /> |
| 138 | + |
| 139 | + <h2 className="text-2xl font-semibold mb-4">When to use which mode</h2> |
| 140 | + <ul className="list-disc list-inside text-muted-foreground space-y-1.5"> |
| 141 | + <li><strong className="text-foreground">in_process</strong> — fastest path, no IPC, fine for trusted code, development, short-lived cells. <code>cell_timeout</code> is best-effort (Unix main thread only).</li> |
| 142 | + <li><strong className="text-foreground">subprocess</strong> — when you need a hard <code>cell_timeout</code> guarantee, or want full namespace / signal / cwd isolation between the LM's code and the RLM host.</li> |
| 143 | + </ul> |
| 144 | + </div> |
| 145 | + ); |
| 146 | +} |
0 commit comments