Skip to content
Draft
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
99 changes: 92 additions & 7 deletions src/rlm/prompt.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,41 @@
"benchmark, or API may have its own environment and normal interface. "
"Evaluate external systems through their own interface, then use IPython "
"to coordinate the process and analyze what comes back.\n\n"
"When running shell commands from IPython, use `%%bash` cells. Avoid "
"`!cmd` shell escapes for project commands so shell behavior is explicit "
"and multi-line commands share one shell context.\n\n"
"The IPython `code` argument is executed literally as one code cell. It "
"must contain only raw Python/IPython syntax, not chat-template wrappers, "
"native tool-call tags, JSON fields, or prose prefixes. Wrong: "
"`<arg_value>%%bash\\npytest -q` or `<arg_value>Now inspect the file`. "
"Correct: `%%bash\\npytest -q` or `print(path.read_text())`.\n\n"
"When running shell commands from IPython, use `%%bash` cells. This is a "
"hard syntax rule: if an IPython tool call contains `%%bash`, the `code` "
"argument must begin with exactly `%%bash\n` as its first seven "
"characters. No leading blank line, no leading spaces, no comments, no "
"imports, and no Python statements may appear before it. If a cell starts "
"with `# note\n%%bash`, IPython parses the whole cell as Python and the "
"shell command fails with `SyntaxError`.\n\n"
"Before every IPython tool call, silently check: does `code` contain "
"`%%bash`? If yes, the first line must be exactly `%%bash`. If not, "
"rewrite the tool call before sending it.\n\n"
"Correct:\n"
"```python\n"
"%%bash\n"
"# explanation belongs here, after the magic\n"
"cd /workspace/project && pytest -q\n"
"```\n"
"Wrong:\n"
"```python\n"
"# explanation before the magic breaks IPython\n"
"%%bash\n"
"cd /workspace/project && pytest -q\n"
"```\n"
"Wrong:\n"
"```python\n"
"\n"
"%%bash\n"
"cd /workspace/project && pytest -q\n"
"```\n\n"
"Avoid `!cmd` shell escapes for project commands so shell behavior is "
"explicit and multi-line commands share one shell context.\n\n"
"Important: do not install dependencies into the IPython kernel just to "
"make an external project import or run there. If a project import, test, "
"script, CLI, or dependency check is needed, run it through that project's "
Expand All @@ -58,10 +90,61 @@
"or the active project interpreter from the repo root. Treat failures from "
"that native environment as the relevant result."
"\n\n"
"Use Python for reading, searching, and editing files — it gives you "
"reusable variables you can slice, filter, and act on without re-reading. "
"Always assign read/search results to named variables so you can revisit "
"them later."
"Use Python for reading and searching files — it gives you reusable "
"variables you can slice, filter, and act on without re-reading. Always "
"assign read/search results to named variables so you can revisit them "
"later."
)
EDIT_SKILL_PROMPT = (
"For targeted modifications to existing files, you must use the "
"pre-imported `edit` skill from IPython instead of manual Python file "
"writes. Read and inspect files with normal Python, then make the change "
'with `await edit(path="relative/file.py", old_str=old, new_str=new)`, '
"where `old` and `new` are exact strings. Inline string literals are fine "
"when they are valid Python. The target `old_str` must appear exactly "
"once. Inside IPython `code`, never write native tool-call markup like "
"`<tool_call>`, `<arg_key>`, or `<arg_value>`; use Python "
'instead, for example: `old = "..."; new = "..."; await '
'edit(path="pkg/module.py", old_str=old, new_str=new)`. '
"The supported keyword arguments are `path`, `old_str`, `new_str`, "
"and optional `cwd`; do not use `file`, `old`, `new`, line numbers, "
"`after`, or `insert`. If an edit call fails because the string is not "
"found or not unique, inspect the file and retry with a smaller exact "
"snippet before falling back. Only use normal Python file I/O for creating "
"new files or for broad generated rewrites that cannot be expressed as "
"one or more exact replacements.\n\n"
"Before calling `edit`, make sure the Python string syntax is valid. Do "
'not wrap text that contains `"""` inside a `"""..."""` string; '
"that creates a SyntaxError before `edit` runs. If the target text "
"contains triple double quotes, use triple single quotes (`'''...'''`) or "
"assign `old`/`new` from inspected file slices. If the text contains both "
"triple quote styles or quoting becomes complex, build `old` and `new` as "
"variables first, then pass those variables to `await edit(...)`.\n\n"
"Good targeted edit pattern:\n"
"```python\n"
"from pathlib import Path\n"
'text = Path("pkg/module.py").read_text()\n'
'print(text[text.index("def broken") : text.index("def next_func")])\n'
"old = '''def broken():\n"
' """Docstring with triple double quotes."""\n'
" return False\n"
"'''\n"
"new = '''def broken():\n"
' """Docstring with triple double quotes."""\n'
" return True\n"
"'''\n"
'await edit(path="pkg/module.py", old_str=old, new_str=new)\n'
"```\n"
"Do not do this for targeted edits:\n"
"```python\n"
'await edit(path="pkg/module.py", old_str="""def broken():\n'
' """Docstring closes the outer string early."""\n'
" return False\n"
'""", new_str="""...\n'
'""")\n'
'Path("pkg/module.py").write_text(text.replace(old, new))\n'
'open("pkg/module.py", "w").write(new_contents)\n'
"```"
)


