gptdiff 0.1.4__tar.gz → 0.1.6__tar.gz

Sign up to get free protection for your applications and to get access to all the features.
@@ -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