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.
Files changed (146) hide show
  1. patch/__init__.py +20 -0
  2. patch/__main__.py +4 -0
  3. patch/_version.py +24 -0
  4. patch/analytics.py +260 -0
  5. patch/args.py +955 -0
  6. patch/args_formatter.py +228 -0
  7. patch/coders/__init__.py +32 -0
  8. patch/coders/architect_coder.py +47 -0
  9. patch/coders/architect_prompts.py +40 -0
  10. patch/coders/ask_coder.py +9 -0
  11. patch/coders/ask_prompts.py +41 -0
  12. patch/coders/base_coder.py +2548 -0
  13. patch/coders/base_prompts.py +60 -0
  14. patch/coders/chat_chunks.py +64 -0
  15. patch/coders/context_coder.py +53 -0
  16. patch/coders/context_prompts.py +75 -0
  17. patch/coders/edit_apply.py +6 -0
  18. patch/coders/editblock_coder.py +620 -0
  19. patch/coders/editblock_fenced_coder.py +10 -0
  20. patch/coders/editblock_fenced_prompts.py +143 -0
  21. patch/coders/editblock_func_coder.py +141 -0
  22. patch/coders/editblock_func_prompts.py +27 -0
  23. patch/coders/editblock_prompts.py +172 -0
  24. patch/coders/editor_diff_fenced_coder.py +9 -0
  25. patch/coders/editor_diff_fenced_prompts.py +11 -0
  26. patch/coders/editor_editblock_coder.py +8 -0
  27. patch/coders/editor_editblock_prompts.py +18 -0
  28. patch/coders/editor_whole_coder.py +8 -0
  29. patch/coders/editor_whole_prompts.py +10 -0
  30. patch/coders/patch_coder.py +731 -0
  31. patch/coders/patch_prompts.py +159 -0
  32. patch/coders/search_replace.py +757 -0
  33. patch/coders/shell.py +37 -0
  34. patch/coders/single_wholefile_func_coder.py +102 -0
  35. patch/coders/single_wholefile_func_prompts.py +27 -0
  36. patch/coders/udiff_coder.py +419 -0
  37. patch/coders/udiff_prompts.py +113 -0
  38. patch/coders/udiff_simple.py +14 -0
  39. patch/coders/udiff_simple_prompts.py +25 -0
  40. patch/coders/wholefile_coder.py +144 -0
  41. patch/coders/wholefile_func_coder.py +134 -0
  42. patch/coders/wholefile_func_prompts.py +27 -0
  43. patch/coders/wholefile_prompts.py +64 -0
  44. patch/commands.py +1668 -0
  45. patch/copypaste.py +70 -0
  46. patch/deprecated.py +126 -0
  47. patch/diffs.py +128 -0
  48. patch/dump.py +29 -0
  49. patch/editor.py +147 -0
  50. patch/exceptions.py +113 -0
  51. patch/format_settings.py +47 -0
  52. patch/gui.py +545 -0
  53. patch/history.py +143 -0
  54. patch/io.py +1209 -0
  55. patch/linter.py +304 -0
  56. patch/llm.py +47 -0
  57. patch/main.py +1466 -0
  58. patch/mdstream.py +243 -0
  59. patch/models.py +1342 -0
  60. patch/onboarding.py +428 -0
  61. patch/openrouter.py +128 -0
  62. patch/prompts.py +61 -0
  63. patch/queries/tree-sitter-language-pack/README.md +7 -0
  64. patch/queries/tree-sitter-language-pack/arduino-tags.scm +5 -0
  65. patch/queries/tree-sitter-language-pack/bash-tags.scm +8 -0
  66. patch/queries/tree-sitter-language-pack/c-tags.scm +9 -0
  67. patch/queries/tree-sitter-language-pack/chatito-tags.scm +16 -0
  68. patch/queries/tree-sitter-language-pack/clojure-tags.scm +7 -0
  69. patch/queries/tree-sitter-language-pack/commonlisp-tags.scm +122 -0
  70. patch/queries/tree-sitter-language-pack/cpp-tags.scm +15 -0
  71. patch/queries/tree-sitter-language-pack/csharp-tags.scm +26 -0
  72. patch/queries/tree-sitter-language-pack/d-tags.scm +26 -0
  73. patch/queries/tree-sitter-language-pack/dart-tags.scm +92 -0
  74. patch/queries/tree-sitter-language-pack/elisp-tags.scm +5 -0
  75. patch/queries/tree-sitter-language-pack/elixir-tags.scm +54 -0
  76. patch/queries/tree-sitter-language-pack/elm-tags.scm +19 -0
  77. patch/queries/tree-sitter-language-pack/gleam-tags.scm +41 -0
  78. patch/queries/tree-sitter-language-pack/go-tags.scm +42 -0
  79. patch/queries/tree-sitter-language-pack/java-tags.scm +20 -0
  80. patch/queries/tree-sitter-language-pack/javascript-tags.scm +88 -0
  81. patch/queries/tree-sitter-language-pack/lua-tags.scm +34 -0
  82. patch/queries/tree-sitter-language-pack/matlab-tags.scm +10 -0
  83. patch/queries/tree-sitter-language-pack/ocaml-tags.scm +115 -0
  84. patch/queries/tree-sitter-language-pack/ocaml_interface-tags.scm +98 -0
  85. patch/queries/tree-sitter-language-pack/pony-tags.scm +39 -0
  86. patch/queries/tree-sitter-language-pack/properties-tags.scm +5 -0
  87. patch/queries/tree-sitter-language-pack/python-tags.scm +14 -0
  88. patch/queries/tree-sitter-language-pack/r-tags.scm +21 -0
  89. patch/queries/tree-sitter-language-pack/racket-tags.scm +12 -0
  90. patch/queries/tree-sitter-language-pack/ruby-tags.scm +64 -0
  91. patch/queries/tree-sitter-language-pack/rust-tags.scm +60 -0
  92. patch/queries/tree-sitter-language-pack/solidity-tags.scm +43 -0
  93. patch/queries/tree-sitter-language-pack/swift-tags.scm +51 -0
  94. patch/queries/tree-sitter-language-pack/udev-tags.scm +20 -0
  95. patch/queries/tree-sitter-languages/README.md +24 -0
  96. patch/queries/tree-sitter-languages/bash-tags.scm +8 -0
  97. patch/queries/tree-sitter-languages/c-tags.scm +9 -0
  98. patch/queries/tree-sitter-languages/c_sharp-tags.scm +46 -0
  99. patch/queries/tree-sitter-languages/cpp-tags.scm +15 -0
  100. patch/queries/tree-sitter-languages/dart-tags.scm +91 -0
  101. patch/queries/tree-sitter-languages/elisp-tags.scm +8 -0
  102. patch/queries/tree-sitter-languages/elixir-tags.scm +54 -0
  103. patch/queries/tree-sitter-languages/elm-tags.scm +19 -0
  104. patch/queries/tree-sitter-languages/fortran-tags.scm +15 -0
  105. patch/queries/tree-sitter-languages/go-tags.scm +30 -0
  106. patch/queries/tree-sitter-languages/haskell-tags.scm +3 -0
  107. patch/queries/tree-sitter-languages/hcl-tags.scm +77 -0
  108. patch/queries/tree-sitter-languages/java-tags.scm +20 -0
  109. patch/queries/tree-sitter-languages/javascript-tags.scm +88 -0
  110. patch/queries/tree-sitter-languages/julia-tags.scm +60 -0
  111. patch/queries/tree-sitter-languages/kotlin-tags.scm +27 -0
  112. patch/queries/tree-sitter-languages/matlab-tags.scm +10 -0
  113. patch/queries/tree-sitter-languages/ocaml-tags.scm +115 -0
  114. patch/queries/tree-sitter-languages/ocaml_interface-tags.scm +98 -0
  115. patch/queries/tree-sitter-languages/php-tags.scm +26 -0
  116. patch/queries/tree-sitter-languages/python-tags.scm +12 -0
  117. patch/queries/tree-sitter-languages/ql-tags.scm +26 -0
  118. patch/queries/tree-sitter-languages/ruby-tags.scm +64 -0
  119. patch/queries/tree-sitter-languages/rust-tags.scm +60 -0
  120. patch/queries/tree-sitter-languages/scala-tags.scm +65 -0
  121. patch/queries/tree-sitter-languages/typescript-tags.scm +41 -0
  122. patch/queries/tree-sitter-languages/zig-tags.scm +3 -0
  123. patch/reasoning_tags.py +82 -0
  124. patch/repo.py +612 -0
  125. patch/repomap.py +859 -0
  126. patch/report.py +200 -0
  127. patch/resources/__init__.py +3 -0
  128. patch/resources/model-metadata.json +715 -0
  129. patch/resources/model-settings.yml +3128 -0
  130. patch/run_cmd.py +146 -0
  131. patch/scrape.py +316 -0
  132. patch/sendchat.py +61 -0
  133. patch/special.py +203 -0
  134. patch/urls.py +17 -0
  135. patch/utils.py +365 -0
  136. patch/versioncheck.py +113 -0
  137. patch/voice.py +200 -0
  138. patch/waiting.py +222 -0
  139. patch/watch.py +318 -0
  140. patch/watch_prompts.py +12 -0
  141. patch_chat-0.1.0.dist-info/METADATA +375 -0
  142. patch_chat-0.1.0.dist-info/RECORD +146 -0
  143. patch_chat-0.1.0.dist-info/WHEEL +5 -0
  144. patch_chat-0.1.0.dist-info/entry_points.txt +2 -0
  145. patch_chat-0.1.0.dist-info/licenses/LICENSE.txt +202 -0
  146. 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()