Expand Down Expand Up @@ -108,6 +191,8 @@ def build_system_prompt(
"Each skill is also available as a shell command by the same name: `<skill> ...`. "
"Discover its CLI usage with `<skill> --help`."
)
if "edit" in installed_skills:
skill_lines.append(EDIT_SKILL_PROMPT)
if skill_lines:
parts.extend(["", *skill_lines])

Expand Down
10 changes: 6 additions & 4 deletions src/rlm/tools/ipython.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@

import copy
import os
from queue import Empty
import re
import sys
import threading
import time
from queue import Empty
from typing import TYPE_CHECKING, Any

from rlm.tools.base import ToolContext, ToolOutcome
Expand All @@ -30,7 +30,10 @@
"Use !command for shell commands (e.g. !ls -la, !cat file.py, !pip install foo). "
"Use !python3 to run code with the project's own packages "
"(e.g. !python3 -m pytest, !python3 -c 'import numpy'). "
"Use %%bash for multi-line shell scripts."
"Use %%bash for multi-line shell scripts. If code contains %%bash, "
"the code string must begin exactly with %%bash followed by a "
"newline. Do not put a blank line, spaces, comments, imports, or "
"Python code before %%bash."
),
"parameters": {
"type": "object",
Expand Down Expand Up @@ -65,8 +68,7 @@ def schema(self) -> dict[str, Any]:
)
schema = copy.deepcopy(IPYTHON_SCHEMA)
schema["function"]["parameters"]["properties"]["timeout"]["description"] = (
"Optional timeout in seconds. "
f"Default: {timeout}s. Maximum: {IPYTHON_TIMEOUT_MAX_SECONDS}s."
f"Optional timeout in seconds. Default: {timeout}s. Maximum: {IPYTHON_TIMEOUT_MAX_SECONDS}s."
)
return schema

Expand Down
34 changes: 32 additions & 2 deletions tests/test_prompt.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from dataclasses import dataclass

from rlm.prompt import (
EDIT_SKILL_PROMPT,
GIT_HISTORY_GUARD_PROMPT,
IPYTHON_CONTROL_PROMPT,
build_system_prompt,
Expand All @@ -16,11 +17,15 @@ class _Tool:
name: str


def _prompt(active_tools: list[_Tool]) -> str:
def _prompt(
active_tools: list[_Tool],
*,
installed_skills: list[str] | None = None,
) -> str:
return build_system_prompt(
"/repo",
None,
[],
installed_skills or [],
"/repo/.rlm/messages.jsonl",
allow_recursion=False,
active_tools=active_tools,
Expand Down Expand Up @@ -57,8 +62,33 @@ def test_ipython_control_prompt_included_for_ipython_tool():
assert "long-lived notebook" in prompt
assert "native runtime" in prompt
assert "use `%%bash` cells" in prompt
assert "first seven characters" in prompt
assert "Before every IPython tool call" in prompt
assert "do not install dependencies into the IPython kernel" in prompt
assert "Use Python for reading and searching files" in prompt
assert "Use Python for reading, searching, and editing files" not in prompt


def test_ipython_control_prompt_omitted_without_ipython_tool():
assert IPYTHON_CONTROL_PROMPT not in _prompt([_Tool("bash")])


def test_edit_skill_prompt_included_when_edit_is_installed():
prompt = _prompt([_Tool("ipython")], installed_skills=["edit"])

assert EDIT_SKILL_PROMPT in prompt
assert 'await edit(path="relative/file.py", old_str=old, new_str=new)' in prompt
assert "pre-imported `edit` skill from IPython" in prompt
assert "`old_str` must appear exactly once" in prompt
assert "do not use `file`, `old`, `new`, line numbers" in prompt
assert "inspect the file and retry with a smaller exact snippet" in prompt
assert "Only use normal Python file I/O for creating new files" in prompt
assert 'Do not wrap text that contains `"""`' in prompt
assert "use triple single quotes" in prompt
assert "Good targeted edit pattern" in prompt


def test_edit_skill_prompt_omitted_without_edit_skill():
prompt = _prompt([_Tool("ipython")], installed_skills=["search_docs"])

assert EDIT_SKILL_PROMPT not in prompt
Loading