gptdiff 0.1.5__py3-none-any.whl → 0.1.6__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.
- gptdiff/gptdiff.py +82 -15
- {gptdiff-0.1.5.dist-info → gptdiff-0.1.6.dist-info}/METADATA +1 -1
- gptdiff-0.1.6.dist-info/RECORD +8 -0
- gptdiff-0.1.5.dist-info/RECORD +0 -8
- {gptdiff-0.1.5.dist-info → gptdiff-0.1.6.dist-info}/LICENSE.txt +0 -0
- {gptdiff-0.1.5.dist-info → gptdiff-0.1.6.dist-info}/WHEEL +0 -0
- {gptdiff-0.1.5.dist-info → gptdiff-0.1.6.dist-info}/entry_points.txt +0 -0
- {gptdiff-0.1.5.dist-info → gptdiff-0.1.6.dist-info}/top_level.txt +0 -0
gptdiff/gptdiff.py
CHANGED
@@ -4,6 +4,7 @@ import openai
|
|
4
4
|
from openai import OpenAI
|
5
5
|
|
6
6
|
import tiktoken
|
7
|
+
import time
|
7
8
|
|
8
9
|
import os
|
9
10
|
import json
|
@@ -20,7 +21,7 @@ import threading
|
|
20
21
|
from pkgutil import get_data
|
21
22
|
|
22
23
|
diff_context = contextvars.ContextVar('diffcontent', default="")
|
23
|
-
def
|
24
|
+
def create_diff_toolbox():
|
24
25
|
toolbox = Toolbox()
|
25
26
|
|
26
27
|
def diff(content: str):
|
@@ -47,6 +48,25 @@ a/file.py b/file.py
|
|
47
48
|
)
|
48
49
|
return toolbox
|
49
50
|
|
51
|
+
def create_think_toolbox():
|
52
|
+
toolbox = Toolbox()
|
53
|
+
|
54
|
+
def think(content: str):
|
55
|
+
print("Swallowed thoughts", content)
|
56
|
+
|
57
|
+
toolbox.add_tool(
|
58
|
+
name="think",
|
59
|
+
fn=think,
|
60
|
+
args={
|
61
|
+
"content": {
|
62
|
+
"type": "string",
|
63
|
+
"description": "Thoughts"
|
64
|
+
}
|
65
|
+
},
|
66
|
+
description=""
|
67
|
+
)
|
68
|
+
return toolbox
|
69
|
+
|
50
70
|
|
51
71
|
def load_gitignore_patterns(gitignore_path):
|
52
72
|
with open(gitignore_path, 'r') as f:
|
@@ -146,10 +166,12 @@ def load_prepend_file(file):
|
|
146
166
|
|
147
167
|
# Function to call GPT-4 API and calculate the cost
|
148
168
|
def call_gpt4_api(system_prompt, user_prompt, files_content, model, temperature=0.7, max_tokens=2500, api_key=None, base_url=None):
|
169
|
+
enc = tiktoken.get_encoding("o200k_base")
|
170
|
+
start_time = time.time()
|
149
171
|
|
150
172
|
parser = FlatXMLParser("diff")
|
151
173
|
formatter = FlatXMLPromptFormatter(tag="diff")
|
152
|
-
toolbox =
|
174
|
+
toolbox = create_diff_toolbox()
|
153
175
|
tool_prompt = formatter.usage_prompt(toolbox)
|
154
176
|
system_prompt += "\n"+tool_prompt
|
155
177
|
|
@@ -164,7 +186,7 @@ def call_gpt4_api(system_prompt, user_prompt, files_content, model, temperature=
|
|
164
186
|
print("SYSTEM PROMPT")
|
165
187
|
print(system_prompt)
|
166
188
|
print("USER PROMPT")
|
167
|
-
print(user_prompt, "+", len(files_content), "
|
189
|
+
print(user_prompt, "+", len(enc.encode(files_content)), "tokens of file content")
|
168
190
|
|
169
191
|
if api_key is None:
|
170
192
|
api_key = os.getenv('GPTDIFF_LLM_API_KEY')
|
@@ -180,6 +202,12 @@ def call_gpt4_api(system_prompt, user_prompt, files_content, model, temperature=
|
|
180
202
|
completion_tokens = response.usage.completion_tokens
|
181
203
|
total_tokens = response.usage.total_tokens
|
182
204
|
|
205
|
+
elapsed = time.time() - start_time
|
206
|
+
minutes, seconds = divmod(int(elapsed), 60)
|
207
|
+
time_str = f"{minutes}m {seconds}s" if minutes else f"{seconds}s"
|
208
|
+
print(f"Diff creation time: {time_str}")
|
209
|
+
print("-" * 40)
|
210
|
+
|
183
211
|
# Now, these rates are updated to per million tokens
|
184
212
|
cost_per_million_prompt_tokens = 30
|
185
213
|
cost_per_million_completion_tokens = 60
|
@@ -187,7 +215,6 @@ def call_gpt4_api(system_prompt, user_prompt, files_content, model, temperature=
|
|
187
215
|
|
188
216
|
full_response = response.choices[0].message.content.strip()
|
189
217
|
|
190
|
-
|
191
218
|
events = parser.parse(full_response)
|
192
219
|
for event in events:
|
193
220
|
toolbox.use(event)
|
@@ -266,7 +293,10 @@ def smartapply(diff_text, files, model=None, api_key=None, base_url=None):
|
|
266
293
|
if model is None:
|
267
294
|
model = os.getenv('GPTDIFF_MODEL', 'deepseek-reasoner')
|
268
295
|
parsed_diffs = parse_diff_per_file(diff_text)
|
269
|
-
print("
|
296
|
+
print("-" * 40)
|
297
|
+
print("SMARTAPPLY")
|
298
|
+
print(diff_text)
|
299
|
+
print("-" * 40)
|
270
300
|
|
271
301
|
def process_file(path, patch):
|
272
302
|
original = files.get(path, '')
|
@@ -275,12 +305,12 @@ def smartapply(diff_text, files, model=None, api_key=None, base_url=None):
|
|
275
305
|
if path in files:
|
276
306
|
del files[path]
|
277
307
|
else:
|
278
|
-
updated =
|
308
|
+
updated = call_llm_for_apply_with_think_tool_available(path, original, patch, model, api_key=api_key, base_url=base_url)
|
279
309
|
files[path] = updated.strip()
|
280
|
-
|
310
|
+
|
281
311
|
for path, patch in parsed_diffs:
|
282
312
|
process_file(path, patch)
|
283
|
-
|
313
|
+
|
284
314
|
return files
|
285
315
|
|
286
316
|
# Function to apply diff to project files
|
@@ -320,20 +350,20 @@ def absolute_to_relative(absolute_path):
|
|
320
350
|
|
321
351
|
def parse_diff_per_file(diff_text):
|
322
352
|
"""Parse unified diff text into individual file patches.
|
323
|
-
|
353
|
+
|
324
354
|
Splits a multi-file diff into per-file entries for processing. Handles:
|
325
355
|
- File creations (+++ /dev/null)
|
326
356
|
- File deletions (--- /dev/null)
|
327
357
|
- Standard modifications
|
328
|
-
|
358
|
+
|
329
359
|
Args:
|
330
360
|
diff_text: Unified diff string as generated by `git diff`
|
331
|
-
|
361
|
+
|
332
362
|
Returns:
|
333
363
|
List of tuples (file_path, patch) where:
|
334
364
|
- file_path: Relative path to modified file
|
335
365
|
- patch: Full diff fragment for this file
|
336
|
-
|
366
|
+
|
337
367
|
Note:
|
338
368
|
Uses 'b/' prefix detection from git diffs to determine target paths
|
339
369
|
"""
|
@@ -373,6 +403,26 @@ def parse_diff_per_file(diff_text):
|
|
373
403
|
|
374
404
|
return diffs
|
375
405
|
|
406
|
+
def call_llm_for_apply_with_think_tool_available(file_path, original_content, file_diff, model, api_key=None, base_url=None):
|
407
|
+
parser = FlatXMLParser("think")
|
408
|
+
formatter = FlatXMLPromptFormatter(tag="think")
|
409
|
+
toolbox = create_think_toolbox()
|
410
|
+
full_response = call_llm_for_apply(file_path, original_content, file_diff, model, api_key=None, base_url=None)
|
411
|
+
notool_response = ""
|
412
|
+
events = parser.parse(full_response)
|
413
|
+
is_in_tool = False
|
414
|
+
appended_content = ""
|
415
|
+
for event in events:
|
416
|
+
if event.mode == 'append':
|
417
|
+
appended_content += event.content
|
418
|
+
if event.mode == 'close' and appended_content and event.tool is None:
|
419
|
+
notool_response += appended_content
|
420
|
+
if event.mode == 'close':
|
421
|
+
appended_content = ""
|
422
|
+
toolbox.use(event)
|
423
|
+
|
424
|
+
return notool_response
|
425
|
+
|
376
426
|
def call_llm_for_apply(file_path, original_content, file_diff, model, api_key=None, base_url=None):
|
377
427
|
"""AI-powered diff application with conflict resolution.
|
378
428
|
|
@@ -409,7 +459,8 @@ def call_llm_for_apply(file_path, original_content, file_diff, model, api_key=No
|
|
409
459
|
|
410
460
|
1. Carefully apply all changes from the diff
|
411
461
|
2. Preserve surrounding context that isn't changed
|
412
|
-
3. Only return the final file content, do not add any additional markup and do not add a code block
|
462
|
+
3. Only return the final file content, do not add any additional markup and do not add a code block
|
463
|
+
4. You must return the entire file. It overwrites the existing file."""
|
413
464
|
|
414
465
|
user_prompt = f"""File: {file_path}
|
415
466
|
File contents:
|
@@ -434,12 +485,19 @@ Diff to apply:
|
|
434
485
|
if base_url is None:
|
435
486
|
base_url = os.getenv('GPTDIFF_LLM_BASE_URL', "https://nano-gpt.com/api/v1/")
|
436
487
|
client = OpenAI(api_key=api_key, base_url=base_url)
|
488
|
+
start_time = time.time()
|
437
489
|
response = client.chat.completions.create(model=model,
|
438
490
|
messages=messages,
|
439
491
|
temperature=0.0,
|
440
492
|
max_tokens=30000)
|
493
|
+
full_response = response.choices[0].message.content
|
441
494
|
|
442
|
-
|
495
|
+
elapsed = time.time() - start_time
|
496
|
+
minutes, seconds = divmod(int(elapsed), 60)
|
497
|
+
time_str = f"{minutes}m {seconds}s" if minutes else f"{seconds}s"
|
498
|
+
print(f"Smartapply time: {time_str}")
|
499
|
+
print("-" * 40)
|
500
|
+
return full_response
|
443
501
|
|
444
502
|
def build_environment_from_filelist(file_list, cwd):
|
445
503
|
"""Build environment string from list of file paths"""
|
@@ -587,8 +645,17 @@ def main():
|
|
587
645
|
print(f"Skipping binary file {file_path}")
|
588
646
|
return
|
589
647
|
|
648
|
+
print("-" * 40)
|
649
|
+
print("SMARTAPPLY")
|
650
|
+
print(file_diff)
|
651
|
+
print("-" * 40)
|
590
652
|
try:
|
591
|
-
updated_content =
|
653
|
+
updated_content = call_llm_for_apply_with_think_tool_available(file_path, original_content, file_diff, args.model)
|
654
|
+
|
655
|
+
if updated_content.strip() == "":
|
656
|
+
print("Cowardly refusing to write empty file to", file_path, "merge failed")
|
657
|
+
return
|
658
|
+
|
592
659
|
full_path.parent.mkdir(parents=True, exist_ok=True)
|
593
660
|
full_path.write_text(updated_content)
|
594
661
|
print(f"\033[1;32mSuccessful 'smartapply' update {file_path}.\033[0m")
|
@@ -0,0 +1,8 @@
|
|
1
|
+
gptdiff/__init__.py,sha256=yGjgwv7tNvH1ZLPsQyoo1CxpTOl1iCAwwDBp-_17ksQ,89
|
2
|
+
gptdiff/gptdiff.py,sha256=VAbTBgxGYzxj2ZxRHCnZAneeFfZ8YAJ_qkB_9umnyjg,25689
|
3
|
+
gptdiff-0.1.6.dist-info/LICENSE.txt,sha256=zCJk7yUYpMjFvlipi1dKtaljF8WdZ2NASndBYYbU8BY,1228
|
4
|
+
gptdiff-0.1.6.dist-info/METADATA,sha256=hMEEWaG6TsauWVkDbtBD05uHtDjrBp3VaCmxXdh2QDk,7316
|
5
|
+
gptdiff-0.1.6.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
6
|
+
gptdiff-0.1.6.dist-info/entry_points.txt,sha256=0yvXYEVAZFI-p32kQ4-h3qKVWS0a86jsM9FAwF89t9w,49
|
7
|
+
gptdiff-0.1.6.dist-info/top_level.txt,sha256=XNkQkQGINaDndEwRxg8qToOrJ9coyfAb-EHrSUXzdCE,8
|
8
|
+
gptdiff-0.1.6.dist-info/RECORD,,
|
gptdiff-0.1.5.dist-info/RECORD
DELETED
@@ -1,8 +0,0 @@
|
|
1
|
-
gptdiff/__init__.py,sha256=yGjgwv7tNvH1ZLPsQyoo1CxpTOl1iCAwwDBp-_17ksQ,89
|
2
|
-
gptdiff/gptdiff.py,sha256=18BXTs2L8TgiwUCnqDm4GD7CdkFHa-9LoHd1ZQnz1io,23407
|
3
|
-
gptdiff-0.1.5.dist-info/LICENSE.txt,sha256=zCJk7yUYpMjFvlipi1dKtaljF8WdZ2NASndBYYbU8BY,1228
|
4
|
-
gptdiff-0.1.5.dist-info/METADATA,sha256=Lb5pD2_Hz3M3nt8dC60O_M0MQBRd-lq9jArcX5isY1Q,7316
|
5
|
-
gptdiff-0.1.5.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
6
|
-
gptdiff-0.1.5.dist-info/entry_points.txt,sha256=0yvXYEVAZFI-p32kQ4-h3qKVWS0a86jsM9FAwF89t9w,49
|
7
|
-
gptdiff-0.1.5.dist-info/top_level.txt,sha256=XNkQkQGINaDndEwRxg8qToOrJ9coyfAb-EHrSUXzdCE,8
|
8
|
-
gptdiff-0.1.5.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|