gptdiff 0.1.28__tar.gz → 0.1.31__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.
Files changed (24) hide show
  1. {gptdiff-0.1.28 → gptdiff-0.1.31}/PKG-INFO +26 -8
  2. {gptdiff-0.1.28 → gptdiff-0.1.31}/README.md +26 -8
  3. {gptdiff-0.1.28 → gptdiff-0.1.31}/gptdiff/applydiff.py +9 -2
  4. {gptdiff-0.1.28 → gptdiff-0.1.31}/gptdiff/gptdiff.py +139 -40
  5. {gptdiff-0.1.28 → gptdiff-0.1.31}/gptdiff.egg-info/PKG-INFO +26 -8
  6. {gptdiff-0.1.28 → gptdiff-0.1.31}/gptdiff.egg-info/SOURCES.txt +1 -0
  7. {gptdiff-0.1.28 → gptdiff-0.1.31}/setup.py +1 -1
  8. gptdiff-0.1.31/tests/test_multidiff.py +50 -0
  9. {gptdiff-0.1.28 → gptdiff-0.1.31}/tests/test_parse_diff_per_file.py +28 -0
  10. {gptdiff-0.1.28 → gptdiff-0.1.31}/tests/test_swallow_reasoning.py +28 -8
  11. {gptdiff-0.1.28 → gptdiff-0.1.31}/LICENSE.txt +0 -0
  12. {gptdiff-0.1.28 → gptdiff-0.1.31}/gptdiff/__init__.py +0 -0
  13. {gptdiff-0.1.28 → gptdiff-0.1.31}/gptdiff/gptpatch.py +0 -0
  14. {gptdiff-0.1.28 → gptdiff-0.1.31}/gptdiff.egg-info/dependency_links.txt +0 -0
  15. {gptdiff-0.1.28 → gptdiff-0.1.31}/gptdiff.egg-info/entry_points.txt +0 -0
  16. {gptdiff-0.1.28 → gptdiff-0.1.31}/gptdiff.egg-info/requires.txt +0 -0
  17. {gptdiff-0.1.28 → gptdiff-0.1.31}/gptdiff.egg-info/top_level.txt +0 -0
  18. {gptdiff-0.1.28 → gptdiff-0.1.31}/setup.cfg +0 -0
  19. {gptdiff-0.1.28 → gptdiff-0.1.31}/tests/test_applydiff.py +0 -0
  20. {gptdiff-0.1.28 → gptdiff-0.1.31}/tests/test_applydiff_edgecases.py +0 -0
  21. {gptdiff-0.1.28 → gptdiff-0.1.31}/tests/test_diff_parse.py +0 -0
  22. {gptdiff-0.1.28 → gptdiff-0.1.31}/tests/test_failing_case.py +0 -0
  23. {gptdiff-0.1.28 → gptdiff-0.1.31}/tests/test_smartapply.py +0 -0
  24. {gptdiff-0.1.28 → gptdiff-0.1.31}/tests/test_strip_bad_ouput.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: gptdiff
3
- Version: 0.1.28
3
+ Version: 0.1.31
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
@@ -31,30 +31,48 @@ GPTDiff: Create and apply diffs using AI.
31
31
  This tool leverages natural language instructions to modify project codebases.
32
32
  -->
33
33
 
