patch-chat 0.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.
- patch/__init__.py +20 -0
- patch/__main__.py +4 -0
- patch/_version.py +24 -0
- patch/analytics.py +260 -0
- patch/args.py +955 -0
- patch/args_formatter.py +228 -0
- patch/coders/__init__.py +32 -0
- patch/coders/architect_coder.py +47 -0
- patch/coders/architect_prompts.py +40 -0
- patch/coders/ask_coder.py +9 -0
- patch/coders/ask_prompts.py +41 -0
- patch/coders/base_coder.py +2548 -0
- patch/coders/base_prompts.py +60 -0
- patch/coders/chat_chunks.py +64 -0
- patch/coders/context_coder.py +53 -0
- patch/coders/context_prompts.py +75 -0
- patch/coders/edit_apply.py +6 -0
- patch/coders/editblock_coder.py +620 -0
- patch/coders/editblock_fenced_coder.py +10 -0
- patch/coders/editblock_fenced_prompts.py +143 -0
- patch/coders/editblock_func_coder.py +141 -0
- patch/coders/editblock_func_prompts.py +27 -0
- patch/coders/editblock_prompts.py +172 -0
- patch/coders/editor_diff_fenced_coder.py +9 -0
- patch/coders/editor_diff_fenced_prompts.py +11 -0
- patch/coders/editor_editblock_coder.py +8 -0
- patch/coders/editor_editblock_prompts.py +18 -0
- patch/coders/editor_whole_coder.py +8 -0
- patch/coders/editor_whole_prompts.py +10 -0
- patch/coders/patch_coder.py +731 -0
- patch/coders/patch_prompts.py +159 -0
- patch/coders/search_replace.py +757 -0
- patch/coders/shell.py +37 -0
- patch/coders/single_wholefile_func_coder.py +102 -0
- patch/coders/single_wholefile_func_prompts.py +27 -0
- patch/coders/udiff_coder.py +419 -0
- patch/coders/udiff_prompts.py +113 -0
- patch/coders/udiff_simple.py +14 -0
- patch/coders/udiff_simple_prompts.py +25 -0
- patch/coders/wholefile_coder.py +144 -0
- patch/coders/wholefile_func_coder.py +134 -0
- patch/coders/wholefile_func_prompts.py +27 -0
- patch/coders/wholefile_prompts.py +64 -0
- patch/commands.py +1668 -0
- patch/copypaste.py +70 -0
- patch/deprecated.py +126 -0
- patch/diffs.py +128 -0
- patch/dump.py +29 -0
- patch/editor.py +147 -0
- patch/exceptions.py +113 -0
- patch/format_settings.py +47 -0
- patch/gui.py +545 -0
- patch/history.py +143 -0
- patch/io.py +1209 -0
- patch/linter.py +304 -0
- patch/llm.py +47 -0
- patch/main.py +1466 -0
- patch/mdstream.py +243 -0
- patch/models.py +1342 -0
- patch/onboarding.py +428 -0
- patch/openrouter.py +128 -0
- patch/prompts.py +61 -0
- patch/queries/tree-sitter-language-pack/README.md +7 -0
- patch/queries/tree-sitter-language-pack/arduino-tags.scm +5 -0
- patch/queries/tree-sitter-language-pack/bash-tags.scm +8 -0
- patch/queries/tree-sitter-language-pack/c-tags.scm +9 -0
- patch/queries/tree-sitter-language-pack/chatito-tags.scm +16 -0
- patch/queries/tree-sitter-language-pack/clojure-tags.scm +7 -0
- patch/queries/tree-sitter-language-pack/commonlisp-tags.scm +122 -0
- patch/queries/tree-sitter-language-pack/cpp-tags.scm +15 -0
- patch/queries/tree-sitter-language-pack/csharp-tags.scm +26 -0
- patch/queries/tree-sitter-language-pack/d-tags.scm +26 -0
- patch/queries/tree-sitter-language-pack/dart-tags.scm +92 -0
- patch/queries/tree-sitter-language-pack/elisp-tags.scm +5 -0
- patch/queries/tree-sitter-language-pack/elixir-tags.scm +54 -0
- patch/queries/tree-sitter-language-pack/elm-tags.scm +19 -0
- patch/queries/tree-sitter-language-pack/gleam-tags.scm +41 -0
- patch/queries/tree-sitter-language-pack/go-tags.scm +42 -0
- patch/queries/tree-sitter-language-pack/java-tags.scm +20 -0
- patch/queries/tree-sitter-language-pack/javascript-tags.scm +88 -0
- patch/queries/tree-sitter-language-pack/lua-tags.scm +34 -0
- patch/queries/tree-sitter-language-pack/matlab-tags.scm +10 -0
- patch/queries/tree-sitter-language-pack/ocaml-tags.scm +115 -0
- patch/queries/tree-sitter-language-pack/ocaml_interface-tags.scm +98 -0
- patch/queries/tree-sitter-language-pack/pony-tags.scm +39 -0
- patch/queries/tree-sitter-language-pack/properties-tags.scm +5 -0
- patch/queries/tree-sitter-language-pack/python-tags.scm +14 -0
- patch/queries/tree-sitter-language-pack/r-tags.scm +21 -0
- patch/queries/tree-sitter-language-pack/racket-tags.scm +12 -0
- patch/queries/tree-sitter-language-pack/ruby-tags.scm +64 -0
- patch/queries/tree-sitter-language-pack/rust-tags.scm +60 -0
- patch/queries/tree-sitter-language-pack/solidity-tags.scm +43 -0
- patch/queries/tree-sitter-language-pack/swift-tags.scm +51 -0
- patch/queries/tree-sitter-language-pack/udev-tags.scm +20 -0
- patch/queries/tree-sitter-languages/README.md +24 -0
- patch/queries/tree-sitter-languages/bash-tags.scm +8 -0
- patch/queries/tree-sitter-languages/c-tags.scm +9 -0
- patch/queries/tree-sitter-languages/c_sharp-tags.scm +46 -0
- patch/queries/tree-sitter-languages/cpp-tags.scm +15 -0
- patch/queries/tree-sitter-languages/dart-tags.scm +91 -0
- patch/queries/tree-sitter-languages/elisp-tags.scm +8 -0
- patch/queries/tree-sitter-languages/elixir-tags.scm +54 -0
- patch/queries/tree-sitter-languages/elm-tags.scm +19 -0
- patch/queries/tree-sitter-languages/fortran-tags.scm +15 -0
- patch/queries/tree-sitter-languages/go-tags.scm +30 -0
- patch/queries/tree-sitter-languages/haskell-tags.scm +3 -0
- patch/queries/tree-sitter-languages/hcl-tags.scm +77 -0
- patch/queries/tree-sitter-languages/java-tags.scm +20 -0
- patch/queries/tree-sitter-languages/javascript-tags.scm +88 -0
- patch/queries/tree-sitter-languages/julia-tags.scm +60 -0
- patch/queries/tree-sitter-languages/kotlin-tags.scm +27 -0
- patch/queries/tree-sitter-languages/matlab-tags.scm +10 -0
- patch/queries/tree-sitter-languages/ocaml-tags.scm +115 -0
- patch/queries/tree-sitter-languages/ocaml_interface-tags.scm +98 -0
- patch/queries/tree-sitter-languages/php-tags.scm +26 -0
- patch/queries/tree-sitter-languages/python-tags.scm +12 -0
- patch/queries/tree-sitter-languages/ql-tags.scm +26 -0
- patch/queries/tree-sitter-languages/ruby-tags.scm +64 -0
- patch/queries/tree-sitter-languages/rust-tags.scm +60 -0
- patch/queries/tree-sitter-languages/scala-tags.scm +65 -0
- patch/queries/tree-sitter-languages/typescript-tags.scm +41 -0
- patch/queries/tree-sitter-languages/zig-tags.scm +3 -0
- patch/reasoning_tags.py +82 -0
- patch/repo.py +612 -0
- patch/repomap.py +859 -0
- patch/report.py +200 -0
- patch/resources/__init__.py +3 -0
- patch/resources/model-metadata.json +715 -0
- patch/resources/model-settings.yml +3128 -0
- patch/run_cmd.py +146 -0
- patch/scrape.py +316 -0
- patch/sendchat.py +61 -0
- patch/special.py +203 -0
- patch/urls.py +17 -0
- patch/utils.py +365 -0
- patch/versioncheck.py +113 -0
- patch/voice.py +200 -0
- patch/waiting.py +222 -0
- patch/watch.py +318 -0
- patch/watch_prompts.py +12 -0
- patch_chat-0.1.0.dist-info/METADATA +375 -0
- patch_chat-0.1.0.dist-info/RECORD +146 -0
- patch_chat-0.1.0.dist-info/WHEEL +5 -0
- patch_chat-0.1.0.dist-info/entry_points.txt +2 -0
- patch_chat-0.1.0.dist-info/licenses/LICENSE.txt +202 -0
- patch_chat-0.1.0.dist-info/top_level.txt +1 -0
patch/gui.py
ADDED
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import random
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
import streamlit as st
|
|
8
|
+
|
|
9
|
+
from patch import urls
|
|
10
|
+
from patch.coders import Coder
|
|
11
|
+
from patch.dump import dump # noqa: F401
|
|
12
|
+
from patch.io import InputOutput
|
|
13
|
+
from patch.main import main as cli_main
|
|
14
|
+
from patch.scrape import Scraper, has_playwright
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CaptureIO(InputOutput):
|
|
18
|
+
lines = []
|
|
19
|
+
|
|
20
|
+
def tool_output(self, msg, log_only=False):
|
|
21
|
+
if not log_only:
|
|
22
|
+
self.lines.append(msg)
|
|
23
|
+
super().tool_output(msg, log_only=log_only)
|
|
24
|
+
|
|
25
|
+
def tool_error(self, msg):
|
|
26
|
+
self.lines.append(msg)
|
|
27
|
+
super().tool_error(msg)
|
|
28
|
+
|
|
29
|
+
def tool_warning(self, msg):
|
|
30
|
+
self.lines.append(msg)
|
|
31
|
+
super().tool_warning(msg)
|
|
32
|
+
|
|
33
|
+
def get_captured_lines(self):
|
|
34
|
+
lines = self.lines
|
|
35
|
+
self.lines = []
|
|
36
|
+
return lines
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def search(text=None):
|
|
40
|
+
results = []
|
|
41
|
+
for root, _, files in os.walk("patch"):
|
|
42
|
+
for file in files:
|
|
43
|
+
path = os.path.join(root, file)
|
|
44
|
+
if not text or text in path:
|
|
45
|
+
results.append(path)
|
|
46
|
+
# dump(results)
|
|
47
|
+
|
|
48
|
+
return results
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# Keep state as a resource, which survives browser reloads (since Coder does too)
|
|
52
|
+
class State:
|
|
53
|
+
keys = set()
|
|
54
|
+
|
|
55
|
+
def init(self, key, val=None):
|
|
56
|
+
if key in self.keys:
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
self.keys.add(key)
|
|
60
|
+
setattr(self, key, val)
|
|
61
|
+
return True
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@st.cache_resource
|
|
65
|
+
def get_state():
|
|
66
|
+
return State()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@st.cache_resource
|
|
70
|
+
def get_coder():
|
|
71
|
+
coder = cli_main(return_coder=True)
|
|
72
|
+
if not isinstance(coder, Coder):
|
|
73
|
+
raise ValueError(coder)
|
|
74
|
+
if not coder.repo:
|
|
75
|
+
raise ValueError("GUI can currently only be used inside a git repo")
|
|
76
|
+
|
|
77
|
+
io = CaptureIO(
|
|
78
|
+
pretty=False,
|
|
79
|
+
yes=True,
|
|
80
|
+
dry_run=coder.io.dry_run,
|
|
81
|
+
encoding=coder.io.encoding,
|
|
82
|
+
)
|
|
83
|
+
# coder.io = io # this breaks the input_history
|
|
84
|
+
coder.commands.io = io
|
|
85
|
+
|
|
86
|
+
for line in coder.get_announcements():
|
|
87
|
+
coder.io.tool_output(line)
|
|
88
|
+
|
|
89
|
+
return coder
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class GUI:
|
|
93
|
+
prompt = None
|
|
94
|
+
prompt_as = "user"
|
|
95
|
+
last_undo_empty = None
|
|
96
|
+
recent_msgs_empty = None
|
|
97
|
+
web_content_empty = None
|
|
98
|
+
|
|
99
|
+
def announce(self):
|
|
100
|
+
lines = self.coder.get_announcements()
|
|
101
|
+
lines = " \n".join(lines)
|
|
102
|
+
return lines
|
|
103
|
+
|
|
104
|
+
def show_edit_info(self, edit):
|
|
105
|
+
commit_hash = edit.get("commit_hash")
|
|
106
|
+
commit_message = edit.get("commit_message")
|
|
107
|
+
diff = edit.get("diff")
|
|
108
|
+
fnames = edit.get("fnames")
|
|
109
|
+
if fnames:
|
|
110
|
+
fnames = sorted(fnames)
|
|
111
|
+
|
|
112
|
+
if not commit_hash and not fnames:
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
show_undo = False
|
|
116
|
+
res = ""
|
|
117
|
+
if commit_hash:
|
|
118
|
+
res += f"Commit `{commit_hash}`: {commit_message} \n"
|
|
119
|
+
if commit_hash == self.coder.last_patch_commit_hash:
|
|
120
|
+
show_undo = True
|
|
121
|
+
|
|
122
|
+
if fnames:
|
|
123
|
+
fnames = [f"`{fname}`" for fname in fnames]
|
|
124
|
+
fnames = ", ".join(fnames)
|
|
125
|
+
res += f"Applied edits to {fnames}."
|
|
126
|
+
|
|
127
|
+
if diff:
|
|
128
|
+
with st.expander(res):
|
|
129
|
+
st.code(diff, language="diff")
|
|
130
|
+
if show_undo:
|
|
131
|
+
self.add_undo(commit_hash)
|
|
132
|
+
else:
|
|
133
|
+
with st.container(border=True):
|
|
134
|
+
st.write(res)
|
|
135
|
+
if show_undo:
|
|
136
|
+
self.add_undo(commit_hash)
|
|
137
|
+
|
|
138
|
+
def add_undo(self, commit_hash):
|
|
139
|
+
if self.last_undo_empty:
|
|
140
|
+
self.last_undo_empty.empty()
|
|
141
|
+
|
|
142
|
+
self.last_undo_empty = st.empty()
|
|
143
|
+
undone = self.state.last_undone_commit_hash == commit_hash
|
|
144
|
+
if not undone:
|
|
145
|
+
with self.last_undo_empty:
|
|
146
|
+
if self.button(f"Undo commit `{commit_hash}`", key=f"undo_{commit_hash}"):
|
|
147
|
+
self.do_undo(commit_hash)
|
|
148
|
+
|
|
149
|
+
def do_sidebar(self):
|
|
150
|
+
with st.sidebar:
|
|
151
|
+
st.title("Patch")
|
|
152
|
+
# self.cmds_tab, self.settings_tab = st.tabs(["Commands", "Settings"])
|
|
153
|
+
|
|
154
|
+
# self.do_recommended_actions()
|
|
155
|
+
self.do_add_to_chat()
|
|
156
|
+
self.do_recent_msgs()
|
|
157
|
+
self.do_clear_chat_history()
|
|
158
|
+
# st.container(height=150, border=False)
|
|
159
|
+
# st.write("### Experimental")
|
|
160
|
+
|
|
161
|
+
st.warning(
|
|
162
|
+
"This browser version of Patch is experimental. Please share feedback in [GitHub"
|
|
163
|
+
" issues](https://github.com/PierrunoYT/patch/issues)."
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
def do_settings_tab(self):
|
|
167
|
+
pass
|
|
168
|
+
|
|
169
|
+
def do_recommended_actions(self):
|
|
170
|
+
text = "Patch works best when your code is stored in a git repo. \n"
|
|
171
|
+
text += f"[See the repository documentation for more info]({urls.git})"
|
|
172
|
+
|
|
173
|
+
with st.expander("Recommended actions", expanded=True):
|
|
174
|
+
with st.popover("Create a git repo to track changes"):
|
|
175
|
+
st.write(text)
|
|
176
|
+
self.button("Create git repo", key=random.random(), help="?")
|
|
177
|
+
|
|
178
|
+
with st.popover("Update your `.gitignore` file"):
|
|
179
|
+
st.write("It's best to keep Patch's internal files out of your git repo.")
|
|
180
|
+
self.button("Add `.patch*` to `.gitignore`", key=random.random(), help="?")
|
|
181
|
+
|
|
182
|
+
def do_add_to_chat(self):
|
|
183
|
+
# with st.expander("Add to the chat", expanded=True):
|
|
184
|
+
self.do_add_files()
|
|
185
|
+
self.do_add_web_page()
|
|
186
|
+
|
|
187
|
+
def do_add_files(self):
|
|
188
|
+
fnames = st.multiselect(
|
|
189
|
+
"Add files to the chat",
|
|
190
|
+
self.coder.get_all_relative_files(),
|
|
191
|
+
default=self.state.initial_inchat_files,
|
|
192
|
+
placeholder="Files to edit",
|
|
193
|
+
disabled=self.prompt_pending(),
|
|
194
|
+
help=(
|
|
195
|
+
"Only add the files that need to be *edited* for the task you are working"
|
|
196
|
+
" on. Patch will pull in other relevant code to provide context to the LLM."
|
|
197
|
+
),
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
for fname in fnames:
|
|
201
|
+
if fname not in self.coder.get_inchat_relative_files():
|
|
202
|
+
self.coder.add_rel_fname(fname)
|
|
203
|
+
self.info(f"Added {fname} to the chat")
|
|
204
|
+
|
|
205
|
+
for fname in self.coder.get_inchat_relative_files():
|
|
206
|
+
if fname not in fnames:
|
|
207
|
+
self.coder.drop_rel_fname(fname)
|
|
208
|
+
self.info(f"Removed {fname} from the chat")
|
|
209
|
+
|
|
210
|
+
def do_add_web_page(self):
|
|
211
|
+
with st.popover("Add a web page to the chat"):
|
|
212
|
+
self.do_web()
|
|
213
|
+
|
|
214
|
+
def do_add_image(self):
|
|
215
|
+
with st.popover("Add image"):
|
|
216
|
+
st.markdown("Hello World 👋")
|
|
217
|
+
st.file_uploader("Image file", disabled=self.prompt_pending())
|
|
218
|
+
|
|
219
|
+
def do_run_shell(self):
|
|
220
|
+
with st.popover("Run shell commands, tests, etc"):
|
|
221
|
+
st.markdown(
|
|
222
|
+
"Run a shell command and optionally share the output with the LLM. This is"
|
|
223
|
+
" a great way to run your program or run tests and have the LLM fix bugs."
|
|
224
|
+
)
|
|
225
|
+
st.text_input("Command:")
|
|
226
|
+
st.radio(
|
|
227
|
+
"Share the command output with the LLM?",
|
|
228
|
+
[
|
|
229
|
+
"Review the output and decide whether to share",
|
|
230
|
+
"Automatically share the output on non-zero exit code (ie, if any tests fail)",
|
|
231
|
+
],
|
|
232
|
+
)
|
|
233
|
+
st.selectbox(
|
|
234
|
+
"Recent commands",
|
|
235
|
+
[
|
|
236
|
+
"my_app.py --doit",
|
|
237
|
+
"my_app.py --cleanup",
|
|
238
|
+
],
|
|
239
|
+
disabled=self.prompt_pending(),
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
def do_tokens_and_cost(self):
|
|
243
|
+
with st.expander("Tokens and costs", expanded=True):
|
|
244
|
+
pass
|
|
245
|
+
|
|
246
|
+
def do_show_token_usage(self):
|
|
247
|
+
with st.popover("Show token usage"):
|
|
248
|
+
st.write("hi")
|
|
249
|
+
|
|
250
|
+
def do_clear_chat_history(self):
|
|
251
|
+
text = "Saves tokens, reduces confusion"
|
|
252
|
+
if self.button("Clear chat history", help=text):
|
|
253
|
+
self.coder.done_messages = []
|
|
254
|
+
self.coder.cur_messages = []
|
|
255
|
+
self.info("Cleared chat history. Now the LLM can't see anything before this line.")
|
|
256
|
+
|
|
257
|
+
def do_show_metrics(self):
|
|
258
|
+
st.metric("Cost of last message send & reply", "$0.0019", help="foo")
|
|
259
|
+
st.metric("Cost to send next message", "$0.0013", help="foo")
|
|
260
|
+
st.metric("Total cost this session", "$0.22")
|
|
261
|
+
|
|
262
|
+
def do_git(self):
|
|
263
|
+
with st.expander("Git", expanded=False):
|
|
264
|
+
# st.button("Show last diff")
|
|
265
|
+
# st.button("Undo last commit")
|
|
266
|
+
self.button("Commit any pending changes")
|
|
267
|
+
with st.popover("Run git command"):
|
|
268
|
+
st.markdown("## Run git command")
|
|
269
|
+
st.text_input("git", value="git ")
|
|
270
|
+
self.button("Run")
|
|
271
|
+
st.selectbox(
|
|
272
|
+
"Recent git commands",
|
|
273
|
+
[
|
|
274
|
+
"git checkout -b experiment",
|
|
275
|
+
"git stash",
|
|
276
|
+
],
|
|
277
|
+
disabled=self.prompt_pending(),
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
def do_recent_msgs(self):
|
|
281
|
+
if not self.recent_msgs_empty:
|
|
282
|
+
self.recent_msgs_empty = st.empty()
|
|
283
|
+
|
|
284
|
+
if self.prompt_pending():
|
|
285
|
+
self.recent_msgs_empty.empty()
|
|
286
|
+
self.state.recent_msgs_num += 1
|
|
287
|
+
|
|
288
|
+
with self.recent_msgs_empty:
|
|
289
|
+
self.old_prompt = st.selectbox(
|
|
290
|
+
"Resend a recent chat message",
|
|
291
|
+
self.state.input_history,
|
|
292
|
+
placeholder="Choose a recent chat message",
|
|
293
|
+
# label_visibility="collapsed",
|
|
294
|
+
index=None,
|
|
295
|
+
key=f"recent_msgs_{self.state.recent_msgs_num}",
|
|
296
|
+
disabled=self.prompt_pending(),
|
|
297
|
+
)
|
|
298
|
+
if self.old_prompt:
|
|
299
|
+
self.prompt = self.old_prompt
|
|
300
|
+
|
|
301
|
+
def do_messages_container(self):
|
|
302
|
+
self.messages = st.container()
|
|
303
|
+
|
|
304
|
+
# stuff a bunch of vertical whitespace at the top
|
|
305
|
+
# to get all the chat text to the bottom
|
|
306
|
+
# self.messages.container(height=300, border=False)
|
|
307
|
+
|
|
308
|
+
with self.messages:
|
|
309
|
+
for msg in self.state.messages:
|
|
310
|
+
role = msg["role"]
|
|
311
|
+
|
|
312
|
+
if role == "edit":
|
|
313
|
+
self.show_edit_info(msg)
|
|
314
|
+
elif role == "info":
|
|
315
|
+
st.info(msg["content"])
|
|
316
|
+
elif role == "text":
|
|
317
|
+
text = msg["content"]
|
|
318
|
+
line = text.splitlines()[0]
|
|
319
|
+
with self.messages.expander(line):
|
|
320
|
+
st.text(text)
|
|
321
|
+
elif role in ("user", "assistant"):
|
|
322
|
+
with st.chat_message(role):
|
|
323
|
+
st.write(msg["content"])
|
|
324
|
+
# self.cost()
|
|
325
|
+
else:
|
|
326
|
+
st.dict(msg)
|
|
327
|
+
|
|
328
|
+
def initialize_state(self):
|
|
329
|
+
messages = [
|
|
330
|
+
dict(role="info", content=self.announce()),
|
|
331
|
+
dict(role="assistant", content="How can I help you?"),
|
|
332
|
+
]
|
|
333
|
+
|
|
334
|
+
self.state.init("messages", messages)
|
|
335
|
+
self.state.init("last_patch_commit_hash", self.coder.last_patch_commit_hash)
|
|
336
|
+
self.state.init("last_undone_commit_hash")
|
|
337
|
+
self.state.init("recent_msgs_num", 0)
|
|
338
|
+
self.state.init("web_content_num", 0)
|
|
339
|
+
self.state.init("prompt")
|
|
340
|
+
self.state.init("scraper")
|
|
341
|
+
|
|
342
|
+
self.state.init("initial_inchat_files", self.coder.get_inchat_relative_files())
|
|
343
|
+
|
|
344
|
+
if "input_history" not in self.state.keys:
|
|
345
|
+
input_history = list(self.coder.io.get_input_history())
|
|
346
|
+
seen = set()
|
|
347
|
+
input_history = [x for x in input_history if not (x in seen or seen.add(x))]
|
|
348
|
+
self.state.input_history = input_history
|
|
349
|
+
self.state.keys.add("input_history")
|
|
350
|
+
|
|
351
|
+
def button(self, args, **kwargs):
|
|
352
|
+
"Create a button, disabled if prompt pending"
|
|
353
|
+
|
|
354
|
+
# Force everything to be disabled if there is a prompt pending
|
|
355
|
+
if self.prompt_pending():
|
|
356
|
+
kwargs["disabled"] = True
|
|
357
|
+
|
|
358
|
+
return st.button(args, **kwargs)
|
|
359
|
+
|
|
360
|
+
def __init__(self):
|
|
361
|
+
self.coder = get_coder()
|
|
362
|
+
self.state = get_state()
|
|
363
|
+
|
|
364
|
+
# Force the coder to cooperate, regardless of cmd line args
|
|
365
|
+
self.coder.yield_stream = True
|
|
366
|
+
self.coder.stream = True
|
|
367
|
+
self.coder.pretty = False
|
|
368
|
+
|
|
369
|
+
self.initialize_state()
|
|
370
|
+
|
|
371
|
+
self.do_messages_container()
|
|
372
|
+
self.do_sidebar()
|
|
373
|
+
|
|
374
|
+
user_inp = st.chat_input("Say something")
|
|
375
|
+
if user_inp:
|
|
376
|
+
self.prompt = user_inp
|
|
377
|
+
|
|
378
|
+
if self.prompt_pending():
|
|
379
|
+
self.process_chat()
|
|
380
|
+
|
|
381
|
+
if not self.prompt:
|
|
382
|
+
return
|
|
383
|
+
|
|
384
|
+
self.state.prompt = self.prompt
|
|
385
|
+
|
|
386
|
+
if self.prompt_as == "user":
|
|
387
|
+
self.coder.io.add_to_input_history(self.prompt)
|
|
388
|
+
|
|
389
|
+
self.state.input_history.append(self.prompt)
|
|
390
|
+
|
|
391
|
+
if self.prompt_as:
|
|
392
|
+
self.state.messages.append({"role": self.prompt_as, "content": self.prompt})
|
|
393
|
+
if self.prompt_as == "user":
|
|
394
|
+
with self.messages.chat_message("user"):
|
|
395
|
+
st.write(self.prompt)
|
|
396
|
+
elif self.prompt_as == "text":
|
|
397
|
+
line = self.prompt.splitlines()[0]
|
|
398
|
+
line += "??"
|
|
399
|
+
with self.messages.expander(line):
|
|
400
|
+
st.text(self.prompt)
|
|
401
|
+
|
|
402
|
+
# re-render the UI for the prompt_pending state
|
|
403
|
+
st.rerun()
|
|
404
|
+
|
|
405
|
+
def prompt_pending(self):
|
|
406
|
+
return self.state.prompt is not None
|
|
407
|
+
|
|
408
|
+
def cost(self):
|
|
409
|
+
cost = random.random() * 0.003 + 0.001
|
|
410
|
+
st.caption(f"${cost:0.4f}")
|
|
411
|
+
|
|
412
|
+
def process_chat(self):
|
|
413
|
+
prompt = self.state.prompt
|
|
414
|
+
self.state.prompt = None
|
|
415
|
+
|
|
416
|
+
# This duplicates logic from within Coder
|
|
417
|
+
self.num_reflections = 0
|
|
418
|
+
self.max_reflections = 3
|
|
419
|
+
|
|
420
|
+
while prompt:
|
|
421
|
+
with self.messages.chat_message("assistant"):
|
|
422
|
+
res = st.write_stream(self.coder.run_stream(prompt))
|
|
423
|
+
self.state.messages.append({"role": "assistant", "content": res})
|
|
424
|
+
# self.cost()
|
|
425
|
+
|
|
426
|
+
prompt = None
|
|
427
|
+
if self.coder.reflected_message:
|
|
428
|
+
if self.num_reflections < self.max_reflections:
|
|
429
|
+
self.num_reflections += 1
|
|
430
|
+
self.info(self.coder.reflected_message)
|
|
431
|
+
prompt = self.coder.reflected_message
|
|
432
|
+
|
|
433
|
+
with self.messages:
|
|
434
|
+
edit = dict(
|
|
435
|
+
role="edit",
|
|
436
|
+
fnames=self.coder.patch_edited_files,
|
|
437
|
+
)
|
|
438
|
+
if self.state.last_patch_commit_hash != self.coder.last_patch_commit_hash:
|
|
439
|
+
edit["commit_hash"] = self.coder.last_patch_commit_hash
|
|
440
|
+
edit["commit_message"] = self.coder.last_patch_commit_message
|
|
441
|
+
commits = f"{self.coder.last_patch_commit_hash}~1"
|
|
442
|
+
diff = self.coder.repo.diff_commits(
|
|
443
|
+
self.coder.pretty,
|
|
444
|
+
commits,
|
|
445
|
+
self.coder.last_patch_commit_hash,
|
|
446
|
+
)
|
|
447
|
+
edit["diff"] = diff
|
|
448
|
+
self.state.last_patch_commit_hash = self.coder.last_patch_commit_hash
|
|
449
|
+
|
|
450
|
+
self.state.messages.append(edit)
|
|
451
|
+
self.show_edit_info(edit)
|
|
452
|
+
|
|
453
|
+
# re-render the UI for the non-prompt_pending state
|
|
454
|
+
st.rerun()
|
|
455
|
+
|
|
456
|
+
def info(self, message, echo=True):
|
|
457
|
+
info = dict(role="info", content=message)
|
|
458
|
+
self.state.messages.append(info)
|
|
459
|
+
|
|
460
|
+
# We will render the tail of the messages array after this call
|
|
461
|
+
if echo:
|
|
462
|
+
self.messages.info(message)
|
|
463
|
+
|
|
464
|
+
def do_web(self):
|
|
465
|
+
st.markdown("Add the text content of a web page to the chat")
|
|
466
|
+
|
|
467
|
+
if not self.web_content_empty:
|
|
468
|
+
self.web_content_empty = st.empty()
|
|
469
|
+
|
|
470
|
+
if self.prompt_pending():
|
|
471
|
+
self.web_content_empty.empty()
|
|
472
|
+
self.state.web_content_num += 1
|
|
473
|
+
|
|
474
|
+
with self.web_content_empty:
|
|
475
|
+
self.web_content = st.text_input(
|
|
476
|
+
"URL",
|
|
477
|
+
placeholder="https://...",
|
|
478
|
+
key=f"web_content_{self.state.web_content_num}",
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
if not self.web_content:
|
|
482
|
+
return
|
|
483
|
+
|
|
484
|
+
url = self.web_content
|
|
485
|
+
|
|
486
|
+
if not self.state.scraper:
|
|
487
|
+
self.scraper = Scraper(print_error=self.info, playwright_available=has_playwright())
|
|
488
|
+
|
|
489
|
+
content = self.scraper.scrape(url) or ""
|
|
490
|
+
if content.strip():
|
|
491
|
+
content = f"{url}\n\n" + content
|
|
492
|
+
self.prompt = content
|
|
493
|
+
self.prompt_as = "text"
|
|
494
|
+
else:
|
|
495
|
+
self.info(f"No web content found for `{url}`.")
|
|
496
|
+
self.web_content = None
|
|
497
|
+
|
|
498
|
+
def do_undo(self, commit_hash):
|
|
499
|
+
self.last_undo_empty.empty()
|
|
500
|
+
|
|
501
|
+
if (
|
|
502
|
+
self.state.last_patch_commit_hash != commit_hash
|
|
503
|
+
or self.coder.last_patch_commit_hash != commit_hash
|
|
504
|
+
):
|
|
505
|
+
self.info(f"Commit `{commit_hash}` is not the latest commit.")
|
|
506
|
+
return
|
|
507
|
+
|
|
508
|
+
self.coder.commands.io.get_captured_lines()
|
|
509
|
+
reply = self.coder.commands.cmd_undo(None)
|
|
510
|
+
lines = self.coder.commands.io.get_captured_lines()
|
|
511
|
+
|
|
512
|
+
lines = "\n".join(lines)
|
|
513
|
+
lines = lines.splitlines()
|
|
514
|
+
lines = " \n".join(lines)
|
|
515
|
+
self.info(lines, echo=False)
|
|
516
|
+
|
|
517
|
+
self.state.last_undone_commit_hash = commit_hash
|
|
518
|
+
|
|
519
|
+
if reply:
|
|
520
|
+
self.prompt_as = None
|
|
521
|
+
self.prompt = reply
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
def gui_main():
|
|
525
|
+
st.set_page_config(
|
|
526
|
+
layout="wide",
|
|
527
|
+
page_title="Patch",
|
|
528
|
+
page_icon="🩹",
|
|
529
|
+
menu_items={
|
|
530
|
+
"Get Help": urls.documentation,
|
|
531
|
+
"Report a bug": "https://github.com/PierrunoYT/patch/issues",
|
|
532
|
+
"About": "# Patch\nAI pair programming in your browser.",
|
|
533
|
+
},
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
# config_options = st.config._config_options
|
|
537
|
+
# for key, value in config_options.items():
|
|
538
|
+
# print(f"{key}: {value.value}")
|
|
539
|
+
|
|
540
|
+
GUI()
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
if __name__ == "__main__":
|
|
544
|
+
status = gui_main()
|
|
545
|
+
sys.exit(status)
|
patch/history.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
|
|
3
|
+
from patch import models, prompts
|
|
4
|
+
from patch.dump import dump # noqa: F401
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ChatSummary:
|
|
8
|
+
def __init__(self, models=None, max_tokens=1024):
|
|
9
|
+
if not models:
|
|
10
|
+
raise ValueError("At least one model must be provided")
|
|
11
|
+
self.models = models if isinstance(models, list) else [models]
|
|
12
|
+
self.max_tokens = max_tokens
|
|
13
|
+
self.token_count = self.models[0].token_count
|
|
14
|
+
|
|
15
|
+
def too_big(self, messages):
|
|
16
|
+
sized = self.tokenize(messages)
|
|
17
|
+
total = sum(tokens for tokens, _msg in sized)
|
|
18
|
+
return total > self.max_tokens
|
|
19
|
+
|
|
20
|
+
def tokenize(self, messages):
|
|
21
|
+
sized = []
|
|
22
|
+
for msg in messages:
|
|
23
|
+
tokens = self.token_count(msg)
|
|
24
|
+
sized.append((tokens, msg))
|
|
25
|
+
return sized
|
|
26
|
+
|
|
27
|
+
def summarize(self, messages, depth=0):
|
|
28
|
+
messages = self.summarize_real(messages)
|
|
29
|
+
if messages and messages[-1]["role"] != "assistant":
|
|
30
|
+
messages.append(dict(role="assistant", content="Ok."))
|
|
31
|
+
return messages
|
|
32
|
+
|
|
33
|
+
def summarize_real(self, messages, depth=0):
|
|
34
|
+
if not self.models:
|
|
35
|
+
raise ValueError("No models available for summarization")
|
|
36
|
+
|
|
37
|
+
sized = self.tokenize(messages)
|
|
38
|
+
total = sum(tokens for tokens, _msg in sized)
|
|
39
|
+
if total <= self.max_tokens and depth == 0:
|
|
40
|
+
return messages
|
|
41
|
+
|
|
42
|
+
min_split = 4
|
|
43
|
+
if len(messages) <= min_split or depth > 3:
|
|
44
|
+
return self.summarize_all(messages)
|
|
45
|
+
|
|
46
|
+
tail_tokens = 0
|
|
47
|
+
split_index = len(messages)
|
|
48
|
+
half_max_tokens = self.max_tokens // 2
|
|
49
|
+
|
|
50
|
+
# Iterate over the messages in reverse order
|
|
51
|
+
for i in range(len(sized) - 1, -1, -1):
|
|
52
|
+
tokens, _msg = sized[i]
|
|
53
|
+
if tail_tokens + tokens < half_max_tokens:
|
|
54
|
+
tail_tokens += tokens
|
|
55
|
+
split_index = i
|
|
56
|
+
else:
|
|
57
|
+
break
|
|
58
|
+
|
|
59
|
+
# Ensure the head ends with an assistant message
|
|
60
|
+
while messages[split_index - 1]["role"] != "assistant" and split_index > 1:
|
|
61
|
+
split_index -= 1
|
|
62
|
+
|
|
63
|
+
if split_index <= min_split:
|
|
64
|
+
return self.summarize_all(messages)
|
|
65
|
+
|
|
66
|
+
# Split head and tail
|
|
67
|
+
tail = messages[split_index:]
|
|
68
|
+
|
|
69
|
+
# Only size the head once
|
|
70
|
+
sized_head = sized[:split_index]
|
|
71
|
+
|
|
72
|
+
# Precompute token limit (fallback to 4096 if undefined)
|
|
73
|
+
model_max_input_tokens = self.models[0].info.get("max_input_tokens") or 4096
|
|
74
|
+
model_max_input_tokens -= 512 # reserve buffer for safety
|
|
75
|
+
|
|
76
|
+
keep = []
|
|
77
|
+
total = 0
|
|
78
|
+
|
|
79
|
+
# Iterate in original order, summing tokens until limit
|
|
80
|
+
for tokens, msg in sized_head:
|
|
81
|
+
total += tokens
|
|
82
|
+
if total > model_max_input_tokens:
|
|
83
|
+
break
|
|
84
|
+
keep.append(msg)
|
|
85
|
+
# No need to reverse lists back and forth
|
|
86
|
+
|
|
87
|
+
summary = self.summarize_all(keep)
|
|
88
|
+
|
|
89
|
+
# If the combined summary and tail still fits, return directly
|
|
90
|
+
summary_tokens = self.token_count(summary)
|
|
91
|
+
tail_tokens = sum(tokens for tokens, _ in sized[split_index:])
|
|
92
|
+
if summary_tokens + tail_tokens < self.max_tokens:
|
|
93
|
+
return summary + tail
|
|
94
|
+
|
|
95
|
+
# Otherwise recurse with increased depth
|
|
96
|
+
return self.summarize_real(summary + tail, depth + 1)
|
|
97
|
+
|
|
98
|
+
def summarize_all(self, messages):
|
|
99
|
+
content = ""
|
|
100
|
+
for msg in messages:
|
|
101
|
+
role = msg["role"].upper()
|
|
102
|
+
if role not in ("USER", "ASSISTANT"):
|
|
103
|
+
continue
|
|
104
|
+
content += f"# {role}\n"
|
|
105
|
+
content += msg["content"]
|
|
106
|
+
if not content.endswith("\n"):
|
|
107
|
+
content += "\n"
|
|
108
|
+
|
|
109
|
+
summarize_messages = [
|
|
110
|
+
dict(role="system", content=prompts.summarize),
|
|
111
|
+
dict(role="user", content=content),
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
for model in self.models:
|
|
115
|
+
try:
|
|
116
|
+
summary = model.simple_send_with_retries(summarize_messages)
|
|
117
|
+
if summary is not None:
|
|
118
|
+
summary = prompts.summary_prefix + summary
|
|
119
|
+
return [dict(role="user", content=summary)]
|
|
120
|
+
except Exception as e:
|
|
121
|
+
print(f"Summarization failed for model {model.name}: {str(e)}")
|
|
122
|
+
|
|
123
|
+
raise ValueError("summarizer unexpectedly failed for all models")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def main():
|
|
127
|
+
parser = argparse.ArgumentParser()
|
|
128
|
+
parser.add_argument("filename", help="Markdown file to parse")
|
|
129
|
+
args = parser.parse_args()
|
|
130
|
+
|
|
131
|
+
model_names = ["gpt-3.5-turbo", "gpt-4"] # Add more model names as needed
|
|
132
|
+
model_list = [models.Model(name) for name in model_names]
|
|
133
|
+
summarizer = ChatSummary(model_list)
|
|
134
|
+
|
|
135
|
+
with open(args.filename, "r") as f:
|
|
136
|
+
text = f.read()
|
|
137
|
+
|
|
138
|
+
summary = summarizer.summarize_chat_history_markdown(text)
|
|
139
|
+
dump(summary)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
if __name__ == "__main__":
|
|
143
|
+
main()
|