wcgw 1.0.0__py3-none-any.whl → 1.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of wcgw might be problematic. Click here for more details.
- wcgw/__init__.py +1 -1
- wcgw/client/anthropic_client.py +415 -0
- wcgw/client/cli.py +40 -0
- wcgw/client/diff-instructions.txt +44 -0
- wcgw/client/{basic.py → openai_client.py} +41 -13
- wcgw/client/tools.py +223 -105
- wcgw/relay/serve.py +22 -71
- wcgw/types_.py +26 -0
- {wcgw-1.0.0.dist-info → wcgw-1.1.0.dist-info}/METADATA +3 -2
- wcgw-1.1.0.dist-info/RECORD +17 -0
- {wcgw-1.0.0.dist-info → wcgw-1.1.0.dist-info}/WHEEL +1 -1
- wcgw/client/claude.py +0 -384
- wcgw-1.0.0.dist-info/RECORD +0 -15
- /wcgw/client/{openai_adapters.py → __init__.py} +0 -0
- {wcgw-1.0.0.dist-info → wcgw-1.1.0.dist-info}/entry_points.txt +0 -0
wcgw/__init__.py
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
from .client.
|
|
1
|
+
from .client.cli import app
|
|
2
2
|
from .client.tools import run as listen
|
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import json
|
|
3
|
+
import mimetypes
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import sys
|
|
6
|
+
import traceback
|
|
7
|
+
from typing import Callable, DefaultDict, Optional, cast
|
|
8
|
+
import anthropic
|
|
9
|
+
from anthropic import Anthropic
|
|
10
|
+
from anthropic.types import (
|
|
11
|
+
ToolParam,
|
|
12
|
+
MessageParam,
|
|
13
|
+
ToolResultBlockParam,
|
|
14
|
+
ToolUseBlockParam,
|
|
15
|
+
ImageBlockParam,
|
|
16
|
+
TextBlockParam,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
import rich
|
|
20
|
+
import petname # type: ignore[import-untyped]
|
|
21
|
+
from typer import Typer
|
|
22
|
+
import uuid
|
|
23
|
+
|
|
24
|
+
from ..types_ import (
|
|
25
|
+
BashCommand,
|
|
26
|
+
BashInteraction,
|
|
27
|
+
CreateFileNew,
|
|
28
|
+
FileEditFindReplace,
|
|
29
|
+
FullFileEdit,
|
|
30
|
+
ReadImage,
|
|
31
|
+
Writefile,
|
|
32
|
+
ResetShell,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
from .common import Models, discard_input
|
|
36
|
+
from .common import CostData
|
|
37
|
+
from .tools import ImageData
|
|
38
|
+
|
|
39
|
+
from .tools import (
|
|
40
|
+
DoneFlag,
|
|
41
|
+
get_tool_output,
|
|
42
|
+
SHELL,
|
|
43
|
+
start_shell,
|
|
44
|
+
which_tool_name,
|
|
45
|
+
)
|
|
46
|
+
import tiktoken
|
|
47
|
+
|
|
48
|
+
from urllib import parse
|
|
49
|
+
import subprocess
|
|
50
|
+
import os
|
|
51
|
+
import tempfile
|
|
52
|
+
|
|
53
|
+
import toml
|
|
54
|
+
from pydantic import BaseModel
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
from dotenv import load_dotenv
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
History = list[MessageParam]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def text_from_editor(console: rich.console.Console) -> str:
|
|
64
|
+
# First consume all the input till now
|
|
65
|
+
discard_input()
|
|
66
|
+
console.print("\n---------------------------------------\n# User message")
|
|
67
|
+
data = input()
|
|
68
|
+
if data:
|
|
69
|
+
return data
|
|
70
|
+
editor = os.environ.get("EDITOR", "vim")
|
|
71
|
+
with tempfile.NamedTemporaryFile(suffix=".tmp") as tf:
|
|
72
|
+
subprocess.run([editor, tf.name], check=True)
|
|
73
|
+
with open(tf.name, "r") as f:
|
|
74
|
+
data = f.read()
|
|
75
|
+
console.print(data)
|
|
76
|
+
return data
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def save_history(history: History, session_id: str) -> None:
|
|
80
|
+
myid = str(history[1]["content"]).replace("/", "_").replace(" ", "_").lower()[:60]
|
|
81
|
+
myid += "_" + session_id
|
|
82
|
+
myid = myid + ".json"
|
|
83
|
+
|
|
84
|
+
mypath = Path(".wcgw") / myid
|
|
85
|
+
mypath.parent.mkdir(parents=True, exist_ok=True)
|
|
86
|
+
with open(mypath, "w") as f:
|
|
87
|
+
json.dump(history, f, indent=3)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def parse_user_message_special(msg: str) -> MessageParam:
|
|
91
|
+
# Search for lines starting with `%` and treat them as special commands
|
|
92
|
+
parts: list[ImageBlockParam | TextBlockParam] = []
|
|
93
|
+
for line in msg.split("\n"):
|
|
94
|
+
if line.startswith("%"):
|
|
95
|
+
args = line[1:].strip().split(" ")
|
|
96
|
+
command = args[0]
|
|
97
|
+
assert command == "image"
|
|
98
|
+
image_path = " ".join(args[1:])
|
|
99
|
+
with open(image_path, "rb") as f:
|
|
100
|
+
image_bytes = f.read()
|
|
101
|
+
image_b64 = base64.b64encode(image_bytes).decode("utf-8")
|
|
102
|
+
image_type = mimetypes.guess_type(image_path)[0]
|
|
103
|
+
parts.append(
|
|
104
|
+
{
|
|
105
|
+
"type": "image",
|
|
106
|
+
"source": {
|
|
107
|
+
"type": "base64",
|
|
108
|
+
"media_type": image_type,
|
|
109
|
+
"data": image_b64,
|
|
110
|
+
},
|
|
111
|
+
}
|
|
112
|
+
)
|
|
113
|
+
else:
|
|
114
|
+
if len(parts) > 0 and parts[-1]["type"] == "text":
|
|
115
|
+
parts[-1]["text"] += "\n" + line
|
|
116
|
+
else:
|
|
117
|
+
parts.append({"type": "text", "text": line})
|
|
118
|
+
return {"role": "user", "content": parts}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
app = Typer(pretty_exceptions_show_locals=False)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@app.command()
|
|
125
|
+
def loop(
|
|
126
|
+
first_message: Optional[str] = None,
|
|
127
|
+
limit: Optional[float] = None,
|
|
128
|
+
resume: Optional[str] = None,
|
|
129
|
+
) -> tuple[str, float]:
|
|
130
|
+
load_dotenv()
|
|
131
|
+
|
|
132
|
+
session_id = str(uuid.uuid4())[:6]
|
|
133
|
+
|
|
134
|
+
history: History = []
|
|
135
|
+
waiting_for_assistant = False
|
|
136
|
+
if resume:
|
|
137
|
+
if resume == "latest":
|
|
138
|
+
resume_path = sorted(Path(".wcgw").iterdir(), key=os.path.getmtime)[-1]
|
|
139
|
+
else:
|
|
140
|
+
resume_path = Path(resume)
|
|
141
|
+
if not resume_path.exists():
|
|
142
|
+
raise FileNotFoundError(f"File {resume} not found")
|
|
143
|
+
with resume_path.open() as f:
|
|
144
|
+
history = json.load(f)
|
|
145
|
+
if len(history) <= 2:
|
|
146
|
+
raise ValueError("Invalid history file")
|
|
147
|
+
first_message = ""
|
|
148
|
+
waiting_for_assistant = history[-1]["role"] != "assistant"
|
|
149
|
+
|
|
150
|
+
limit = 1
|
|
151
|
+
|
|
152
|
+
enc = tiktoken.encoding_for_model(
|
|
153
|
+
"gpt-4o-2024-08-06",
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
tools = [
|
|
157
|
+
ToolParam(
|
|
158
|
+
input_schema=BashCommand.model_json_schema(),
|
|
159
|
+
name="BashCommand",
|
|
160
|
+
description="""
|
|
161
|
+
- Execute a bash command. This is stateful (beware with subsequent calls).
|
|
162
|
+
- Do not use interactive commands like nano. Prefer writing simpler commands.
|
|
163
|
+
- Status of the command and the current working directory will always be returned at the end.
|
|
164
|
+
- Optionally `exit shell has restarted` is the output, in which case environment resets, you can run fresh commands.
|
|
165
|
+
- The first line might be `(...truncated)` if the output is too long.
|
|
166
|
+
- Always run `pwd` if you get any file or directory not found error to make sure you're not lost.
|
|
167
|
+
""",
|
|
168
|
+
),
|
|
169
|
+
ToolParam(
|
|
170
|
+
input_schema=BashInteraction.model_json_schema(),
|
|
171
|
+
name="BashInteraction",
|
|
172
|
+
description="""
|
|
173
|
+
- Interact with running program using this tool
|
|
174
|
+
- Special keys like arrows, interrupts, enter, etc.
|
|
175
|
+
- Send text input to the running program.
|
|
176
|
+
- Send send_specials=["Enter"] to recheck status of a running program.
|
|
177
|
+
- Only one of send_text, send_specials, send_ascii should be provided.
|
|
178
|
+
""",
|
|
179
|
+
),
|
|
180
|
+
ToolParam(
|
|
181
|
+
input_schema=CreateFileNew.model_json_schema(),
|
|
182
|
+
name="CreateFileNew",
|
|
183
|
+
description="""
|
|
184
|
+
- Write content to a new file. Provide file path and content. Use this instead of BashCommand for writing new files.
|
|
185
|
+
- This doesn't create any directories, please create directories using `mkdir -p` BashCommand.
|
|
186
|
+
- Provide absolute file path only.
|
|
187
|
+
- For editing existing files, use FullFileEdit.
|
|
188
|
+
""",
|
|
189
|
+
),
|
|
190
|
+
ToolParam(
|
|
191
|
+
input_schema=ReadImage.model_json_schema(),
|
|
192
|
+
name="ReadImage",
|
|
193
|
+
description="Read an image from the shell.",
|
|
194
|
+
),
|
|
195
|
+
ToolParam(
|
|
196
|
+
input_schema=ResetShell.model_json_schema(),
|
|
197
|
+
name="ResetShell",
|
|
198
|
+
description="Resets the shell. Use only if all interrupts and prompt reset attempts have failed repeatedly.",
|
|
199
|
+
),
|
|
200
|
+
ToolParam(
|
|
201
|
+
input_schema=FullFileEdit.model_json_schema(),
|
|
202
|
+
name="FullFileEdit",
|
|
203
|
+
description="""
|
|
204
|
+
- Use absolute file path only.
|
|
205
|
+
- Use SEARCH/REPLACE blocks to edit the file.
|
|
206
|
+
""",
|
|
207
|
+
),
|
|
208
|
+
]
|
|
209
|
+
uname_sysname = os.uname().sysname
|
|
210
|
+
uname_machine = os.uname().machine
|
|
211
|
+
|
|
212
|
+
system = f"""
|
|
213
|
+
You're a cli assistant.
|
|
214
|
+
|
|
215
|
+
Instructions:
|
|
216
|
+
|
|
217
|
+
- You should use the provided bash execution tool to run script to complete objective.
|
|
218
|
+
- Do not use sudo. Do not use interactive commands.
|
|
219
|
+
- Ask user for confirmation before running anything major
|
|
220
|
+
|
|
221
|
+
System information:
|
|
222
|
+
- System: {uname_sysname}
|
|
223
|
+
- Machine: {uname_machine}
|
|
224
|
+
"""
|
|
225
|
+
|
|
226
|
+
with open(os.path.join(os.path.dirname(__file__), "diff-instructions.txt")) as f:
|
|
227
|
+
system += f.read()
|
|
228
|
+
|
|
229
|
+
if history:
|
|
230
|
+
if (
|
|
231
|
+
(last_msg := history[-1])["role"] == "user"
|
|
232
|
+
and isinstance((content := last_msg["content"]), dict)
|
|
233
|
+
and content["type"] == "tool_result"
|
|
234
|
+
):
|
|
235
|
+
waiting_for_assistant = True
|
|
236
|
+
|
|
237
|
+
client = Anthropic()
|
|
238
|
+
|
|
239
|
+
cost: float = 0
|
|
240
|
+
input_toks = 0
|
|
241
|
+
output_toks = 0
|
|
242
|
+
system_console = rich.console.Console(style="blue", highlight=False, markup=False)
|
|
243
|
+
error_console = rich.console.Console(style="red", highlight=False, markup=False)
|
|
244
|
+
user_console = rich.console.Console(
|
|
245
|
+
style="bright_black", highlight=False, markup=False
|
|
246
|
+
)
|
|
247
|
+
assistant_console = rich.console.Console(
|
|
248
|
+
style="white bold", highlight=False, markup=False
|
|
249
|
+
)
|
|
250
|
+
while True:
|
|
251
|
+
if cost > limit:
|
|
252
|
+
system_console.print(
|
|
253
|
+
f"\nCost limit exceeded. Current cost: {cost}, input tokens: {input_toks}, output tokens: {output_toks}"
|
|
254
|
+
)
|
|
255
|
+
break
|
|
256
|
+
|
|
257
|
+
if not waiting_for_assistant:
|
|
258
|
+
if first_message:
|
|
259
|
+
msg = first_message
|
|
260
|
+
first_message = ""
|
|
261
|
+
else:
|
|
262
|
+
msg = text_from_editor(user_console)
|
|
263
|
+
|
|
264
|
+
history.append(parse_user_message_special(msg))
|
|
265
|
+
else:
|
|
266
|
+
waiting_for_assistant = False
|
|
267
|
+
|
|
268
|
+
cost_, input_toks_ = 0, 0
|
|
269
|
+
cost += cost_
|
|
270
|
+
input_toks += input_toks_
|
|
271
|
+
|
|
272
|
+
stream = client.messages.stream(
|
|
273
|
+
model="claude-3-5-sonnet-20241022",
|
|
274
|
+
messages=history,
|
|
275
|
+
tools=tools,
|
|
276
|
+
max_tokens=8096,
|
|
277
|
+
system=system,
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
system_console.print(
|
|
281
|
+
"\n---------------------------------------\n# Assistant response",
|
|
282
|
+
style="bold",
|
|
283
|
+
)
|
|
284
|
+
_histories: History = []
|
|
285
|
+
full_response: str = ""
|
|
286
|
+
|
|
287
|
+
tool_calls = []
|
|
288
|
+
tool_results: list[ToolResultBlockParam] = []
|
|
289
|
+
try:
|
|
290
|
+
with stream as stream_:
|
|
291
|
+
for chunk in stream_:
|
|
292
|
+
type_ = chunk.type
|
|
293
|
+
if type_ in {"message_start", "message_stop"}:
|
|
294
|
+
continue
|
|
295
|
+
elif type_ == "content_block_start":
|
|
296
|
+
content_block = chunk.content_block
|
|
297
|
+
if content_block.type == "text":
|
|
298
|
+
chunk_str = content_block.text
|
|
299
|
+
assistant_console.print(chunk_str, end="")
|
|
300
|
+
full_response += chunk_str
|
|
301
|
+
elif content_block.type == "tool_use":
|
|
302
|
+
assert content_block.input == {}
|
|
303
|
+
tool_calls.append(
|
|
304
|
+
{
|
|
305
|
+
"name": content_block.name,
|
|
306
|
+
"input": "",
|
|
307
|
+
"done": False,
|
|
308
|
+
"id": content_block.id,
|
|
309
|
+
}
|
|
310
|
+
)
|
|
311
|
+
else:
|
|
312
|
+
error_console.log(
|
|
313
|
+
f"Ignoring unknown content block type {content_block.type}"
|
|
314
|
+
)
|
|
315
|
+
elif type_ == "content_block_delta":
|
|
316
|
+
if chunk.delta.type == "text_delta":
|
|
317
|
+
chunk_str = chunk.delta.text
|
|
318
|
+
assistant_console.print(chunk_str, end="")
|
|
319
|
+
full_response += chunk_str
|
|
320
|
+
elif chunk.delta.type == "input_json_delta":
|
|
321
|
+
tool_calls[-1]["input"] += chunk.delta.partial_json
|
|
322
|
+
else:
|
|
323
|
+
error_console.log(
|
|
324
|
+
f"Ignoring unknown content block delta type {chunk.delta.type}"
|
|
325
|
+
)
|
|
326
|
+
elif type_ == "content_block_stop":
|
|
327
|
+
if tool_calls and not tool_calls[-1]["done"]:
|
|
328
|
+
tc = tool_calls[-1]
|
|
329
|
+
tool_parsed = which_tool_name(
|
|
330
|
+
tc["name"]
|
|
331
|
+
).model_validate_json(tc["input"])
|
|
332
|
+
system_console.print(
|
|
333
|
+
f"\n---------------------------------------\n# Assistant invoked tool: {tool_parsed}"
|
|
334
|
+
)
|
|
335
|
+
_histories.append(
|
|
336
|
+
{
|
|
337
|
+
"role": "assistant",
|
|
338
|
+
"content": [
|
|
339
|
+
ToolUseBlockParam(
|
|
340
|
+
id=tc["id"],
|
|
341
|
+
name=tc["name"],
|
|
342
|
+
input=tool_parsed.model_dump(),
|
|
343
|
+
type="tool_use",
|
|
344
|
+
)
|
|
345
|
+
],
|
|
346
|
+
}
|
|
347
|
+
)
|
|
348
|
+
try:
|
|
349
|
+
output_or_done, _ = get_tool_output(
|
|
350
|
+
tool_parsed,
|
|
351
|
+
enc,
|
|
352
|
+
limit - cost,
|
|
353
|
+
loop,
|
|
354
|
+
max_tokens=8096,
|
|
355
|
+
)
|
|
356
|
+
except Exception as e:
|
|
357
|
+
output_or_done = (
|
|
358
|
+
f"GOT EXCEPTION while calling tool. Error: {e}"
|
|
359
|
+
)
|
|
360
|
+
tb = traceback.format_exc()
|
|
361
|
+
error_console.print(output_or_done + "\n" + tb)
|
|
362
|
+
|
|
363
|
+
if isinstance(output_or_done, DoneFlag):
|
|
364
|
+
system_console.print(
|
|
365
|
+
f"\n# Task marked done, with output {output_or_done.task_output}",
|
|
366
|
+
)
|
|
367
|
+
return output_or_done.task_output, cost
|
|
368
|
+
|
|
369
|
+
output = output_or_done
|
|
370
|
+
if isinstance(output, ImageData):
|
|
371
|
+
tool_results.append(
|
|
372
|
+
ToolResultBlockParam(
|
|
373
|
+
type="tool_result",
|
|
374
|
+
tool_use_id=tc["id"],
|
|
375
|
+
content=[
|
|
376
|
+
{
|
|
377
|
+
"type": "image",
|
|
378
|
+
"source": {
|
|
379
|
+
"type": "base64",
|
|
380
|
+
"media_type": output.media_type,
|
|
381
|
+
"data": output.data,
|
|
382
|
+
},
|
|
383
|
+
}
|
|
384
|
+
],
|
|
385
|
+
)
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
else:
|
|
389
|
+
tool_results.append(
|
|
390
|
+
ToolResultBlockParam(
|
|
391
|
+
type="tool_result",
|
|
392
|
+
tool_use_id=tc["id"],
|
|
393
|
+
content=output,
|
|
394
|
+
)
|
|
395
|
+
)
|
|
396
|
+
else:
|
|
397
|
+
_histories.append(
|
|
398
|
+
{"role": "assistant", "content": full_response}
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
except KeyboardInterrupt:
|
|
402
|
+
waiting_for_assistant = False
|
|
403
|
+
input("Interrupted...enter to redo the current turn")
|
|
404
|
+
else:
|
|
405
|
+
history.extend(_histories)
|
|
406
|
+
if tool_results:
|
|
407
|
+
history.append({"role": "user", "content": tool_results})
|
|
408
|
+
waiting_for_assistant = True
|
|
409
|
+
save_history(history, session_id)
|
|
410
|
+
|
|
411
|
+
return "Couldn't finish the task", cost
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
if __name__ == "__main__":
|
|
415
|
+
app()
|
wcgw/client/cli.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
from typing import Optional
|
|
3
|
+
from typer import Typer
|
|
4
|
+
import typer
|
|
5
|
+
|
|
6
|
+
from .openai_client import loop as openai_loop
|
|
7
|
+
from .anthropic_client import loop as claude_loop
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
app = Typer(pretty_exceptions_show_locals=False)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@app.command()
|
|
14
|
+
def loop(
|
|
15
|
+
claude: bool = False,
|
|
16
|
+
first_message: Optional[str] = None,
|
|
17
|
+
limit: Optional[float] = None,
|
|
18
|
+
resume: Optional[str] = None,
|
|
19
|
+
version: bool = typer.Option(False, "--version", "-v"),
|
|
20
|
+
) -> tuple[str, float]:
|
|
21
|
+
if version:
|
|
22
|
+
version_ = importlib.metadata.version("wcgw")
|
|
23
|
+
print(f"wcgw version: {version_}")
|
|
24
|
+
exit()
|
|
25
|
+
if claude:
|
|
26
|
+
return claude_loop(
|
|
27
|
+
first_message=first_message,
|
|
28
|
+
limit=limit,
|
|
29
|
+
resume=resume,
|
|
30
|
+
)
|
|
31
|
+
else:
|
|
32
|
+
return openai_loop(
|
|
33
|
+
first_message=first_message,
|
|
34
|
+
limit=limit,
|
|
35
|
+
resume=resume,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
if __name__ == "__main__":
|
|
40
|
+
app()
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
|
|
2
|
+
Instructions for
|
|
3
|
+
Only edit the files using the following SEARCH/REPLACE blocks.
|
|
4
|
+
```
|
|
5
|
+
<<<<<<< SEARCH
|
|
6
|
+
=======
|
|
7
|
+
def hello():
|
|
8
|
+
"print a greeting"
|
|
9
|
+
|
|
10
|
+
print("hello")
|
|
11
|
+
>>>>>>> REPLACE
|
|
12
|
+
|
|
13
|
+
<<<<<<< SEARCH
|
|
14
|
+
def hello():
|
|
15
|
+
"print a greeting"
|
|
16
|
+
|
|
17
|
+
print("hello")
|
|
18
|
+
=======
|
|
19
|
+
from hello import hello
|
|
20
|
+
>>>>>>> REPLACE
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
# *SEARCH/REPLACE block* Rules:
|
|
24
|
+
|
|
25
|
+
Every *SEARCH/REPLACE block* must use this format:
|
|
26
|
+
1. The start of search block: <<<<<<< SEARCH
|
|
27
|
+
2. A contiguous chunk of lines to search for in the existing source code
|
|
28
|
+
3. The dividing line: =======
|
|
29
|
+
4. The lines to replace into the source code
|
|
30
|
+
5. The end of the replace block: >>>>>>> REPLACE
|
|
31
|
+
|
|
32
|
+
Use the *FULL* file path, as shown to you by the user.
|
|
33
|
+
|
|
34
|
+
Every *SEARCH* section must *EXACTLY MATCH* the existing file content, character for character, including all comments, docstrings, etc.
|
|
35
|
+
If the file contains code or other data wrapped/escaped in json/xml/quotes or other containers, you need to propose edits to the literal contents of the file, including the container markup.
|
|
36
|
+
|
|
37
|
+
*SEARCH/REPLACE* blocks will *only* replace the first match occurrence.
|
|
38
|
+
Including multiple unique *SEARCH/REPLACE* blocks if needed.
|
|
39
|
+
Include enough lines in each SEARCH section to uniquely match each set of lines that need to change.
|
|
40
|
+
|
|
41
|
+
Keep *SEARCH/REPLACE* blocks concise.
|
|
42
|
+
Break large *SEARCH/REPLACE* blocks into a series of smaller blocks that each change a small portion of the file.
|
|
43
|
+
Include just the changing lines, and a few surrounding lines if needed for uniqueness.
|
|
44
|
+
Do not include long runs of unchanging lines in *SEARCH/REPLACE* blocks.
|
|
@@ -20,7 +20,15 @@ import petname # type: ignore[import-untyped]
|
|
|
20
20
|
from typer import Typer
|
|
21
21
|
import uuid
|
|
22
22
|
|
|
23
|
-
from ..types_ import
|
|
23
|
+
from ..types_ import (
|
|
24
|
+
BashCommand,
|
|
25
|
+
BashInteraction,
|
|
26
|
+
CreateFileNew,
|
|
27
|
+
FullFileEdit,
|
|
28
|
+
ReadImage,
|
|
29
|
+
Writefile,
|
|
30
|
+
ResetShell,
|
|
31
|
+
)
|
|
24
32
|
|
|
25
33
|
from .common import Models, discard_input
|
|
26
34
|
from .common import CostData, History
|
|
@@ -91,7 +99,7 @@ def parse_user_message_special(msg: str) -> ChatCompletionUserMessageParam:
|
|
|
91
99
|
args = line[1:].strip().split(" ")
|
|
92
100
|
command = args[0]
|
|
93
101
|
assert command == "image"
|
|
94
|
-
image_path = args[1]
|
|
102
|
+
image_path = " ".join(args[1:])
|
|
95
103
|
with open(image_path, "rb") as f:
|
|
96
104
|
image_bytes = f.read()
|
|
97
105
|
image_b64 = base64.b64encode(image_bytes).decode("utf-8")
|
|
@@ -134,13 +142,11 @@ def loop(
|
|
|
134
142
|
history = json.load(f)
|
|
135
143
|
if len(history) <= 2:
|
|
136
144
|
raise ValueError("Invalid history file")
|
|
137
|
-
if history[1]["role"] != "user":
|
|
138
|
-
raise ValueError("Invalid history file, second message should be user")
|
|
139
145
|
first_message = ""
|
|
140
146
|
waiting_for_assistant = history[-1]["role"] != "assistant"
|
|
141
147
|
|
|
142
148
|
my_dir = os.path.dirname(__file__)
|
|
143
|
-
config_file = os.path.join(my_dir, "..", "..", "config.toml")
|
|
149
|
+
config_file = os.path.join(my_dir, "..", "..", "..", "config.toml")
|
|
144
150
|
with open(config_file) as f:
|
|
145
151
|
config_json = toml.load(f)
|
|
146
152
|
config = Config.model_validate(config_json)
|
|
@@ -168,11 +174,26 @@ def loop(
|
|
|
168
174
|
openai.pydantic_function_tool(
|
|
169
175
|
BashInteraction,
|
|
170
176
|
description="""
|
|
171
|
-
- Interact with running program using this tool
|
|
177
|
+
- Interact with running program using this tool
|
|
178
|
+
- Special keys like arrows, interrupts, enter, etc.
|
|
179
|
+
- Send text input to the running program.
|
|
180
|
+
- Only one of send_text, send_specials, send_ascii should be provided.""",
|
|
181
|
+
),
|
|
182
|
+
openai.pydantic_function_tool(
|
|
183
|
+
CreateFileNew,
|
|
184
|
+
description="""
|
|
185
|
+
- Write content to a new file. Provide file path and content. Use this instead of BashCommand for writing new files.
|
|
186
|
+
- This doesn't create any directories, please create directories using `mkdir -p` BashCommand.
|
|
187
|
+
- Provide absolute file path only.
|
|
188
|
+
- For editing existing files, use FullFileEdit.""",
|
|
172
189
|
),
|
|
173
190
|
openai.pydantic_function_tool(
|
|
174
|
-
|
|
175
|
-
description="
|
|
191
|
+
FullFileEdit,
|
|
192
|
+
description="""
|
|
193
|
+
- Use absolute file path only.
|
|
194
|
+
- Use ONLY SEARCH/REPLACE blocks to edit the file.
|
|
195
|
+
- file_edit_using_searh_replace_blocks should start with <<<<<<< SEARCH
|
|
196
|
+
""",
|
|
176
197
|
),
|
|
177
198
|
openai.pydantic_function_tool(
|
|
178
199
|
ReadImage, description="Read an image from the shell."
|
|
@@ -197,8 +218,12 @@ Instructions:
|
|
|
197
218
|
System information:
|
|
198
219
|
- System: {uname_sysname}
|
|
199
220
|
- Machine: {uname_machine}
|
|
221
|
+
|
|
200
222
|
"""
|
|
201
223
|
|
|
224
|
+
with open(os.path.join(os.path.dirname(__file__), "diff-instructions.txt")) as f:
|
|
225
|
+
system += f.read()
|
|
226
|
+
|
|
202
227
|
if not history:
|
|
203
228
|
history = [{"role": "system", "content": system}]
|
|
204
229
|
else:
|
|
@@ -210,11 +235,14 @@ System information:
|
|
|
210
235
|
cost: float = 0
|
|
211
236
|
input_toks = 0
|
|
212
237
|
output_toks = 0
|
|
213
|
-
system_console = rich.console.Console(style="blue", highlight=False)
|
|
214
|
-
error_console = rich.console.Console(style="red", highlight=False)
|
|
215
|
-
user_console = rich.console.Console(
|
|
216
|
-
|
|
217
|
-
|
|
238
|
+
system_console = rich.console.Console(style="blue", highlight=False, markup=False)
|
|
239
|
+
error_console = rich.console.Console(style="red", highlight=False, markup=False)
|
|
240
|
+
user_console = rich.console.Console(
|
|
241
|
+
style="bright_black", highlight=False, markup=False
|
|
242
|
+
)
|
|
243
|
+
assistant_console = rich.console.Console(
|
|
244
|
+
style="white bold", highlight=False, markup=False
|
|
245
|
+
)
|
|
218
246
|
while True:
|
|
219
247
|
if cost > limit:
|
|
220
248
|
system_console.print(
|