patch-code 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 (153) hide show
  1. patch/__init__.py +3 -0
  2. patch/__main__.py +4 -0
  3. patch/analytics.py +268 -0
  4. patch/args.py +949 -0
  5. patch/args_formatter.py +227 -0
  6. patch/coders/__init__.py +34 -0
  7. patch/coders/architect_coder.py +48 -0
  8. patch/coders/architect_prompts.py +40 -0
  9. patch/coders/ask_coder.py +9 -0
  10. patch/coders/ask_prompts.py +41 -0
  11. patch/coders/base_coder.py +2485 -0
  12. patch/coders/base_prompts.py +60 -0
  13. patch/coders/chat_chunks.py +64 -0
  14. patch/coders/context_coder.py +53 -0
  15. patch/coders/context_prompts.py +75 -0
  16. patch/coders/editblock_coder.py +657 -0
  17. patch/coders/editblock_fenced_coder.py +10 -0
  18. patch/coders/editblock_fenced_prompts.py +143 -0
  19. patch/coders/editblock_func_coder.py +141 -0
  20. patch/coders/editblock_func_prompts.py +27 -0
  21. patch/coders/editblock_prompts.py +172 -0
  22. patch/coders/editor_diff_fenced_coder.py +9 -0
  23. patch/coders/editor_diff_fenced_prompts.py +11 -0
  24. patch/coders/editor_editblock_coder.py +8 -0
  25. patch/coders/editor_editblock_prompts.py +18 -0
  26. patch/coders/editor_whole_coder.py +8 -0
  27. patch/coders/editor_whole_prompts.py +10 -0
  28. patch/coders/help_coder.py +16 -0
  29. patch/coders/help_prompts.py +46 -0
  30. patch/coders/patch_coder.py +706 -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 +429 -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 +1712 -0
  45. patch/copypaste.py +72 -0
  46. patch/deprecated.py +126 -0
  47. patch/diffs.py +128 -0
  48. patch/docs/__init__.py +1 -0
  49. patch/docs/analytics.md +28 -0
  50. patch/docs/config.md +43 -0
  51. patch/docs/git.md +22 -0
  52. patch/docs/install.md +54 -0
  53. patch/docs/models.md +59 -0
  54. patch/docs/troubleshooting.md +23 -0
  55. patch/docs/usage.md +47 -0
  56. patch/dump.py +29 -0
  57. patch/editor.py +147 -0
  58. patch/exceptions.py +113 -0
  59. patch/format_settings.py +26 -0
  60. patch/gui.py +545 -0
  61. patch/help.py +118 -0
  62. patch/history.py +143 -0
  63. patch/io.py +1191 -0
  64. patch/linter.py +304 -0
  65. patch/llm.py +47 -0
  66. patch/main.py +1274 -0
  67. patch/mdstream.py +243 -0
  68. patch/models.py +1338 -0
  69. patch/onboarding.py +428 -0
  70. patch/openrouter.py +128 -0
  71. patch/prompts.py +61 -0
  72. patch/queries/tree-sitter-language-pack/arduino-tags.scm +5 -0
  73. patch/queries/tree-sitter-language-pack/bash-tags.scm +8 -0
  74. patch/queries/tree-sitter-language-pack/c-tags.scm +9 -0
  75. patch/queries/tree-sitter-language-pack/chatito-tags.scm +16 -0
  76. patch/queries/tree-sitter-language-pack/clojure-tags.scm +7 -0
  77. patch/queries/tree-sitter-language-pack/commonlisp-tags.scm +122 -0
  78. patch/queries/tree-sitter-language-pack/cpp-tags.scm +15 -0
  79. patch/queries/tree-sitter-language-pack/csharp-tags.scm +26 -0
  80. patch/queries/tree-sitter-language-pack/d-tags.scm +26 -0
  81. patch/queries/tree-sitter-language-pack/dart-tags.scm +92 -0
  82. patch/queries/tree-sitter-language-pack/elisp-tags.scm +5 -0
  83. patch/queries/tree-sitter-language-pack/elixir-tags.scm +54 -0
  84. patch/queries/tree-sitter-language-pack/elm-tags.scm +19 -0
  85. patch/queries/tree-sitter-language-pack/gleam-tags.scm +41 -0
  86. patch/queries/tree-sitter-language-pack/go-tags.scm +42 -0
  87. patch/queries/tree-sitter-language-pack/java-tags.scm +20 -0
  88. patch/queries/tree-sitter-language-pack/javascript-tags.scm +88 -0
  89. patch/queries/tree-sitter-language-pack/lua-tags.scm +34 -0
  90. patch/queries/tree-sitter-language-pack/matlab-tags.scm +10 -0
  91. patch/queries/tree-sitter-language-pack/ocaml-tags.scm +115 -0
  92. patch/queries/tree-sitter-language-pack/ocaml_interface-tags.scm +98 -0
  93. patch/queries/tree-sitter-language-pack/pony-tags.scm +39 -0
  94. patch/queries/tree-sitter-language-pack/properties-tags.scm +5 -0
  95. patch/queries/tree-sitter-language-pack/python-tags.scm +14 -0
  96. patch/queries/tree-sitter-language-pack/r-tags.scm +21 -0
  97. patch/queries/tree-sitter-language-pack/racket-tags.scm +12 -0
  98. patch/queries/tree-sitter-language-pack/ruby-tags.scm +64 -0
  99. patch/queries/tree-sitter-language-pack/rust-tags.scm +60 -0
  100. patch/queries/tree-sitter-language-pack/solidity-tags.scm +43 -0
  101. patch/queries/tree-sitter-language-pack/swift-tags.scm +51 -0
  102. patch/queries/tree-sitter-language-pack/udev-tags.scm +20 -0
  103. patch/queries/tree-sitter-languages/bash-tags.scm +8 -0
  104. patch/queries/tree-sitter-languages/c-tags.scm +9 -0
  105. patch/queries/tree-sitter-languages/c_sharp-tags.scm +46 -0
  106. patch/queries/tree-sitter-languages/cpp-tags.scm +15 -0
  107. patch/queries/tree-sitter-languages/dart-tags.scm +91 -0
  108. patch/queries/tree-sitter-languages/elisp-tags.scm +8 -0
  109. patch/queries/tree-sitter-languages/elixir-tags.scm +54 -0
  110. patch/queries/tree-sitter-languages/elm-tags.scm +19 -0
  111. patch/queries/tree-sitter-languages/fortran-tags.scm +15 -0
  112. patch/queries/tree-sitter-languages/go-tags.scm +30 -0
  113. patch/queries/tree-sitter-languages/haskell-tags.scm +3 -0
  114. patch/queries/tree-sitter-languages/hcl-tags.scm +77 -0
  115. patch/queries/tree-sitter-languages/java-tags.scm +20 -0
  116. patch/queries/tree-sitter-languages/javascript-tags.scm +88 -0
  117. patch/queries/tree-sitter-languages/julia-tags.scm +60 -0
  118. patch/queries/tree-sitter-languages/kotlin-tags.scm +27 -0
  119. patch/queries/tree-sitter-languages/matlab-tags.scm +10 -0
  120. patch/queries/tree-sitter-languages/ocaml-tags.scm +115 -0
  121. patch/queries/tree-sitter-languages/ocaml_interface-tags.scm +98 -0
  122. patch/queries/tree-sitter-languages/php-tags.scm +26 -0
  123. patch/queries/tree-sitter-languages/python-tags.scm +12 -0
  124. patch/queries/tree-sitter-languages/ql-tags.scm +26 -0
  125. patch/queries/tree-sitter-languages/ruby-tags.scm +64 -0
  126. patch/queries/tree-sitter-languages/rust-tags.scm +60 -0
  127. patch/queries/tree-sitter-languages/scala-tags.scm +65 -0
  128. patch/queries/tree-sitter-languages/typescript-tags.scm +41 -0
  129. patch/queries/tree-sitter-languages/zig-tags.scm +3 -0
  130. patch/reasoning_tags.py +82 -0
  131. patch/repo.py +622 -0
  132. patch/repomap.py +867 -0
  133. patch/report.py +200 -0
  134. patch/resources/__init__.py +3 -0
  135. patch/resources/model-metadata.json +715 -0
  136. patch/resources/model-settings.yml +3128 -0
  137. patch/run_cmd.py +132 -0
  138. patch/scrape.py +284 -0
  139. patch/sendchat.py +61 -0
  140. patch/special.py +203 -0
  141. patch/urls.py +17 -0
  142. patch/utils.py +348 -0
  143. patch/versioncheck.py +130 -0
  144. patch/voice.py +187 -0
  145. patch/waiting.py +221 -0
  146. patch/watch.py +318 -0
  147. patch/watch_prompts.py +12 -0
  148. patch_code-0.1.0.dist-info/METADATA +467 -0
  149. patch_code-0.1.0.dist-info/RECORD +153 -0
  150. patch_code-0.1.0.dist-info/WHEEL +5 -0
  151. patch_code-0.1.0.dist-info/entry_points.txt +2 -0
  152. patch_code-0.1.0.dist-info/licenses/LICENSE.txt +202 -0
  153. patch_code-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 FAQ 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.website,
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/help.py ADDED
@@ -0,0 +1,118 @@
1
+ #!/usr/bin/env python
2
+
3
+ import json
4
+ import os
5
+ import shutil
6
+ import warnings
7
+ from pathlib import Path
8
+
9
+ import importlib_resources
10
+
11
+ from patch import __version__, urls, utils
12
+ from patch.dump import dump # noqa: F401
13
+
14
+ warnings.simplefilter("ignore", category=FutureWarning)
15
+
16
+
17
+ def install_help_extra(io):
18
+ pip_install_cmd = [
19
+ "patch-code[help]",
20
+ "--extra-index-url",
21
+ "https://download.pytorch.org/whl/cpu",
22
+ ]
23
+ res = utils.check_pip_install_extra(
24
+ io,
25
+ "llama_index.embeddings.huggingface",
26
+ "To use interactive /help you need to install the help extras",
27
+ pip_install_cmd,
28
+ )
29
+ return res
30
+
31
+
32
+ def get_package_files():
33
+ for path in importlib_resources.files("patch.docs").iterdir():
34
+ if path.is_file() and path.name.endswith(".md"):
35
+ yield path
36
+
37
+
38
+ def fname_to_url(filepath):
39
+ name = Path(str(filepath).replace("\\", "/")).name
40
+ if name not in {path.name for path in get_package_files()}:
41
+ return ""
42
+ return urls.docs + name
43
+
44
+
45
+ def get_index():
46
+ from llama_index.core import (
47
+ Document,
48
+ StorageContext,
49
+ VectorStoreIndex,
50
+ load_index_from_storage,
51
+ )
52
+ from llama_index.core.node_parser import MarkdownNodeParser
53
+
54
+ dname = Path.home() / ".patch" / "caches" / ("help.docs." + __version__)
55
+
56
+ index = None
57
+ try:
58
+ if dname.exists():
59
+ storage_context = StorageContext.from_defaults(
60
+ persist_dir=dname,
61
+ )
62
+ index = load_index_from_storage(storage_context)
63
+ except (OSError, json.JSONDecodeError):
64
+ shutil.rmtree(dname)
65
+
66
+ if index is None:
67
+ parser = MarkdownNodeParser()
68
+
69
+ nodes = []
70
+ for fname in get_package_files():
71
+ doc = Document(
72
+ text=fname.read_text(encoding="utf-8"),
73
+ metadata=dict(
74
+ filename=fname.name,
75
+ extension=".md",
76
+ url=fname_to_url(str(fname)),
77
+ ),
78
+ )
79
+ nodes += parser.get_nodes_from_documents([doc])
80
+
81
+ index = VectorStoreIndex(nodes, show_progress=True)
82
+ dname.parent.mkdir(parents=True, exist_ok=True)
83
+ index.storage_context.persist(dname)
84
+
85
+ return index
86
+
87
+
88
+ class Help:
89
+ def __init__(self):
90
+ from llama_index.core import Settings
91
+ from llama_index.embeddings.huggingface import HuggingFaceEmbedding
92
+
93
+ os.environ["TOKENIZERS_PARALLELISM"] = "true"
94
+ Settings.embed_model = HuggingFaceEmbedding(model_name="BAAI/bge-small-en-v1.5")
95
+
96
+ index = get_index()
97
+
98
+ self.retriever = index.as_retriever(similarity_top_k=20)
99
+
100
+ def ask(self, question):
101
+ nodes = self.retriever.retrieve(question)
102
+
103
+ context = f"""# Question: {question}
104
+
105
+ # Relevant docs:
106
+
107
+ """ # noqa: E231
108
+
109
+ for node in nodes:
110
+ url = node.metadata.get("url", "")
111
+ if url:
112
+ url = f' from_url="{url}"'
113
+
114
+ context += f"<doc{url}>\n"
115
+ context += node.text
116
+ context += "\n</doc>\n\n"
117
+
118
+ return context