34
+ ## Table of Contents
35
+
36
+ - [Quick Start](#quick-start)
37
+ - [Example Usage](#example-usage-of-gptdiff)
38
+ - [Basic Usage](#basic-usage)
39
+ - [Simple Command Line Agent Loops](#simple-command-line-agent-loops)
40
+ - [Why Choose GPTDiff?](#why-choose-gptdiff)
41
+ - [Core Capabilities](#core-capabilities)
42
+ - [CLI Excellence](#-cli-excellence)
43
+ - [Magic Diff Generation](#-magic-diff-generation)
44
+ - [Smart Apply System](#-smart-apply-system)
45
+ - [Get Started](#get-started)
46
+ - [Installation](#installation)
47
+ - [Configuration](#configuration)
48
+ - [Command Line Usage](#command-line-usage)
49
+
34
50
  🚀 **Create and apply diffs with AI**
35
51
  Modify your project using plain English.
36
52
 
37
- More documentation at [gptdiff.255labs.xyz](gptdiff.255labs.xyz)
53
+ More documentation at [gptdiff.255labs.xyz](https://gptdiff.255labs.xyz)
54
+
55
+ ## Quick Start
38
56
 
57
+ 1. **Install GPTDiff**
58
+
59
+
39
60
  ### Example Usage of `gptdiff`
40
61
 
41
62
  #### Apply a Patch Directly
42
- ```
43
- bash
63
+ ```bash
44
64
  gptdiff "Add button animations on press" --apply
45
65
  ```
46
66
  ✅ Successfully applied patch
47
67
 
48
68
  #### Generate a Patch File
49
- ```
50
- bash
69
+ ```bash
51
70
  gptdiff "Add API documentation" --call
52
71
  ```
53
72
  🔧 Patch written to `diff.patch`
54
73
 
55
74
  #### Generate a Prompt File Without Calling LLM
56
- ```
57
- bash
75
+ ```bash
58
76
  gptdiff "Improve error messages"
59
77
  ```
60
78
  📄 LLM not called, written to `prompt.txt`
@@ -4,30 +4,48 @@ GPTDiff: Create and apply diffs using AI.
4
4
  This tool leverages natural language instructions to modify project codebases.
5
5
  -->
6
6
 
7
+ ## Table of Contents
8
+
9
+ - [Quick Start](#quick-start)
10
+ - [Example Usage](#example-usage-of-gptdiff)
11
+ - [Basic Usage](#basic-usage)
12
+ - [Simple Command Line Agent Loops](#simple-command-line-agent-loops)
13
+ - [Why Choose GPTDiff?](#why-choose-gptdiff)
14
+ - [Core Capabilities](#core-capabilities)
15
+ - [CLI Excellence](#-cli-excellence)
16
+ - [Magic Diff Generation](#-magic-diff-generation)
17
+ - [Smart Apply System](#-smart-apply-system)
18
+ - [Get Started](#get-started)
19
+ - [Installation](#installation)
20
+ - [Configuration](#configuration)
21
+ - [Command Line Usage](#command-line-usage)
22
+
7
23
  🚀 **Create and apply diffs with AI**
8
24
  Modify your project using plain English.
9
25
 
10
- More documentation at [gptdiff.255labs.xyz](gptdiff.255labs.xyz)
26
+ More documentation at [gptdiff.255labs.xyz](https://gptdiff.255labs.xyz)
27
+
28
+ ## Quick Start
11
29
 
30
+ 1. **Install GPTDiff**
31
+
32
+
12
33
  ### Example Usage of `gptdiff`
13
34
 
14
35
  #### Apply a Patch Directly
15
- ```
16
- bash
36
+ ```bash
17
37
  gptdiff "Add button animations on press" --apply
18
38
  ```
19
39
  ✅ Successfully applied patch
20
40
 
21
41
  #### Generate a Patch File
22
- ```
23
- bash
42
+ ```bash
24
43
  gptdiff "Add API documentation" --call
25
44
  ```
26
45
  🔧 Patch written to `diff.patch`
27
46
 
28
47
  #### Generate a Prompt File Without Calling LLM
29
- ```
30
- bash
48
+ ```bash
31
49
  gptdiff "Improve error messages"
32
50
  ```
33
51
  📄 LLM not called, written to `prompt.txt`
@@ -301,4 +319,4 @@ pip install -e .[test]
301
319
  pytest tests/
302
320
  ```
303
321
 
304
- This will execute all unit tests verifying core diff generation and application logic.
322
+ This will execute all unit tests verifying core diff generation and application logic.
@@ -7,6 +7,7 @@ Contains the function to apply unified git diffs to files on disk.
7
7
  from pathlib import Path
8
8
  import re
9
9
  import hashlib
10
+ from collections import defaultdict
10
11
 
11
12
  def apply_diff(project_dir, diff_text):
12
13
  """
@@ -191,6 +192,12 @@ def parse_diff_per_file(diff_text):
191
192
  Uses 'b/' prefix detection from git diffs to determine target paths
192
193
  This doesn't work all the time and needs to be revised with stronger models
193
194
  """
195
+ def dedup_diffs(diffs):
196
+ groups = defaultdict(list)
197
+ for key, value in diffs:
198
+ groups[key].append(value)
199
+ return [[key, "\n".join(values)] for key, values in groups.items()]
200
+
194
201
  header_re = re.compile(r'^(?:diff --git\s+)?(a/[^ ]+)\s+(b/[^ ]+)\s*$', re.MULTILINE)
195
202
  lines = diff_text.splitlines()
196
203
 
@@ -227,7 +234,7 @@ def parse_diff_per_file(diff_text):
227
234
  if deletion_mode and not any(l.startswith("+++ ") for l in current_lines):
228
235
  current_lines.append("+++ /dev/null")
229
236
  diffs.append((current_file, "\n".join(current_lines)))
230
- return diffs
237
+ return dedup_diffs(diffs)
231
238
  else:
232
239
  # Use header-based strategy.
233
240
  diffs = []
@@ -260,6 +267,6 @@ def parse_diff_per_file(diff_text):
260
267
  if deletion_mode and not any(l.startswith("+++ ") for l in current_lines):
261
268
  current_lines.append("+++ /dev/null")
262
269
  diffs.append((current_file, "\n".join(current_lines)))
263
- return diffs
270
+ return dedup_diffs(diffs)
264
271
 
265
272
 
@@ -20,6 +20,7 @@ from threading import Lock
20
20
  import openai
21
21
  from openai import OpenAI
22
22
  import tiktoken
23
+ import requests
23
24
  import time
24
25
  import os
25
26
  import json
@@ -35,13 +36,14 @@ from ai_agent_toolbox import MarkdownParser, MarkdownPromptFormatter, Toolbox, F
35
36
  from .applydiff import apply_diff, parse_diff_per_file
36
37
 
37
38
  VERBOSE = False
38
- diff_context = contextvars.ContextVar('diffcontent', default="")
39
+ diff_context = contextvars.ContextVar('diffcontent', default=[])
39
40
 
40
41
  def create_diff_toolbox():
41
42
  toolbox = Toolbox()
43
+ diff_context.set([])
42
44
 
43
45
  def diff(content: str):
44
- diff_context.set(content)
46
+ diff_context.set(diff_context.get()+[content])
45
47
  return content
46
48
 
47
49
  toolbox.add_tool(
@@ -218,7 +220,105 @@ def domain_for_url(base_url):
218
220
  domain = base_url
219
221
  return domain
220
222
 
221
- def call_llm_for_diff(system_prompt, user_prompt, files_content, model, temperature=0.7, max_tokens=30000, api_key=None, base_url=None):
223
+ def call_llm(api_key, base_url, model, messages, max_tokens, temperature, budget_tokens=None):
224
+ # Check if we're using Anthropic
225
+ if "api.anthropic.com" in base_url:
226
+ anthropic_url = "https://api.anthropic.com/v1/messages"
227
+
228
+ headers = {
229
+ "x-api-key": api_key,
230
+ "Content-Type": "application/json",
231
+ "anthropic-version": "2023-06-01"
232
+ }
233
+
234
+ # Extract system message if present
235
+ system_message = None
236
+ filtered_messages = []
237
+
238
+ for message in messages:
239
+ if message["role"] == "system":
240
+ system_message = message["content"]
241
+ else:
242
+ filtered_messages.append(message)
243
+
244
+ # Prepare request data
245
+ data = {
246
+ "model": model,
247
+ "messages": filtered_messages,
248
+ "max_tokens": max_tokens,
249
+ "temperature": temperature
250
+ }
251
+
252
+ # Add system message as top-level parameter if found
253
+ if system_message:
254
+ data["system"] = system_message
255
+
256
+ if budget_tokens:
257
+ data["temperature"] = 1
258
+ data["thinking"] = {"budget_tokens": budget_tokens, "type": "enabled"}
259
+
260
+ # Make the API call
261
+ response = requests.post(anthropic_url, headers=headers, json=data)
262
+ response_data = response.json()
263
+
264
+ if 'error' in response_data:
265
+ print(f"Error from Anthropic API: {response_data}")
266
+ return response_data
267
+
268
+ # Format response to match OpenAI structure for compatibility
269
+ class OpenAICompatResponse:
270
+ class Choice:
271
+ class Message:
272
+ def __init__(self, content):
273
+ self.content = content
274
+
275
+ def __init__(self, message):
276
+ self.message = message
277
+
278
+ class Usage:
279
+ def __init__(self, prompt_tokens, completion_tokens, total_tokens):
280
+ self.prompt_tokens = prompt_tokens
281
+ self.completion_tokens = completion_tokens
282
+ self.total_tokens = total_tokens
283
+
284
+ def __init__(self, choices, usage):
285
+ self.choices = choices
286
+ self.usage = usage
287
+
288
+ # Get content from the response
289
+ thinking_items = [item["thinking"] for item in response_data["content"] if item["type"] == "thinking"]
290
+ text_items = [item["text"] for item in response_data["content"] if item["type"] == "text"]
291
+ if not text_items:
292
+ raise ValueError("No 'text' type found in response content")
293
+ text_content = text_items[0]
294
+ if thinking_items:
295
+ wrapped_thinking = f"<think>{thinking_items[0]}</think>"
296
+ message_content = wrapped_thinking + "\n" + text_content
297
+ else:
298
+ message_content = text_content
299
+
300
+ # Extract token usage information
301
+ input_tokens = response_data["usage"]["input_tokens"]
302
+ output_tokens = response_data["usage"]["output_tokens"]
303
+ total_tokens = input_tokens + output_tokens
304
+
305
+ # Create the response object with usage information
306
+ message = OpenAICompatResponse.Choice.Message(message_content)
307
+ choice = OpenAICompatResponse.Choice(message)
308
+ usage = OpenAICompatResponse.Usage(input_tokens, output_tokens, total_tokens)
309
+
310
+ return OpenAICompatResponse([choice], usage)
311
+ else:
312
+ # Use OpenAI client as before
313
+ client = OpenAI(api_key=api_key, base_url=base_url)
314
+ return client.chat.completions.create(
315
+ model=model,
316
+ messages=messages,
317
+ max_tokens=max_tokens,
318
+ temperature=temperature
319
+ )
320
+
321
+ def call_llm_for_diff(system_prompt, user_prompt, files_content, model, temperature=1.0, max_tokens=30000, api_key=None, base_url=None, budget_tokens=None):
222
322
  enc = tiktoken.get_encoding("o200k_base")
223
323
 
224
324
  # Use colors in print statements
@@ -258,13 +358,16 @@ def call_llm_for_diff(system_prompt, user_prompt, files_content, model, temperat
258
358
  if not base_url:
259
359
  base_url = os.getenv('GPTDIFF_LLM_BASE_URL', "https://nano-gpt.com/api/v1/")
260
360
  base_url = base_url or "https://nano-gpt.com/api/v1/"
261
-
262
- client = OpenAI(api_key=api_key, base_url=base_url)
263
- response = client.chat.completions.create(model=model,
361
+
362
+ response = call_llm(
363
+ api_key=api_key,
364
+ base_url=base_url,
365
+ model=model,
264
366
  messages=messages,
265
367
  max_tokens=max_tokens,
266
- temperature=temperature)
267
-
368
+ budget_tokens=budget_tokens,
369
+ temperature=temperature
370
+ )
268
371
  if VERBOSE:
269
372
  print("Debug: Raw LLM Response\n---")
270
373
  print(response.choices[0].message.content.strip())
@@ -294,7 +397,7 @@ def call_llm_for_diff(system_prompt, user_prompt, files_content, model, temperat
294
397
  toolbox.use(event)
295
398
  diff_response = diff_context.get()
296
399
 
297
- return full_response, diff_response, prompt_tokens, completion_tokens, total_tokens
400
+ return full_response, "\n".join(diff_response), prompt_tokens, completion_tokens, total_tokens
298
401
 
299
402
  # New API functions
300
403
  def build_environment(files_dict):
@@ -306,7 +409,7 @@ def build_environment(files_dict):
306
409
  env.append(content)
307
410
  return '\n'.join(env)
308
411
 
309
- def generate_diff(environment, goal, model=None, temperature=0.7, max_tokens=32000, api_key=None, base_url=None, prepend=None):
412
+ def generate_diff(environment, goal, model=None, temperature=1.0, max_tokens=32000, api_key=None, base_url=None, prepend=None, anthropic_budget_tokens=None):
310
413
  """API: Generate a git diff from the environment and goal.
311
414
 
312
415
  If 'prepend' is provided, it should be a path to a file whose content will be
@@ -314,6 +417,10 @@ prepended to the system prompt.
314
417
  """
315
418
  if model is None:
316
419
  model = os.getenv('GPTDIFF_MODEL', 'deepseek-reasoner')
420
+ # Use ANTHROPIC_BUDGET_TOKENS env var if set and no cli override provided
421
+ if anthropic_budget_tokens is None:
422
+ anthropic_budget_tokens = os.getenv('ANTHROPIC_BUDGET_TOKENS')
423
+
317
424
  if prepend:
318
425
  if prepend.startswith("http://") or prepend.startswith("https://"):
319
426
  import urllib.request
@@ -326,15 +433,16 @@ prepended to the system prompt.
326
433
 
327
434
  diff_tag = "```diff"
328
435
  system_prompt = prepend + f"Output a git diff into a \"{diff_tag}\" block."
329
- _, diff_text, _, _, _, _ = call_llm_for_diff(
436
+ _, diff_text, _, _, _ = call_llm_for_diff(
330
437
  system_prompt,
331
438
  goal,
332
439
  environment,
333
- model=model,
440
+ model,
441
+ temperature=temperature,
442
+ max_tokens=max_tokens,
334
443
  api_key=api_key,
335
444
  base_url=base_url,
336
- max_tokens=max_tokens,
337
- temperature=temperature
445
+ budget_tokens=int(anthropic_budget_tokens) if anthropic_budget_tokens is not None else None
338
446
  )
339
447
  return diff_text
340
448
 
@@ -416,11 +524,12 @@ def parse_arguments():
416
524
  parser.add_argument('--call', action='store_true',
417
525
  help='Call the GPT-4 API. Writes the full prompt to prompt.txt if not specified.')
418
526
  parser.add_argument('files', nargs='*', default=[], help='Specify additional files or directories to include.')
419
- parser.add_argument('--temperature', type=float, default=0.7, help='Temperature parameter for model creativity (0.0 to 2.0)')
527
+ parser.add_argument('--temperature', type=float, default=1.0, help='Temperature parameter for model creativity (0.0 to 2.0)')
420
528
  parser.add_argument('--max_tokens', type=int, default=30000, help='Temperature parameter for model creativity (0.0 to 2.0)')
421
529
  parser.add_argument('--model', type=str, default=None, help='Model to use for the API call.')
422
530
  parser.add_argument('--applymodel', type=str, default=None, help='Model to use for applying the diff. Defaults to the value of --model if not specified.')
423
531
  parser.add_argument('--nowarn', action='store_true', help='Disable large token warning')
532
+ parser.add_argument('--anthropic_budget_tokens', type=int, default=None, help='Budget tokens for Anthropic extended thinking')
424
533
  parser.add_argument('--verbose', action='store_true', help='Enable verbose output with detailed information')
425
534
  return parser.parse_args()
426
535
 
@@ -768,20 +877,13 @@ def main():
768
877
  if confirmation != 'y':
769
878
  print("Request canceled")
770
879
  sys.exit(0)
771
- try:
772
- full_text, diff_text, prompt_tokens, completion_tokens, total_tokens = call_llm_for_diff(system_prompt, user_prompt, files_content, args.model,
773
- temperature=args.temperature,
774
- api_key=os.getenv('GPTDIFF_LLM_API_KEY'),
775
- base_url=os.getenv('GPTDIFF_LLM_BASE_URL', "https://nano-gpt.com/api/v1/"),
776
- max_tokens=args.max_tokens
777
- )
778
- except Exception as e:
779
- full_text = f"{e}"
780
- diff_text = ""
781
- prompt_tokens = 0
782
- completion_tokens = 0
783
- total_tokens = 0
784
- print(f"Error in LLM response {e}")
880
+ full_text, diff_text, prompt_tokens, completion_tokens, total_tokens = call_llm_for_diff(system_prompt, user_prompt, files_content, args.model,
881
+ temperature=args.temperature,
882
+ api_key=os.getenv('GPTDIFF_LLM_API_KEY'),
883
+ base_url=os.getenv('GPTDIFF_LLM_BASE_URL', "https://nano-gpt.com/api/v1/"),
884
+ max_tokens=args.max_tokens,
885
+ budget_tokens=args.anthropic_budget_tokens
886
+ )
785
887
 
786
888
  if(diff_text.strip() == ""):
787
889
  print(f"\033[1;33mWarning: No valid diff data was generated. This could be due to an unclear prompt or an invalid LLM response.\033[0m")
@@ -831,20 +933,17 @@ def swallow_reasoning(full_response: str) -> (str, str):
831
933
  r"(?P<reasoning>>\s*Reasoning.*?Reasoned.*?seconds)",
832
934
  re.DOTALL
833
935
  )
834
- match = pattern.search(full_response)
835
- if match:
936
+ reasoning_list = []
937
+ def replacer(match):
836
938
  raw_reasoning = match.group("reasoning")
837
- # Remove any leading '+' characters and extra whitespace from each line
838
939
  reasoning_lines = [line.lstrip('+').strip() for line in raw_reasoning.splitlines()]
839
940
  reasoning = "\n".join(reasoning_lines).strip()
840
-
841
- # Remove the reasoning block from the response using its exact span
842
- final_content = full_response[:match.start()] + full_response[match.end():]
843
- final_content = final_content.strip()
844
- else:
845
- reasoning = ""
846
- final_content = full_response.strip()
847
- return final_content, reasoning
941
+ reasoning_list.append(reasoning)
942
+ return ""
943
+
944
+ final_content = re.sub(pattern, replacer, full_response)
945
+ reasoning = "\n".join(reasoning_list)
946
+ return final_content.strip(), reasoning
848
947
 
849
948
  def strip_bad_output(updated: str, original: str) -> str:
850
949
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: gptdiff
3
- Version: 0.1.28
3
+ Version: 0.1.31
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
@@ -31,30 +31,48 @@ GPTDiff: Create and apply diffs using AI.
31
31
  This tool leverages natural language instructions to modify project codebases.
32
32
  -->
33
33
 
34
+ ## Table of Contents
35
+
36
+ - [Quick Start](#quick-start)
37
+ - [Example Usage](#example-usage-of-gptdiff)
38
+ - [Basic Usage](#basic-usage)
39
+ - [Simple Command Line Agent Loops](#simple-command-line-agent-loops)
40
+ - [Why Choose GPTDiff?](#why-choose-gptdiff)
41
+ - [Core Capabilities](#core-capabilities)
42
+ - [CLI Excellence](#-cli-excellence)
43
+ - [Magic Diff Generation](#-magic-diff-generation)
44
+ - [Smart Apply System](#-smart-apply-system)
45
+ - [Get Started](#get-started)
46
+ - [Installation](#installation)
47
+ - [Configuration](#configuration)
48
+ - [Command Line Usage](#command-line-usage)
49
+
34
50
  🚀 **Create and apply diffs with AI**
35
51
  Modify your project using plain English.
36
52
 
37
- More documentation at [gptdiff.255labs.xyz](gptdiff.255labs.xyz)
53
+ More documentation at [gptdiff.255labs.xyz](https://gptdiff.255labs.xyz)
54
+
55
+ ## Quick Start
38
56
 
57
+ 1. **Install GPTDiff**
58
+
59
+
39
60
  ### Example Usage of `gptdiff`
40
61
 
41
62
  #### Apply a Patch Directly
42
- ```
43
- bash
63
+ ```bash
44
64
  gptdiff "Add button animations on press" --apply
45
65
  ```
46
66
  ✅ Successfully applied patch
47
67
 
48
68
  #### Generate a Patch File
49
- ```
50
- bash
69
+ ```bash
51
70
  gptdiff "Add API documentation" --call
52
71
  ```
53
72
  🔧 Patch written to `diff.patch`
54
73
 
55
74
  #### Generate a Prompt File Without Calling LLM
56
- ```
57
- bash
75
+ ```bash
58
76
  gptdiff "Improve error messages"
59
77
  ```
60
78
  📄 LLM not called, written to `prompt.txt`
@@ -15,6 +15,7 @@ tests/test_applydiff.py
15
15
  tests/test_applydiff_edgecases.py
16
16
  tests/test_diff_parse.py
17
17
  tests/test_failing_case.py
18
+ tests/test_multidiff.py
18
19
  tests/test_parse_diff_per_file.py
19
20
  tests/test_smartapply.py
20
21
  tests/test_strip_bad_ouput.py
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name='gptdiff',
5
- version='0.1.28',
5
+ version='0.1.31',
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
@@ -0,0 +1,50 @@
1
+ import pytest
2
+ from gptdiff.gptdiff import generate_diff
3
+
4
+ # Dummy classes to simulate an LLM response
5
+ class DummyMessage:
6
+ def __init__(self, content):
7
+ self.content = content
8
+
9
+ class DummyChoice:
10
+ def __init__(self, content):
11
+ self.message = DummyMessage(content)
12
+
13
+ class DummyUsage:
14
+ def __init__(self, prompt_tokens, completion_tokens, total_tokens):
15
+ self.prompt_tokens = prompt_tokens
16
+ self.completion_tokens = completion_tokens
17
+ self.total_tokens = total_tokens
18
+
19
+ class DummyResponse:
20
+ def __init__(self, content, prompt_tokens, completion_tokens, total_tokens):
21
+ self.choices = [DummyChoice(content)]
22
+ self.usage = DummyUsage(prompt_tokens, completion_tokens, total_tokens)
23
+
24
+ def test_fail_diff_through_call_llm(monkeypatch):
25
+ diff_str = """```diff
26
+ DIFF 1
27
+ ```
28
+
29
+ Some text here
30
+ ```diff
31
+ DIFF 2
32
+ ```"""
33
+
34
+ expected = """
35
+ DIFF 1
36
+
37
+ DIFF 2"""
38
+
39
+
40
+ # Define a dummy call_llm function that returns our fake response
41
+ def dummy_call_llm(api_key, base_url, model, messages, max_tokens, budget_tokens, temperature):
42
+ return DummyResponse(diff_str, prompt_tokens=10, completion_tokens=20, total_tokens=30)
43
+
44
+ # Patch call_llm in the gptdiff module with our dummy function.
45
+ monkeypatch.setattr("gptdiff.gptdiff.call_llm", dummy_call_llm)
46
+
47
+ # generate_diff calls call_llm_for_diff internally, which now uses our dummy_call_llm.
48
+ result = generate_diff("dummy environment", "dummy goal", model="test-model")
49
+
50
+ assert result.strip() == expected.strip()
@@ -126,6 +126,34 @@ diff --git a/file2.py b/file2.py
126
126
  self.assertIn("file1.py", paths)
127
127
  self.assertIn("file2.py", paths)
128
128
 
129
+
130
+ def test_multiple_files(self):
131
+ diff_text = """diff --git a/file1.py b/file1.py
132
+ --- a/file1.py
133
+ +++ b/file1.py
134
+ @@ -1 +1 @@
135
+ -print("Hello")
136
+ +print("Hi")
137
+ diff --git a/file2.py b/file2.py
138
+ --- a/file2.py
139
+ +++ b/file2.py
140
+ @@ -1 +1 @@
141
+ -print("World")
142
+ +print("Earth")
143
+ diff --git a/file1.py b/file1.py
144
+ --- a/file1.py
145
+ +++ b/file1.py
146
+ @@ -3 +3 @@
147
+ -print("Hello2")
148
+ +print("Hi2")
149
+ """
150
+ result = parse_diff_per_file(diff_text)
151
+ self.assertEqual(len(result), 2)
152
+ paths = [fp for fp, _ in result]
153
+ self.assertIn("file1.py", paths)
154
+ self.assertIn("file2.py", paths)
155
+ assert("Hi2" in result[0][1])
156
+
129
157
  def test_parse_diff_per_file_unconventional_header():
130
158
  diff_text = """--- game.js
131
159
  +++ game.js
@@ -2,7 +2,7 @@ import pytest
2
2
 
3
3
  from gptdiff.gptdiff import swallow_reasoning
4
4
 
5
- def test_swallow_reasoning_extraction():
5
+ def test_swallow_reasoning_extraction_simple():
6
6
  llm_response = (
7
7
  "+> Reasoning\n"
8
8
  "+None\n"
@@ -13,19 +13,39 @@ def test_swallow_reasoning_extraction():
13
13
  final_content, reasoning = swallow_reasoning(llm_response)
14
14
  expected_reasoning = (
15
15
  "> Reasoning\n"
16
- "**Applying the diff**\n"
17
- "I'm piecing together how to efficiently apply a diff to a file...\n"
18
- "**Returning the result**\n"
19
- "I'm finalizing the method to apply the diff updates...\n"
20
- "Reasoned for 6 seconds"
16
+ "None\n"
17
+ "Reasoned about summary drawer button 변경 for 15 seconds"
21
18
  )
22
19
  assert reasoning == expected_reasoning
23
20
  # The final content should no longer contain the reasoning block.
24
21
  assert expected_reasoning not in final_content
25
- # And it should contain the diff block.
26
- assert "```diff" in final_content
27
22
 
28
23
 
24
+ def test_swallow_reasoning_extraction_multiline():
25
+ llm_response = (
26
+ "line 1> Reasoning\n"
27
+ "+None\n"
28
+ "+Reasoned about summary drawer button 변경 for 1 seconds\n"
29
+ "line 2\n"
30
+ " > Reasoning\n"
31
+ "+None\n"
32
+ "+Reasoned about summary drawer button 변경 for 2 seconds\n"
33
+ "line 3:"
34
+ )
35
+ final_content, reasoning = swallow_reasoning(llm_response)
36
+ expected_reasoning = (
37
+ "> Reasoning\n"
38
+ "None\n"
39
+ "Reasoned about summary drawer button 변경 for 1 seconds\n"
40
+ "> Reasoning\n"
41
+ "None\n"
42
+ "Reasoned about summary drawer button 변경 for 2 seconds"
43
+ )
44
+ assert reasoning == expected_reasoning
45
+ assert "line 1\nline 2\n \nline 3:" == final_content
46
+ # The final content should no longer contain the reasoning block.
47
+ assert expected_reasoning not in final_content
48
+
29
49
  def test_swallow_reasoning_with_untested_response():
30
50
  llm_response = (
31
51
  "> Reasoning\n"
File without changes
File without changes
File without changes
File without changes