gptdiff 0.1.4__tar.gz → 0.1.6__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: gptdiff
3
- Version: 0.1.4
3
+ Version: 0.1.6
4
4
  Summary: A tool to generate and apply git diffs using LLMs
5
5
  Author: 255labs
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -10,7 +10,7 @@ Description-Content-Type: text/markdown
10
10
  License-File: LICENSE.txt
11
11
  Requires-Dist: openai>=1.0.0
12
12
  Requires-Dist: tiktoken>=0.5.0
13
- Requires-Dist: ai_agent_toolbox>=0.1.0
13
+ Requires-Dist: ai_agent_toolbox>=0.1.9
14
14
  Provides-Extra: test
15
15
  Requires-Dist: pytest; extra == "test"
16
16
  Requires-Dist: pytest-mock; extra == "test"
@@ -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 create_toolbox():
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 = create_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), "characters of file 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("SMARTAPPLY", diff_text)
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 = call_llm_for_apply(path, original, patch, model, api_key=api_key, base_url=base_url)
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
@@ -289,7 +319,7 @@ def apply_diff(project_dir, diff_text):
289
319
  with open(diff_file, 'w') as f:
290
320
  f.write(diff_text)
291
321
 
292
- result = subprocess.run(["patch", "-p1", "--remove-empty-files", "--input", str(diff_file)], cwd=project_dir, capture_output=True, text=True)
322
+ result = subprocess.run(["patch", "-p1", "-f", "--remove-empty-files", "--input", str(diff_file)], cwd=project_dir, capture_output=True, text=True)
293
323
  if result.returncode != 0:
294
324
  return False
295
325
  else:
@@ -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
- return response.choices[0].message.content
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 = call_llm_for_apply(file_path, original_content, file_diff, args.model)
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")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: gptdiff
3
- Version: 0.1.4
3
+ Version: 0.1.6
4
4
  Summary: A tool to generate and apply git diffs using LLMs
5
5
  Author: 255labs
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -10,7 +10,7 @@ Description-Content-Type: text/markdown
10
10
  License-File: LICENSE.txt
11
11
  Requires-Dist: openai>=1.0.0
12
12
  Requires-Dist: tiktoken>=0.5.0
13
- Requires-Dist: ai_agent_toolbox>=0.1.0
13
+ Requires-Dist: ai_agent_toolbox>=0.1.9
14
14
  Provides-Extra: test
15
15
  Requires-Dist: pytest; extra == "test"
16
16
  Requires-Dist: pytest-mock; extra == "test"
@@ -1,6 +1,6 @@
1
1
  openai>=1.0.0
2
2
  tiktoken>=0.5.0
3
- ai_agent_toolbox>=0.1.0
3
+ ai_agent_toolbox>=0.1.9
4
4
 
5
5
  [docs]
6
6
  mkdocs
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name='gptdiff',
5
- version='0.1.4',
5
+ version='0.1.6',
6
6
  description='A tool to generate and apply git diffs using LLMs',
7
7
  author='255labs',
8
8
  packages=find_packages(), # Use find_packages() to automatically discover packages
@@ -12,7 +12,7 @@ setup(
12
12
  install_requires=[
13
13
  'openai>=1.0.0',
14
14
  'tiktoken>=0.5.0',
15
- 'ai_agent_toolbox>=0.1.0'
15
+ 'ai_agent_toolbox>=0.1.9'
16
16
  ],
17
17
  extras_require={
18
18
  'test': ['pytest', 'pytest-mock'],
@@ -35,6 +35,32 @@ def test_smartapply_file_modification():
35
35
  print('Hello')
36
36
  +
37
37
  +def goodbye():
38
+ + print('Goodbye')'''
39
+
40
+ original_hello = "def hello():\n print('Hello')"
41
+ original_files = {
42
+ "hello.py": original_hello
43
+ }
44
+
45
+ # Mock LLM to return modified content
46
+ with patch('gptdiff.gptdiff.call_llm_for_apply',
47
+ return_value="\ndef goodbye():\n print('Goodbye')"):
48
+
49
+ updated_files = smartapply(diff_text, original_files)
50
+
51
+ assert "hello.py" in updated_files
52
+ assert original_hello != updated_files["hello.py"]
53
+
54
+ def test_smartapply_think_then_modify():
55
+ """Test that smartapply correctly handles file modification diffs"""
56
+ diff_text = '''diff --git a/hello.py b/hello.py
57
+ --- a/hello.py
58
+ +++ b/hello.py
59
+ @@ -1,2 +1,5 @@
60
+ def hello():
61
+ print('Hello')
62
+ +
63
+ +def goodbye():
38
64
  + print('Goodbye')'''
39
65
 
40
66
  original_files = {
@@ -43,12 +69,13 @@ def test_smartapply_file_modification():
43
69
 
44
70
  # Mock LLM to return modified content
45
71
  with patch('gptdiff.gptdiff.call_llm_for_apply',
46
- return_value="def hello():\n print('Hello')\n\ndef goodbye():\n print('Goodbye')"):
72
+ return_value="<think>Hello from thoughts</think>\ndef goodbye():\n print('Goodbye')"):
47
73
 
48
74
  updated_files = smartapply(diff_text, original_files)
49
75
 
50
76
  assert "hello.py" in updated_files
51
- assert original_files["hello.py"] != updated_files["hello.py"]
77
+ assert updated_files["hello.py"] == "def goodbye():\n print('Goodbye')"
78
+
52
79
 
53
80
  def test_smartapply_new_file_creation():
54
81
  """Test that smartapply handles new file creation through diffs"""
File without changes
File without changes
File without changes
File without changes