Skip to content

Commit 2c2a6c0

Browse files
committed
feat: added patch tool, added more integration test examples
1 parent f77a34a commit 2c2a6c0

File tree

5 files changed

+177
-33
lines changed

5 files changed

+177
-33
lines changed

gptme/commands.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ def handle_cmd(
164164
sys.exit(0)
165165
case "replay":
166166
log.undo(1, quiet=True)
167+
log.write()
167168
print("Replaying conversation...")
168169
for msg in log.log:
169170
if msg.role == "assistant":

gptme/prompts.py

Lines changed: 4 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from .config import get_config
99
from .message import Message
10+
from .tools import patch
1011

1112
USER = os.environ.get("USER", None)
1213

@@ -160,29 +161,9 @@ def initial_prompt(short: bool = False) -> Generator[Message, None, None]:
160161
python hello.py
161162
```
162163
> stdout: `Hello world!`
163-
164-
## Producing and applying patches
165-
Use `diff` and `patch` to produce and apply patches.
166-
167-
> User: add a name input to hello.py
168-
First we create the patch:
169-
```hello.patch
170-
@@ -1,1 +1,2 @@
171-
-print("Hello world!")
172-
+name = input("What is your name? ")
173-
+print(f"Hello {name}!")
174-
```
175-
176-
Now, we apply it:
177-
```bash
178-
patch hello.py < hello.patch
179-
```
180-
181-
Now, we can run it:
182-
```bash
183-
echo "John" | python hello.py
184-
```
185-
""".strip(),
164+
""".strip()
165+
+ "\n\n"
166+
+ patch.instructions,
186167
hide=True,
187168
pinned=True,
188169
)

gptme/tools/__init__.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from typing import Generator
33

44
from ..message import Message
5+
from .patch import execute_patch
56
from .python import execute_python, init_python
67
from .save import execute_save
78
from .shell import execute_shell
@@ -32,16 +33,19 @@ def execute_msg(msg: Message, ask: bool) -> Generator[Message, None, None]:
3233
def execute_codeblock(codeblock: str, ask: bool) -> Generator[Message, None, None]:
3334
"""Executes a codeblock and returns the output."""
3435
lang_or_fn = codeblock.splitlines()[0].strip()
35-
codeblock = codeblock[len(lang_or_fn) :]
36+
codeblock_content = codeblock[len(lang_or_fn) :]
3637

3738
is_filename = lang_or_fn.count(".") >= 1
3839

3940
if lang_or_fn in ["python", "py"]:
40-
yield from execute_python(codeblock, ask=ask)
41+
yield from execute_python(codeblock_content, ask=ask)
4142
elif lang_or_fn in ["terminal", "bash", "sh"]:
42-
yield from execute_shell(codeblock, ask=ask)
43+
yield from execute_shell(codeblock_content, ask=ask)
44+
elif lang_or_fn.startswith("patch "):
45+
fn = lang_or_fn[len("patch ") :]
46+
yield from execute_patch(f"```{codeblock}```", fn, ask=ask)
4347
elif is_filename:
44-
yield from execute_save(lang_or_fn, codeblock, ask=ask)
48+
yield from execute_save(lang_or_fn, codeblock_content, ask=ask)
4549
else:
4650
logger.warning(
4751
f"Unknown codeblock type '{lang_or_fn}', neither supported language or filename."

gptme/tools/patch.py

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
"""
2+
Gives the LLM agent the ability to patch files, by using a adapted version git conflict markers.
3+
4+
Inspired by aider.
5+
"""
6+
7+
import re
8+
from pathlib import Path
9+
from typing import Generator
10+
11+
from ..message import Message
12+
from ..util import ask_execute
13+
14+
example_patch = """
15+
```patch filename.py
16+
<<<<<<< ORIGINAL
17+
original lines
18+
=======
19+
modified lines
20+
>>>>>>> UPDATED
21+
```
22+
"""
23+
24+
instructions = """
25+
# Patching files
26+
27+
The LLM agent can patch files, by using a adapted version git conflict markers.
28+
This can be used to make changes to files we have written in the past, without having to rewrite the whole file.
29+
30+
## Example
31+
32+
> User: Patch the file `hello.py` to ask for the name of the user.
33+
```hello.py
34+
def hello():
35+
print("hello world")
36+
```
37+
38+
> Assistant:
39+
```patch hello.py
40+
<<<<<<< ORIGINAL
41+
print("hello world")
42+
=======
43+
name = input("What is your name? ")
44+
print(f"hello {name}")
45+
>>>>>>> UPDATED
46+
```
47+
"""
48+
49+
50+
def apply(codeblock: str, content: str) -> str:
51+
"""
52+
Applies the patch to the file.
53+
"""
54+
codeblock = codeblock.strip()
55+
56+
# get the original chunk
57+
original = re.split("\n<<<<<<< ORIGINAL\n", codeblock)[1]
58+
original = re.split("\n=======\n", original)[0]
59+
60+
# get the modified chunk
61+
modified = re.split("\n=======\n", codeblock)[1]
62+
modified = re.split("\n>>>>>>> UPDATED\n", modified)[0]
63+
64+
# replace the original chunk with the modified chunk
65+
content = content.replace(original, modified)
66+
67+
return content
68+
69+
70+
def apply_file(codeblock, filename):
71+
codeblock = codeblock.strip()
72+
_patch, filename = codeblock.splitlines()[0].split()
73+
assert _patch == "```patch"
74+
assert Path(filename).exists()
75+
76+
with open(filename, "r") as f:
77+
content = f.read()
78+
79+
result = apply(codeblock, content)
80+
81+
with open(filename, "w") as f:
82+
f.write(result)
83+
84+
print(f"Applied patch to {filename}")
85+
86+
87+
def execute_patch(codeblock: str, fn: str, ask: bool) -> Generator[Message, None, None]:
88+
"""
89+
Executes the patch.
90+
"""
91+
if ask:
92+
confirm = ask_execute()
93+
if not confirm:
94+
print("Patch not applied")
95+
return
96+
97+
apply_file(codeblock, fn)
98+
yield Message("system", "Patch applied")
99+
100+
101+
def test_apply_simple():
102+
codeblock = example_patch
103+
content = """original lines"""
104+
result = apply(codeblock, content)
105+
assert result == """modified lines"""
106+
107+
108+
def test_apply_function():
109+
content = """
110+
def hello():
111+
print("hello")
112+
113+
if __name__ == "__main__":
114+
hello()
115+
"""
116+
117+
codeblock = """
118+
```patch test.py
119+
<<<<<<< ORIGINAL
120+
def hello():
121+
print("hello")
122+
=======
123+
def hello(name="world"):
124+
print(f"hello {name}")
125+
>>>>>>> UPDATED
126+
```
127+
"""
128+
129+
result = apply(codeblock, content)
130+
assert result.startswith(
131+
"""
132+
def hello(name="world"):
133+
print(f"hello {name}")
134+
"""
135+
)

tests/test-integration.sh

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,46 @@ cd "$(dirname "$0")"
88
mkdir -p output
99
cd output
1010

11+
# run interactive tests if not in CI (GITHUB_ACTIONS is set by github actions)
12+
interactive=${GITHUB_ACTIONS:-1}
13+
1114
# set this to indicate tests are run (non-interactive)
1215
export PYTEST_CURRENT_TEST=1
1316

1417
# test stdin and cli-provided prompt
1518
echo "The project mascot is a flying pig" | gptme "What is the project mascot?"
1619

20+
# test load context from file
21+
echo "The project mascot is a flying pig" > mascot.txt
22+
gptme "What is the project mascot?" mascot.txt
23+
1724
# test python command
1825
gptme "/python print('hello world')"
1926

2027
# test shell command
2128
gptme "/shell echo 'hello world'"
2229

23-
# interactive matplotlib
24-
gptme 'plot an x^2 graph'
30+
# test write small game
31+
gptme 'write a snake game with curses to snake.py'
32+
# works!
33+
34+
# test implement game of life
35+
gptme 'write an implementation of the game of life with curses to life.py'
36+
# works? almost, needed to try-catch-pass an exception
37+
38+
# test implement wireworld
39+
gptme 'write a implementation of wireworld with curses to wireworld.py'
40+
# works? almost, needed to try-catch-pass an exception, fix color setup, and build a proper circuit
41+
42+
# test plot to file
43+
gptme 'plot up to the 5rd degree taylor expansion of sin(x), save to sin.png'
44+
# works!
2545

26-
# matplotlib to file
27-
gptme 'plot up to the 3rd degree taylor expansion of sin(x), save to sin.png'
46+
# write C code and apply patch
47+
gptme 'write a hello world program in c to hello.c, then patch it to ask for your name and print it'
48+
# works!
2849

29-
# interactive curses
30-
gptme 'write a snake game with curses'
50+
if [ "$interactive" = "1" ]; then
51+
# interactive matplotlib
52+
gptme 'plot an x^2 graph'
53+
fi

0 commit comments

Comments
 (0)