patchllm 0.1.0__py3-none-any.whl → 0.2.1__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.
- patchllm/context.py +79 -16
- patchllm/listener.py +6 -4
- patchllm/main.py +247 -191
- patchllm/parser.py +32 -19
- patchllm-0.2.1.dist-info/METADATA +127 -0
- patchllm-0.2.1.dist-info/RECORD +12 -0
- patchllm-0.2.1.dist-info/entry_points.txt +2 -0
- patchllm-0.1.0.dist-info/METADATA +0 -83
- patchllm-0.1.0.dist-info/RECORD +0 -12
- patchllm-0.1.0.dist-info/entry_points.txt +0 -2
- {patchllm-0.1.0.dist-info → patchllm-0.2.1.dist-info}/WHEEL +0 -0
- {patchllm-0.1.0.dist-info → patchllm-0.2.1.dist-info}/licenses/LICENSE +0 -0
- {patchllm-0.1.0.dist-info → patchllm-0.2.1.dist-info}/top_level.txt +0 -0
patchllm/context.py
CHANGED
@@ -1,8 +1,11 @@
|
|
1
|
-
import os
|
2
1
|
import glob
|
3
2
|
import textwrap
|
4
|
-
import
|
3
|
+
import subprocess
|
4
|
+
import shutil
|
5
5
|
from pathlib import Path
|
6
|
+
from rich.console import Console
|
7
|
+
|
8
|
+
console = Console()
|
6
9
|
|
7
10
|
# --- Default Settings & Templates ---
|
8
11
|
|
@@ -29,12 +32,19 @@ BASE_TEMPLATE = textwrap.dedent('''
|
|
29
32
|
```
|
30
33
|
{{source_tree}}
|
31
34
|
```
|
32
|
-
|
35
|
+
{{url_contents}}
|
33
36
|
Relevant Files:
|
34
37
|
---------------
|
35
38
|
{{files_content}}
|
36
39
|
''')
|
37
40
|
|
41
|
+
URL_CONTENT_TEMPLATE = textwrap.dedent('''
|
42
|
+
URL Contents:
|
43
|
+
-------------
|
44
|
+
{{content}}
|
45
|
+
''')
|
46
|
+
|
47
|
+
|
38
48
|
# --- Helper Functions (File Discovery, Filtering, Tree Generation) ---
|
39
49
|
|
40
50
|
def find_files(base_path: Path, include_patterns: list[str], exclude_patterns: list[str] | None = None) -> list[Path]:
|
@@ -70,11 +80,10 @@ def filter_files_by_keyword(file_paths: list[Path], search_words: list[str]) ->
|
|
70
80
|
matching_files = []
|
71
81
|
for file_path in file_paths:
|
72
82
|
try:
|
73
|
-
# Using pathlib's read_text for cleaner code
|
74
83
|
if any(word in file_path.read_text(encoding='utf-8', errors='ignore') for word in search_words):
|
75
84
|
matching_files.append(file_path)
|
76
85
|
except Exception as e:
|
77
|
-
print(f"
|
86
|
+
console.print(f"⚠️ Could not read {file_path} for keyword search: {e}", style="yellow")
|
78
87
|
return matching_files
|
79
88
|
|
80
89
|
|
@@ -86,11 +95,8 @@ def generate_source_tree(base_path: Path, file_paths: list[Path]) -> str:
|
|
86
95
|
tree = {}
|
87
96
|
for path in file_paths:
|
88
97
|
try:
|
89
|
-
# Create a path relative to the intended base_path for the tree structure
|
90
98
|
rel_path = path.relative_to(base_path)
|
91
99
|
except ValueError:
|
92
|
-
# This occurs if a file (from an absolute pattern) is outside the base_path.
|
93
|
-
# In this case, we use the absolute path as a fallback.
|
94
100
|
rel_path = path
|
95
101
|
|
96
102
|
level = tree
|
@@ -112,6 +118,61 @@ def generate_source_tree(base_path: Path, file_paths: list[Path]) -> str:
|
|
112
118
|
return f"{base_path.name}\n" + "\n".join(_format_tree(tree))
|
113
119
|
|
114
120
|
|
121
|
+
def fetch_and_process_urls(urls: list[str]) -> str:
|
122
|
+
"""Downloads and converts a list of URLs to text, returning a formatted string."""
|
123
|
+
if not urls:
|
124
|
+
return ""
|
125
|
+
|
126
|
+
try:
|
127
|
+
import html2text
|
128
|
+
except ImportError:
|
129
|
+
console.print("⚠️ To use the URL feature, please install the required extras:", style="yellow")
|
130
|
+
console.print(" pip install patchllm[url]", style="cyan")
|
131
|
+
return ""
|
132
|
+
|
133
|
+
downloader = None
|
134
|
+
if shutil.which("curl"):
|
135
|
+
downloader = "curl"
|
136
|
+
elif shutil.which("wget"):
|
137
|
+
downloader = "wget"
|
138
|
+
|
139
|
+
if not downloader:
|
140
|
+
console.print("⚠️ Cannot fetch URL content: 'curl' or 'wget' not found in PATH.", style="yellow")
|
141
|
+
return ""
|
142
|
+
|
143
|
+
h = html2text.HTML2Text()
|
144
|
+
h.ignore_links = True
|
145
|
+
h.ignore_images = True
|
146
|
+
|
147
|
+
all_url_contents = []
|
148
|
+
|
149
|
+
console.print("\n--- Fetching URL Content... ---", style="bold")
|
150
|
+
for url in urls:
|
151
|
+
try:
|
152
|
+
console.print(f"Fetching [cyan]{url}[/cyan]...")
|
153
|
+
if downloader == "curl":
|
154
|
+
command = ["curl", "-s", "-L", url]
|
155
|
+
else: # wget
|
156
|
+
command = ["wget", "-q", "-O", "-", url]
|
157
|
+
|
158
|
+
result = subprocess.run(command, capture_output=True, text=True, check=True, timeout=15)
|
159
|
+
html_content = result.stdout
|
160
|
+
text_content = h.handle(html_content)
|
161
|
+
all_url_contents.append(f"<url_content:{url}>\n```\n{text_content}\n```")
|
162
|
+
|
163
|
+
except subprocess.CalledProcessError as e:
|
164
|
+
console.print(f"❌ Failed to fetch {url}: {e.stderr}", style="red")
|
165
|
+
except subprocess.TimeoutExpired:
|
166
|
+
console.print(f"❌ Failed to fetch {url}: Request timed out.", style="red")
|
167
|
+
except Exception as e:
|
168
|
+
console.print(f"❌ An unexpected error occurred while fetching {url}: {e}", style="red")
|
169
|
+
|
170
|
+
if not all_url_contents:
|
171
|
+
return ""
|
172
|
+
|
173
|
+
content_str = "\n\n".join(all_url_contents)
|
174
|
+
return URL_CONTENT_TEMPLATE.replace("{{content}}", content_str)
|
175
|
+
|
115
176
|
# --- Main Context Building Function ---
|
116
177
|
|
117
178
|
def build_context(config: dict) -> dict | None:
|
@@ -124,13 +185,13 @@ def build_context(config: dict) -> dict | None:
|
|
124
185
|
Returns:
|
125
186
|
dict: A dictionary with the source tree and formatted context, or None.
|
126
187
|
"""
|
127
|
-
# Resolve the base path immediately to get a predictable absolute path.
|
128
188
|
base_path = Path(config.get("path", ".")).resolve()
|
129
189
|
|
130
190
|
include_patterns = config.get("include_patterns", [])
|
131
191
|
exclude_patterns = config.get("exclude_patterns", [])
|
132
192
|
exclude_extensions = config.get("exclude_extensions", DEFAULT_EXCLUDE_EXTENSIONS)
|
133
193
|
search_words = config.get("search_words", [])
|
194
|
+
urls = config.get("urls", [])
|
134
195
|
|
135
196
|
# Step 1: Find files
|
136
197
|
relevant_files = find_files(base_path, include_patterns, exclude_patterns)
|
@@ -140,20 +201,18 @@ def build_context(config: dict) -> dict | None:
|
|
140
201
|
norm_ext = {ext.lower() for ext in exclude_extensions}
|
141
202
|
relevant_files = [p for p in relevant_files if p.suffix.lower() not in norm_ext]
|
142
203
|
if count_before_ext > len(relevant_files):
|
143
|
-
print(f"Filtered {count_before_ext - len(relevant_files)} files by extension.")
|
204
|
+
console.print(f"Filtered {count_before_ext - len(relevant_files)} files by extension.", style="cyan")
|
144
205
|
|
145
206
|
# Step 3: Filter by keyword
|
146
207
|
if search_words:
|
147
208
|
count_before_kw = len(relevant_files)
|
148
209
|
relevant_files = filter_files_by_keyword(relevant_files, search_words)
|
149
|
-
print(f"Filtered {count_before_kw - len(relevant_files)} files by keyword search.")
|
210
|
+
console.print(f"Filtered {count_before_kw - len(relevant_files)} files by keyword search.", style="cyan")
|
150
211
|
|
151
|
-
if not relevant_files:
|
152
|
-
print("\
|
212
|
+
if not relevant_files and not urls:
|
213
|
+
console.print("\n⚠️ No files or URLs matched the specified criteria.", style="yellow")
|
153
214
|
return None
|
154
215
|
|
155
|
-
print(f"\nFinal count of relevant files: {len(relevant_files)}.")
|
156
|
-
|
157
216
|
# Generate source tree and file content blocks
|
158
217
|
source_tree_str = generate_source_tree(base_path, relevant_files)
|
159
218
|
|
@@ -164,12 +223,16 @@ def build_context(config: dict) -> dict | None:
|
|
164
223
|
content = file_path.read_text(encoding='utf-8')
|
165
224
|
file_contents.append(f"<file_path:{display_path}>\n```\n{content}\n```")
|
166
225
|
except Exception as e:
|
167
|
-
print(f"
|
226
|
+
console.print(f"⚠️ Could not read file {file_path}: {e}", style="yellow")
|
168
227
|
|
169
228
|
files_content_str = "\n\n".join(file_contents)
|
170
229
|
|
230
|
+
# Fetch and process URL contents
|
231
|
+
url_contents_str = fetch_and_process_urls(urls)
|
232
|
+
|
171
233
|
# Assemble the final context using the base template
|
172
234
|
final_context = BASE_TEMPLATE.replace("{{source_tree}}", source_tree_str)
|
235
|
+
final_context = final_context.replace("{{url_contents}}", url_contents_str)
|
173
236
|
final_context = final_context.replace("{{files_content}}", files_content_str)
|
174
237
|
|
175
238
|
return {"tree": source_tree_str, "context": final_context}
|
patchllm/listener.py
CHANGED
@@ -1,11 +1,13 @@
|
|
1
1
|
import speech_recognition as sr
|
2
2
|
import pyttsx3
|
3
|
+
from rich.console import Console
|
3
4
|
|
5
|
+
console = Console()
|
4
6
|
recognizer = sr.Recognizer()
|
5
7
|
tts_engine = pyttsx3.init()
|
6
8
|
|
7
9
|
def speak(text):
|
8
|
-
print("🤖 Speaking:",
|
10
|
+
console.print(f"🤖 Speaking: {text}", style="magenta")
|
9
11
|
tts_engine.say(text)
|
10
12
|
tts_engine.runAndWait()
|
11
13
|
|
@@ -13,11 +15,11 @@ def listen(prompt=None, timeout=5):
|
|
13
15
|
with sr.Microphone() as source:
|
14
16
|
if prompt:
|
15
17
|
speak(prompt)
|
16
|
-
print("🎙 Listening...")
|
18
|
+
console.print("🎙 Listening...", style="cyan")
|
17
19
|
try:
|
18
20
|
audio = recognizer.listen(source, timeout=timeout)
|
19
21
|
text = recognizer.recognize_google(audio)
|
20
|
-
print(f"🗣 Recognized: {text}")
|
22
|
+
console.print(f"🗣 Recognized: {text}", style="cyan")
|
21
23
|
return text
|
22
24
|
except sr.WaitTimeoutError:
|
23
25
|
speak("No speech detected.")
|
@@ -25,4 +27,4 @@ def listen(prompt=None, timeout=5):
|
|
25
27
|
speak("Sorry, I didn’t catch that.")
|
26
28
|
except sr.RequestError:
|
27
29
|
speak("Speech recognition failed. Check your internet.")
|
28
|
-
return None
|
30
|
+
return None
|
patchllm/main.py
CHANGED
@@ -1,230 +1,286 @@
|
|
1
|
-
import sys
|
2
|
-
|
3
|
-
from context import build_context
|
4
|
-
from parser import paste_response
|
5
|
-
from utils import load_from_py_file
|
6
1
|
import textwrap
|
7
2
|
import argparse
|
8
3
|
import litellm
|
9
|
-
|
4
|
+
import pprint
|
5
|
+
import os
|
10
6
|
from dotenv import load_dotenv
|
7
|
+
from rich.console import Console
|
8
|
+
from rich.panel import Panel
|
9
|
+
|
10
|
+
from .context import build_context
|
11
|
+
from .parser import paste_response
|
12
|
+
from .utils import load_from_py_file
|
11
13
|
|
12
|
-
|
14
|
+
console = Console()
|
13
15
|
|
14
|
-
|
16
|
+
# --- Core Functions ---
|
17
|
+
|
18
|
+
def collect_context(config_name, configs):
|
19
|
+
"""Builds the code context from a provided configuration dictionary."""
|
20
|
+
console.print("\n--- Building Code Context... ---", style="bold")
|
21
|
+
if not configs:
|
22
|
+
raise FileNotFoundError("Could not find a 'configs.py' file.")
|
23
|
+
selected_config = configs.get(config_name)
|
24
|
+
if selected_config is None:
|
25
|
+
raise KeyError(f"Context config '{config_name}' not found in provided configs file.")
|
26
|
+
|
27
|
+
context_object = build_context(selected_config)
|
28
|
+
if context_object:
|
29
|
+
tree, context = context_object.values()
|
30
|
+
console.print("--- Context Building Finished. The following files were extracted ---", style="bold")
|
31
|
+
console.print(tree)
|
32
|
+
return context
|
33
|
+
else:
|
34
|
+
console.print("--- Context Building Failed (No files found) ---", style="yellow")
|
35
|
+
return None
|
36
|
+
|
37
|
+
def run_update(task_instructions, model_name, history, context=None):
|
15
38
|
"""
|
16
|
-
|
39
|
+
Assembles the final prompt, sends it to the LLM, and applies file updates.
|
17
40
|
"""
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
"""
|
29
|
-
self.model_name = model_name
|
30
|
-
self.configs = load_from_py_file(configs_file, "configs")
|
31
|
-
system_prompt = textwrap.dedent("""
|
32
|
-
You are an expert pair programmer. Your purpose is to help users by modifying files based on their instructions.
|
33
|
-
|
34
|
-
Follow these rules strictly:
|
35
|
-
Your output should be a single file including all the updated files. For each file-block:
|
36
|
-
1. Only include code for files that need to be updated / edited.
|
37
|
-
2. For updated files, do not exclude any code even if it is unchanged code; assume the file code will be copy-pasted full in the file.
|
38
|
-
3. Do not include verbose inline comments explaining what every small change does. Try to keep comments concise but informative, if any.
|
39
|
-
4. Only update the relevant parts of each file relative to the provided task; do not make irrelevant edits even if you notice areas of improvements elsewhere.
|
40
|
-
5. Do not use diffs.
|
41
|
-
6. Make sure each file-block is returned in the following exact format. No additional text, comments, or explanations should be outside these blocks.
|
42
|
-
|
43
|
-
Expected format for a modified or new file:
|
44
|
-
<file_path:/absolute/path/to/your/file.py>
|
45
|
-
```python
|
46
|
-
# The full, complete content of /absolute/path/to/your/file.py goes here.
|
47
|
-
def example_function():
|
48
|
-
return "Hello, World!"
|
49
|
-
```
|
50
|
-
|
51
|
-
Example of multiple files:
|
52
|
-
<file_path:/home/user/project/src/main.py>
|
53
|
-
```python
|
54
|
-
print("Main application start")
|
55
|
-
```
|
56
|
-
|
57
|
-
<file_path:/home/user/project/tests/test_main.py>
|
58
|
-
```python
|
59
|
-
def test_main():
|
60
|
-
assert True
|
61
|
-
```
|
62
|
-
""")
|
63
|
-
self.history = [{"role": "system", "content": system_prompt}]
|
64
|
-
|
65
|
-
def collect(self, config_name):
|
66
|
-
"""Builds the code context from a provided configuration dictionary."""
|
67
|
-
print("\n--- Building Code Context... ---")
|
68
|
-
selected_config = self.configs.get(config_name)
|
69
|
-
if selected_config is None:
|
70
|
-
raise KeyError(f"Context config '{config_name}' not found in provided configs file.")
|
71
|
-
context_object = build_context(selected_config)
|
72
|
-
if context_object:
|
73
|
-
tree, context = context_object.values()
|
74
|
-
print("--- Context Building Finished. The following files were extracted ---", file=sys.stderr)
|
75
|
-
print(tree)
|
76
|
-
return context
|
77
|
-
else:
|
78
|
-
print("--- Context Building Failed (No files found) ---", file=sys.stderr)
|
79
|
-
return None
|
80
|
-
|
81
|
-
def update(self, task_instructions, context=None):
|
82
|
-
"""
|
83
|
-
Assembles the final prompt and sends it to the LLM to generate code,
|
84
|
-
then in-place update the files from the response.
|
85
|
-
Args:
|
86
|
-
task_instructions (str): Specific instructions for this run.
|
87
|
-
context (str, optional): The code context. If None, only the task is sent.
|
88
|
-
"""
|
89
|
-
print("\n--- Sending Prompt to LLM... ---")
|
90
|
-
final_prompt = task_instructions
|
91
|
-
if context:
|
92
|
-
final_prompt = f"{context}\n\n{task_instructions}"
|
41
|
+
console.print("\n--- Sending Prompt to LLM... ---", style="bold")
|
42
|
+
final_prompt = task_instructions
|
43
|
+
if context:
|
44
|
+
final_prompt = f"{context}\n\n{task_instructions}"
|
45
|
+
|
46
|
+
history.append({"role": "user", "content": final_prompt})
|
47
|
+
|
48
|
+
try:
|
49
|
+
with console.status("[bold cyan]Waiting for LLM response...", spinner="dots"):
|
50
|
+
response = litellm.completion(model=model_name, messages=history)
|
93
51
|
|
94
|
-
|
52
|
+
assistant_response_content = response.choices[0].message.content
|
53
|
+
history.append({"role": "assistant", "content": assistant_response_content})
|
54
|
+
|
55
|
+
if not assistant_response_content or not assistant_response_content.strip():
|
56
|
+
console.print("⚠️ Response is empty. Nothing to paste.", style="yellow")
|
57
|
+
return
|
95
58
|
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
# Extract the message content from the response
|
100
|
-
assistant_response_content = response.choices[0].message.content
|
101
|
-
|
102
|
-
# Add the assistant's response to the history for future context
|
103
|
-
self.history.append({"role": "assistant", "content": assistant_response_content})
|
104
|
-
|
105
|
-
if not assistant_response_content or not assistant_response_content.strip():
|
106
|
-
print("Response is empty. Nothing to paste.")
|
107
|
-
return
|
108
|
-
|
109
|
-
print("\n--- Updating files ---")
|
110
|
-
paste_response(assistant_response_content)
|
111
|
-
print("--- File Update Process Finished ---")
|
59
|
+
console.print("\n--- Updating files ---", style="bold")
|
60
|
+
paste_response(assistant_response_content)
|
61
|
+
console.print("--- File Update Process Finished ---", style="bold")
|
112
62
|
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
content = file.read()
|
63
|
+
except Exception as e:
|
64
|
+
history.pop() # Keep history clean on error
|
65
|
+
raise RuntimeError(f"An error occurred while communicating with the LLM via litellm: {e}") from e
|
66
|
+
|
67
|
+
def write_context_to_file(file_path, context):
|
68
|
+
"""Utility function to write the context to a file."""
|
69
|
+
console.print("Exporting context..", style="cyan")
|
70
|
+
with open(file_path, "w", encoding="utf-8") as file:
|
71
|
+
file.write(context)
|
72
|
+
console.print(f'✅ Context exported to {file_path.split("/")[-1]}', style="green")
|
73
|
+
|
74
|
+
def read_from_file(file_path):
|
75
|
+
"""Utility function to read and return the content of a file."""
|
76
|
+
console.print(f"Importing from {file_path}..", style="cyan")
|
77
|
+
try:
|
78
|
+
with open(file_path, "r", encoding="utf-8") as file:
|
79
|
+
content = file.read()
|
80
|
+
console.print("✅ Finished reading file.", style="green")
|
132
81
|
return content
|
133
|
-
|
134
|
-
|
82
|
+
except Exception as e:
|
83
|
+
raise RuntimeError(f"Failed to read from file {file_path}: {e}") from e
|
84
|
+
|
85
|
+
def create_new_config(configs, configs_file_str):
|
86
|
+
"""Interactively creates a new configuration and saves it to the specified configs file."""
|
87
|
+
console.print(f"\n--- Creating a new configuration in '{configs_file_str}' ---", style="bold")
|
88
|
+
|
89
|
+
try:
|
90
|
+
name = console.input("[bold]Enter a name for the new configuration: [/]").strip()
|
91
|
+
if not name:
|
92
|
+
console.print("❌ Configuration name cannot be empty.", style="red")
|
93
|
+
return
|
94
|
+
|
95
|
+
if name in configs:
|
96
|
+
overwrite = console.input(f"Configuration '[bold]{name}[/]' already exists. Overwrite? (y/n): ").lower()
|
97
|
+
if overwrite not in ['y', 'yes']:
|
98
|
+
console.print("Operation cancelled.", style="yellow")
|
99
|
+
return
|
100
|
+
|
101
|
+
path = console.input("[bold]Enter the base path[/] (e.g., '.' for current directory): ").strip() or "."
|
135
102
|
|
103
|
+
console.print("\nEnter comma-separated glob patterns for files to include.")
|
104
|
+
include_raw = console.input('[cyan]> (e.g., "[bold]**/*.py, src/**/*.js[/]"): [/]').strip()
|
105
|
+
include_patterns = [p.strip() for p in include_raw.split(',') if p.strip()]
|
106
|
+
|
107
|
+
console.print("\nEnter comma-separated glob patterns for files to exclude (optional).")
|
108
|
+
exclude_raw = console.input('[cyan]> (e.g., "[bold]**/tests/*, venv/*[/]"): [/]').strip()
|
109
|
+
exclude_patterns = [p.strip() for p in exclude_raw.split(',') if p.strip()]
|
110
|
+
|
111
|
+
console.print("\nEnter comma-separated URLs to include as context (optional).")
|
112
|
+
urls_raw = console.input('[cyan]> (e.g., "[bold]https://docs.example.com, ...[/]"): [/]').strip()
|
113
|
+
urls = [u.strip() for u in urls_raw.split(',') if u.strip()]
|
114
|
+
|
115
|
+
new_config_data = {
|
116
|
+
"path": path,
|
117
|
+
"include_patterns": include_patterns,
|
118
|
+
"exclude_patterns": exclude_patterns,
|
119
|
+
"urls": urls,
|
120
|
+
}
|
121
|
+
|
122
|
+
configs[name] = new_config_data
|
123
|
+
|
124
|
+
with open(configs_file_str, "w", encoding="utf-8") as f:
|
125
|
+
f.write("# configs.py\n")
|
126
|
+
f.write("configs = ")
|
127
|
+
f.write(pprint.pformat(configs, indent=4))
|
128
|
+
f.write("\n")
|
129
|
+
|
130
|
+
console.print(f"\n✅ Successfully created and saved configuration '[bold]{name}[/]' in '[bold]{configs_file_str}[/]'.", style="green")
|
131
|
+
|
132
|
+
except KeyboardInterrupt:
|
133
|
+
console.print("\n\n⚠️ Configuration creation cancelled by user.", style="yellow")
|
134
|
+
return
|
135
|
+
|
136
136
|
def main():
|
137
|
+
"""
|
138
|
+
Main entry point for the patchllm command-line tool.
|
139
|
+
"""
|
140
|
+
load_dotenv()
|
141
|
+
|
142
|
+
configs_file_path = os.getenv("PATCHLLM_CONFIGS_FILE", "./configs.py")
|
143
|
+
|
137
144
|
parser = argparse.ArgumentParser(
|
138
|
-
description="
|
139
|
-
|
140
|
-
parser.add_argument(
|
141
|
-
"--config",
|
142
|
-
type=str,
|
143
|
-
default=None,
|
144
|
-
help="Name of the config key to use from the configs.py file."
|
145
|
-
)
|
146
|
-
parser.add_argument(
|
147
|
-
"--task",
|
148
|
-
type=str,
|
149
|
-
default=None,
|
150
|
-
help="The task instructions to guide the assistant."
|
151
|
-
)
|
152
|
-
parser.add_argument(
|
153
|
-
"--context-out",
|
154
|
-
type=str,
|
155
|
-
default=None,
|
156
|
-
help="Optional path to export the generated context to a file."
|
157
|
-
)
|
158
|
-
parser.add_argument(
|
159
|
-
"--context-in",
|
160
|
-
type=str,
|
161
|
-
default=None,
|
162
|
-
help="Optional path to import a previously saved context from a file."
|
163
|
-
)
|
164
|
-
parser.add_argument(
|
165
|
-
"--model",
|
166
|
-
type=str,
|
167
|
-
default="gemini/gemini-2.5-flash",
|
168
|
-
help="Optional model name to override the default model."
|
169
|
-
)
|
170
|
-
parser.add_argument(
|
171
|
-
"--from-file",
|
172
|
-
type=str,
|
173
|
-
default=None,
|
174
|
-
help="File path for a file with pre-formatted updates."
|
175
|
-
)
|
176
|
-
parser.add_argument(
|
177
|
-
"--update",
|
178
|
-
type=str,
|
179
|
-
default="True",
|
180
|
-
help="Whether to pass the input context to the llm to update the files."
|
181
|
-
)
|
182
|
-
parser.add_argument(
|
183
|
-
"--voice",
|
184
|
-
type=str,
|
185
|
-
default="False",
|
186
|
-
help="Whether to interact with the script using voice commands."
|
145
|
+
description="A CLI tool to apply code changes using an LLM.",
|
146
|
+
formatter_class=argparse.RawTextHelpFormatter
|
187
147
|
)
|
188
148
|
|
149
|
+
parser.add_argument("-i", "--init", action="store_true", help="Create a new configuration interactively.")
|
150
|
+
|
151
|
+
parser.add_argument("-c", "--config", type=str, default=None, help="Name of the config key to use from the configs file.")
|
152
|
+
parser.add_argument("-t", "--task", type=str, default=None, help="The task instructions to guide the assistant.")
|
153
|
+
|
154
|
+
parser.add_argument("-co", "--context-out", nargs='?', const="context.md", default=None, help="Export the generated context to a file. Defaults to 'context.md'.")
|
155
|
+
parser.add_argument("-ci", "--context-in", type=str, default=None, help="Import a previously saved context from a file.")
|
156
|
+
|
157
|
+
parser.add_argument("-u", "--update", type=str, default="True", help="Control whether to send the context to the LLM for updates. (True/False)")
|
158
|
+
parser.add_argument("-ff", "--from-file", type=str, default=None, help="Apply updates directly from a file instead of the LLM.")
|
159
|
+
parser.add_argument("-fc", "--from-clipboard", action="store_true", help="Apply updates directly from the clipboard.")
|
160
|
+
|
161
|
+
parser.add_argument("--model", type=str, default="gemini/gemini-1.5-flash", help="Model name to use (e.g., 'gpt-4o', 'claude-3-sonnet').")
|
162
|
+
parser.add_argument("--voice", type=str, default="False", help="Enable voice interaction for providing task instructions. (True/False)")
|
163
|
+
|
164
|
+
parser.add_argument("--list-configs", action="store_true", help="List all available configurations from the configs file and exit.")
|
165
|
+
parser.add_argument("--show-config", type=str, help="Display the settings for a specific configuration and exit.")
|
166
|
+
|
189
167
|
args = parser.parse_args()
|
190
168
|
|
191
|
-
|
169
|
+
try:
|
170
|
+
configs = load_from_py_file(configs_file_path, "configs")
|
171
|
+
except FileNotFoundError:
|
172
|
+
configs = {}
|
173
|
+
if not any([args.init, args.list_configs, args.show_config]):
|
174
|
+
console.print(f"⚠️ Config file '{configs_file_path}' not found. You can create one with the --init flag.", style="yellow")
|
192
175
|
|
193
|
-
# Handle voice input
|
194
|
-
if args.voice not in ["False", "false"]:
|
195
|
-
from listener import listen, speak
|
196
176
|
|
177
|
+
if args.list_configs:
|
178
|
+
console.print(f"Available configurations in '[bold]{configs_file_path}[/]':", style="bold")
|
179
|
+
if not configs:
|
180
|
+
console.print(f" -> No configurations found or '{configs_file_path}' is missing.")
|
181
|
+
else:
|
182
|
+
for config_name in configs:
|
183
|
+
console.print(f" - {config_name}")
|
184
|
+
return
|
185
|
+
|
186
|
+
if args.show_config:
|
187
|
+
config_name = args.show_config
|
188
|
+
if not configs:
|
189
|
+
console.print(f"⚠️ Config file '{configs_file_path}' not found or is empty.", style="yellow")
|
190
|
+
return
|
191
|
+
|
192
|
+
config_data = configs.get(config_name)
|
193
|
+
if config_data:
|
194
|
+
pretty_config = pprint.pformat(config_data, indent=2)
|
195
|
+
console.print(
|
196
|
+
Panel(
|
197
|
+
pretty_config,
|
198
|
+
title=f"[bold cyan]Configuration: '{config_name}'[/]",
|
199
|
+
subtitle=f"[dim]from {configs_file_path}[/dim]",
|
200
|
+
border_style="blue"
|
201
|
+
)
|
202
|
+
)
|
203
|
+
else:
|
204
|
+
console.print(f"❌ Configuration '[bold]{config_name}[/]' not found in '{configs_file_path}'.", style="red")
|
205
|
+
return
|
206
|
+
|
207
|
+
if args.init:
|
208
|
+
create_new_config(configs, configs_file_path)
|
209
|
+
return
|
210
|
+
|
211
|
+
if args.from_clipboard:
|
212
|
+
try:
|
213
|
+
import pyperclip
|
214
|
+
updates = pyperclip.paste()
|
215
|
+
if updates:
|
216
|
+
console.print("--- Parsing updates from clipboard ---", style="bold")
|
217
|
+
paste_response(updates)
|
218
|
+
else:
|
219
|
+
console.print("⚠️ Clipboard is empty. Nothing to parse.", style="yellow")
|
220
|
+
except ImportError:
|
221
|
+
console.print("❌ The 'pyperclip' library is required for clipboard functionality.", style="red")
|
222
|
+
console.print("Please install it using: pip install pyperclip", style="cyan")
|
223
|
+
except Exception as e:
|
224
|
+
console.print(f"❌ An error occurred while reading from the clipboard: {e}", style="red")
|
225
|
+
return
|
226
|
+
|
227
|
+
if args.from_file:
|
228
|
+
updates = read_from_file(args.from_file)
|
229
|
+
paste_response(updates)
|
230
|
+
return
|
231
|
+
|
232
|
+
system_prompt = textwrap.dedent("""
|
233
|
+
You are an expert pair programmer. Your purpose is to help users by modifying files based on their instructions.
|
234
|
+
Follow these rules strictly:
|
235
|
+
Your output should be a single file including all the updated files. For each file-block:
|
236
|
+
1. Only include code for files that need to be updated / edited.
|
237
|
+
2. For updated files, do not exclude any code even if it is unchanged code; assume the file code will be copy-pasted full in the file.
|
238
|
+
3. Do not include verbose inline comments explaining what every small change does. Try to keep comments concise but informative, if any.
|
239
|
+
4. Only update the relevant parts of each file relative to the provided task; do not make irrelevant edits even if you notice areas of improvements elsewhere.
|
240
|
+
5. Do not use diffs.
|
241
|
+
6. Make sure each file-block is returned in the following exact format. No additional text, comments, or explanations should be outside these blocks.
|
242
|
+
Expected format for a modified or new file:
|
243
|
+
<file_path:/absolute/path/to/your/file.py>
|
244
|
+
```python
|
245
|
+
# The full, complete content of /absolute/path/to/your/file.py goes here.
|
246
|
+
def example_function():
|
247
|
+
return "Hello, World!"
|
248
|
+
```
|
249
|
+
""")
|
250
|
+
history = [{"role": "system", "content": system_prompt}]
|
251
|
+
|
252
|
+
context = None
|
253
|
+
if args.voice not in ["False", "false"]:
|
254
|
+
from .listener import listen, speak
|
197
255
|
speak("Say your task instruction.")
|
198
256
|
task = listen()
|
199
257
|
if not task:
|
200
258
|
speak("No instruction heard. Exiting.")
|
201
259
|
return
|
202
|
-
|
203
260
|
speak(f"You said: {task}. Should I proceed?")
|
204
261
|
confirm = listen()
|
205
262
|
if confirm and "yes" in confirm.lower():
|
206
|
-
context =
|
207
|
-
|
263
|
+
context = collect_context(args.config, configs)
|
264
|
+
run_update(task, args.model, history, context)
|
208
265
|
speak("Changes applied.")
|
209
266
|
else:
|
210
267
|
speak("Cancelled.")
|
211
268
|
return
|
212
269
|
|
213
|
-
# Parse updates from a local file
|
214
|
-
if args.from_file:
|
215
|
-
updates = assistant.read(args.from_file)
|
216
|
-
paste_response(updates)
|
217
|
-
return
|
218
|
-
|
219
|
-
# Otherwise generate updates from llm response
|
220
270
|
if args.context_in:
|
221
|
-
context =
|
271
|
+
context = read_from_file(args.context_in)
|
222
272
|
else:
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
273
|
+
if not args.config:
|
274
|
+
parser.error("A --config name is required unless using other flags like --context-in or other utility flags.")
|
275
|
+
context = collect_context(args.config, configs)
|
276
|
+
if context and args.context_out:
|
277
|
+
write_context_to_file(args.context_out, context)
|
278
|
+
|
279
|
+
if args.update not in ["False", "false"]:
|
280
|
+
if not args.task:
|
281
|
+
parser.error("The --task argument is required to generate updates.")
|
282
|
+
if context:
|
283
|
+
run_update(args.task, args.model, history, context)
|
228
284
|
|
229
285
|
if __name__ == "__main__":
|
230
286
|
main()
|
patchllm/parser.py
CHANGED
@@ -1,5 +1,10 @@
|
|
1
1
|
import re
|
2
2
|
from pathlib import Path
|
3
|
+
from rich.console import Console
|
4
|
+
from rich.panel import Panel
|
5
|
+
from rich.text import Text
|
6
|
+
|
7
|
+
console = Console()
|
3
8
|
|
4
9
|
def paste_response(response_content):
|
5
10
|
"""
|
@@ -15,7 +20,10 @@ def paste_response(response_content):
|
|
15
20
|
)
|
16
21
|
|
17
22
|
matches = pattern.finditer(response_content)
|
18
|
-
|
23
|
+
|
24
|
+
files_written = []
|
25
|
+
files_skipped = []
|
26
|
+
files_failed = []
|
19
27
|
found_matches = False
|
20
28
|
|
21
29
|
for match in matches:
|
@@ -24,49 +32,54 @@ def paste_response(response_content):
|
|
24
32
|
code_content = match.group(2)
|
25
33
|
|
26
34
|
if not file_path_str:
|
27
|
-
print("
|
35
|
+
console.print("⚠️ Found a code block with an empty file path. Skipping.", style="yellow")
|
28
36
|
continue
|
29
37
|
|
30
|
-
print(f"Found path in response: '{file_path_str}'")
|
38
|
+
console.print(f"Found path in response: '[cyan]{file_path_str}[/]'")
|
31
39
|
raw_path = Path(file_path_str)
|
32
40
|
|
33
|
-
# Determine the final target path.
|
34
|
-
# If the path from the LLM is absolute, use it directly.
|
35
|
-
# If it's relative, resolve it against the current working directory.
|
36
41
|
if raw_path.is_absolute():
|
37
42
|
target_path = raw_path
|
38
43
|
else:
|
39
44
|
target_path = Path.cwd() / raw_path
|
40
45
|
|
41
|
-
# Normalize the path to resolve any ".." or "." segments.
|
42
46
|
target_path = target_path.resolve()
|
43
47
|
|
44
48
|
try:
|
45
|
-
# Ensure parent directory exists
|
46
49
|
target_path.parent.mkdir(parents=True, exist_ok=True)
|
47
50
|
|
48
|
-
# If file exists, compare content to avoid unnecessary overwrites
|
49
51
|
if target_path.exists():
|
50
52
|
with open(target_path, 'r', encoding='utf-8') as existing_file:
|
51
53
|
if existing_file.read() == code_content:
|
52
|
-
print(f" -> No changes for '{target_path}', skipping.")
|
54
|
+
console.print(f" -> No changes for '[cyan]{target_path}[/]', skipping.", style="dim")
|
55
|
+
files_skipped.append(target_path)
|
53
56
|
continue
|
54
57
|
|
55
|
-
# Write the extracted code to the file
|
56
58
|
with open(target_path, 'w', encoding='utf-8') as outfile:
|
57
59
|
outfile.write(code_content)
|
58
60
|
|
59
|
-
print(f" -> Wrote {len(code_content)} bytes to '{target_path}'")
|
60
|
-
|
61
|
+
console.print(f" -> ✅ Wrote {len(code_content)} bytes to '[cyan]{target_path}[/]'", style="green")
|
62
|
+
files_written.append(target_path)
|
61
63
|
|
62
64
|
except OSError as e:
|
63
|
-
print(f" -> Error writing file '{target_path}': {e}")
|
65
|
+
console.print(f" -> ❌ Error writing file '[cyan]{target_path}[/]': {e}", style="red")
|
66
|
+
files_failed.append(target_path)
|
64
67
|
except Exception as e:
|
65
|
-
print(f" -> An unexpected error occurred for file '{target_path}': {e}")
|
68
|
+
console.print(f" -> ❌ An unexpected error occurred for file '[cyan]{target_path}[/]': {e}", style="red")
|
69
|
+
files_failed.append(target_path)
|
66
70
|
|
71
|
+
summary_text = Text()
|
67
72
|
if not found_matches:
|
68
|
-
|
69
|
-
elif files_processed > 0:
|
70
|
-
print(f"\nSuccessfully processed {files_processed} file(s).")
|
73
|
+
summary_text.append("No file paths and code blocks matching the expected format were found in the response.", style="yellow")
|
71
74
|
else:
|
72
|
-
|
75
|
+
if files_written:
|
76
|
+
summary_text.append(f"Successfully wrote {len(files_written)} file(s).\n", style="green")
|
77
|
+
if files_skipped:
|
78
|
+
summary_text.append(f"Skipped {len(files_skipped)} file(s) (no changes).\n", style="cyan")
|
79
|
+
if files_failed:
|
80
|
+
summary_text.append(f"Failed to write {len(files_failed)} file(s).\n", style="red")
|
81
|
+
|
82
|
+
if not any([files_written, files_skipped, files_failed]):
|
83
|
+
summary_text.append("Found matching blocks, but no files were processed.", style="yellow")
|
84
|
+
|
85
|
+
console.print(Panel(summary_text, title="[bold]Summary[/bold]", border_style="blue"))
|
@@ -0,0 +1,127 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: patchllm
|
3
|
+
Version: 0.2.1
|
4
|
+
Summary: Lightweight tool to manage contexts and update code with LLMs
|
5
|
+
Author: nassimberrada
|
6
|
+
License: MIT License
|
7
|
+
|
8
|
+
Copyright (c) 2025 nassimberrada
|
9
|
+
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
11
|
+
of this software and associated documentation files (the “Software”), to deal
|
12
|
+
in the Software without restriction, including without limitation the rights
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
15
|
+
furnished to do so, subject to the following conditions:
|
16
|
+
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
18
|
+
copies or substantial portions of the Software.
|
19
|
+
|
20
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
26
|
+
SOFTWARE.
|
27
|
+
Requires-Python: >=3.8
|
28
|
+
Description-Content-Type: text/markdown
|
29
|
+
License-File: LICENSE
|
30
|
+
Requires-Dist: litellm
|
31
|
+
Requires-Dist: python-dotenv
|
32
|
+
Requires-Dist: rich
|
33
|
+
Provides-Extra: voice
|
34
|
+
Requires-Dist: SpeechRecognition; extra == "voice"
|
35
|
+
Requires-Dist: pyttsx3; extra == "voice"
|
36
|
+
Provides-Extra: url
|
37
|
+
Requires-Dist: html2text; extra == "url"
|
38
|
+
Dynamic: license-file
|
39
|
+
|
40
|
+
<p align="center">
|
41
|
+
<picture>
|
42
|
+
<source srcset="./assets/logo_dark.png" media="(prefers-color-scheme: dark)">
|
43
|
+
<source srcset="./assets/logo_light.png" media="(prefers-color-scheme: light)">
|
44
|
+
<img src="./assets/logo_light.png" alt="PatchLLM Logo" height="200">
|
45
|
+
</picture>
|
46
|
+
</p>
|
47
|
+
|
48
|
+
## About
|
49
|
+
PatchLLM is a command-line tool that lets you flexibly build LLM context from your codebase using glob patterns, URLs, and keyword searches. It then automatically applies file edits directly from the LLM's response.
|
50
|
+
|
51
|
+
## Usage
|
52
|
+
PatchLLM is designed to be used directly from your terminal.
|
53
|
+
|
54
|
+
### 1. Initialize a Configuration
|
55
|
+
The easiest way to get started is to run the interactive initializer. This will create a `configs.py` file for you.
|
56
|
+
|
57
|
+
```bash
|
58
|
+
patchllm --init
|
59
|
+
```
|
60
|
+
|
61
|
+
This will guide you through creating your first context configuration, including setting a base path and file patterns. You can add multiple configurations to this file.
|
62
|
+
|
63
|
+
A generated `configs.py` might look like this:
|
64
|
+
```python
|
65
|
+
# configs.py
|
66
|
+
configs = {
|
67
|
+
"default": {
|
68
|
+
"path": ".",
|
69
|
+
"include_patterns": ["**/*.py"],
|
70
|
+
"exclude_patterns": ["**/tests/*", "venv/*"],
|
71
|
+
"urls": ["https://docs.python.org/3/library/argparse.html"]
|
72
|
+
},
|
73
|
+
"docs": {
|
74
|
+
"path": "./docs",
|
75
|
+
"include_patterns": ["**/*.md"],
|
76
|
+
}
|
77
|
+
}
|
78
|
+
```
|
79
|
+
|
80
|
+
### 2. Run a Task
|
81
|
+
Use the `patchllm` command with a configuration name and a task instruction.
|
82
|
+
|
83
|
+
```bash
|
84
|
+
# Apply a change using the 'default' configuration
|
85
|
+
patchllm --config default --task "Add type hints to the main function in main.py"
|
86
|
+
```
|
87
|
+
|
88
|
+
The tool will then:
|
89
|
+
1. Build a context from the files and URLs matching your configuration.
|
90
|
+
2. Send the context and your task to the configured LLM.
|
91
|
+
3. Parse the response and automatically write the changes to the relevant files.
|
92
|
+
|
93
|
+
### All Commands & Options
|
94
|
+
|
95
|
+
#### Configuration Management
|
96
|
+
* `--init`: Create a new configuration interactively.
|
97
|
+
* `--list-configs`: List all available configurations from your `configs.py`.
|
98
|
+
* `--show-config <name>`: Display the settings for a specific configuration.
|
99
|
+
|
100
|
+
#### Core Task Execution
|
101
|
+
* `--config <name>`: The name of the configuration to use for building context.
|
102
|
+
* `--task "<instruction>"`: The task instruction for the LLM.
|
103
|
+
* `--model <model_name>`: Specify a different model (e.g., `claude-3-opus`). Defaults to `gemini/gemini-1.5-flash`.
|
104
|
+
|
105
|
+
#### Context Handling
|
106
|
+
* `--context-out [filename]`: Save the generated context to a file (defaults to `context.md`) instead of sending it to the LLM.
|
107
|
+
* `--context-in <filename>`: Use a previously saved context file directly, skipping context generation.
|
108
|
+
* `--update False`: A flag to prevent sending the prompt to the LLM. Useful when you only want to generate and save the context with `--context-out`.
|
109
|
+
|
110
|
+
#### Alternative Inputs
|
111
|
+
* `--from-file <filename>`: Apply file patches directly from a local file instead of from an LLM response.
|
112
|
+
* `--from-clipboard`: Apply file patches directly from your clipboard content.
|
113
|
+
* `--voice True`: Use voice recognition to provide the task instruction. Requires extra dependencies.
|
114
|
+
|
115
|
+
### Setup
|
116
|
+
|
117
|
+
PatchLLM uses [LiteLLM](https://github.com/BerriAI/litellm) under the hood. Please refer to their documentation for setting up API keys (e.g., `OPENAI_API_KEY`, `GEMINI_API_KEY`) in a `.env` file and for a full list of available models.
|
118
|
+
|
119
|
+
To use the voice feature (`--voice True`), you will need to install extra dependencies:
|
120
|
+
```bash
|
121
|
+
pip install "speechrecognition>=3.10" "pyttsx3>=2.90"
|
122
|
+
# Note: speechrecognition may require PyAudio, which might have system-level dependencies.
|
123
|
+
```
|
124
|
+
|
125
|
+
## License
|
126
|
+
|
127
|
+
This project is licensed under the MIT License. See the `LICENSE` file for details.
|
@@ -0,0 +1,12 @@
|
|
1
|
+
patchllm/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
+
patchllm/context.py,sha256=BZxhUUnlwejcY3I5jfIuk9r2xAqgN1eNcOARmD5VLzU,8520
|
3
|
+
patchllm/listener.py,sha256=VjQ_CrSRT4-PolXAAradPKyt8NSUaUQwvgPNH7Oi9q0,968
|
4
|
+
patchllm/main.py,sha256=lwEE98yFULW1C0Lo9i1z5u9_r7zJYPSalWxh8juSRT8,12931
|
5
|
+
patchllm/parser.py,sha256=DNcf9iUH8umExfK78CSIwac1Bbu7K9iE3754y7CvYzs,3229
|
6
|
+
patchllm/utils.py,sha256=hz28hd017gRGT632VQAYLPdX0KAS1GLvZzeUDCKbLc0,647
|
7
|
+
patchllm-0.2.1.dist-info/licenses/LICENSE,sha256=vZxgIRNxffjkTV2NWLemgYjDRu0hSMTyFXCZ1zEWbUc,1077
|
8
|
+
patchllm-0.2.1.dist-info/METADATA,sha256=zbk1TqpmkEbpXV1jtjLP34CYZ_MyDboZCRQ9FTnNCNk,5429
|
9
|
+
patchllm-0.2.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
10
|
+
patchllm-0.2.1.dist-info/entry_points.txt,sha256=xm-W7FKOQd3o9RgK_4krVnO2sC8phpYxDCobf0htLiU,48
|
11
|
+
patchllm-0.2.1.dist-info/top_level.txt,sha256=SLIZj9EhBXbSnYrbnV8EjL-OfNz-hXRwABCPCjE5Fas,9
|
12
|
+
patchllm-0.2.1.dist-info/RECORD,,
|
@@ -1,83 +0,0 @@
|
|
1
|
-
Metadata-Version: 2.4
|
2
|
-
Name: patchllm
|
3
|
-
Version: 0.1.0
|
4
|
-
Summary: Lightweight tool to manage contexts and update code with LLMs
|
5
|
-
Author: nassimberrada
|
6
|
-
License: MIT License
|
7
|
-
|
8
|
-
Copyright (c) 2025 nassimberrada
|
9
|
-
|
10
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
11
|
-
of this software and associated documentation files (the “Software”), to deal
|
12
|
-
in the Software without restriction, including without limitation the rights
|
13
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
14
|
-
copies of the Software, and to permit persons to whom the Software is
|
15
|
-
furnished to do so, subject to the following conditions:
|
16
|
-
|
17
|
-
The above copyright notice and this permission notice shall be included in all
|
18
|
-
copies or substantial portions of the Software.
|
19
|
-
|
20
|
-
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
21
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
22
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
23
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
24
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
25
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
26
|
-
SOFTWARE.
|
27
|
-
Requires-Python: >=3.8
|
28
|
-
Description-Content-Type: text/markdown
|
29
|
-
License-File: LICENSE
|
30
|
-
Requires-Dist: litellm
|
31
|
-
Dynamic: license-file
|
32
|
-
|
33
|
-
<p align="center">
|
34
|
-
<picture>
|
35
|
-
<source srcset="./assets/logo_dark.png" media="(prefers-color-scheme: dark)">
|
36
|
-
<source srcset="./assets/logo_light.png" media="(prefers-color-scheme: light)">
|
37
|
-
<img src="./assets/logo_light.png" alt="PatchLLM Logo" height="200">
|
38
|
-
</picture>
|
39
|
-
</p>
|
40
|
-
|
41
|
-
## About
|
42
|
-
PatchLLM lets you flexibly build LLM context from your codebase using search patterns, and automatically edit files from the LLM response in a couple lines of code.
|
43
|
-
|
44
|
-
## Usage
|
45
|
-
Here's a basic example of how to use the `Assistant` class:
|
46
|
-
|
47
|
-
```python
|
48
|
-
from main import Assistant
|
49
|
-
|
50
|
-
assistant = Assistant()
|
51
|
-
|
52
|
-
context = assistant.collect(config_name="default")
|
53
|
-
>> The following files were extracted:
|
54
|
-
>> my_project
|
55
|
-
>> ├── README.md
|
56
|
-
>> ├── configs.py
|
57
|
-
>> ├── context.py
|
58
|
-
>> ├── main.py
|
59
|
-
>> ├── parser.py
|
60
|
-
>> ├── requirements.txt
|
61
|
-
>> ├── systems.py
|
62
|
-
>> └── utils.py
|
63
|
-
|
64
|
-
assistant.update("Fix any bug in these files", context=context)
|
65
|
-
>> Wrote 5438 bytes to '/my_project/context.py'
|
66
|
-
>> Wrote 1999 bytes to '/my_project/utils.py'
|
67
|
-
>> Wrote 2345 bytes to '/my_project/main.py'
|
68
|
-
```
|
69
|
-
|
70
|
-
You can decide which files to include / exclude from the prompt by adding a config in `configs.py`, specifying:
|
71
|
-
- `path`: The root path from which to perform the file search
|
72
|
-
- `include_patterns`: A list of glob patterns for files to include. e.g `[./**/*]`
|
73
|
-
- `exclude_patterns`: A list of glob patterns for files to exlucde. e.g `[./*.md]`
|
74
|
-
- `search_word`: A list of keywords included in the target files. e.g `["config"]`
|
75
|
-
- `exclude_extensions`: A list of file extensions to exclude. e.g `[.jpg]`
|
76
|
-
|
77
|
-
### Setup
|
78
|
-
|
79
|
-
PatchLLM uses [LiteLLM](https://github.com/BerriAI/litellm) under the hood. Please refer to their documentation for environment variable naming and available models.
|
80
|
-
|
81
|
-
## License
|
82
|
-
|
83
|
-
This project is licensed under the MIT License. See the `LICENSE` file for details.
|
patchllm-0.1.0.dist-info/RECORD
DELETED
@@ -1,12 +0,0 @@
|
|
1
|
-
patchllm/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
-
patchllm/context.py,sha256=zUrXf5l3cdxAbmxB7IjbShTAWA_ZEMBz8OGlaB-cofE,6450
|
3
|
-
patchllm/listener.py,sha256=EdcceJCLEoSftX1dVSWxtwBsLaII2lcZ0VnllHwCGWI,845
|
4
|
-
patchllm/main.py,sha256=-11bAS-bx2SfGx14KCCZhuwrfh_FDcQ80cwUfYrszY8,8569
|
5
|
-
patchllm/parser.py,sha256=4wipa6deoE2gUIhYrvUZcbKTIr5j6lw5Z6bOItUH6YI,2629
|
6
|
-
patchllm/utils.py,sha256=hz28hd017gRGT632VQAYLPdX0KAS1GLvZzeUDCKbLc0,647
|
7
|
-
patchllm-0.1.0.dist-info/licenses/LICENSE,sha256=vZxgIRNxffjkTV2NWLemgYjDRu0hSMTyFXCZ1zEWbUc,1077
|
8
|
-
patchllm-0.1.0.dist-info/METADATA,sha256=BtSvIfjwiWqvv-1d_GPBf1n7ao9R2YY3m8Qd_Rr4c6A,3404
|
9
|
-
patchllm-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
10
|
-
patchllm-0.1.0.dist-info/entry_points.txt,sha256=_jqCdL7snk6RZfqiSzP_XttWYAPNw_UdnAEqYS-rrd8,48
|
11
|
-
patchllm-0.1.0.dist-info/top_level.txt,sha256=SLIZj9EhBXbSnYrbnV8EjL-OfNz-hXRwABCPCjE5Fas,9
|
12
|
-
patchllm-0.1.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|