gptdiff 0.1.28__py3-none-any.whl → 0.1.31__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/applydiff.py CHANGED
@@ -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
 
gptdiff/gptdiff.py CHANGED
@@ -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`
@@ -0,0 +1,10 @@
1
+ gptdiff/__init__.py,sha256=o1hrK4GFvbfKcHPlLVArz4OunE3euIicEBYaLrdDo0k,198
2
+ gptdiff/applydiff.py,sha256=FMLkEtNnOdnU7c8LgYafGuROsDz3utpyXKJVFRs_BvI,11473
3
+ gptdiff/gptdiff.py,sha256=EC2PpES2NchJvcTtNo0U0CfKLH9DdV86OuzOzLCyTr8,37424
4
+ gptdiff/gptpatch.py,sha256=Vqk2vliYs_BxtuTpwdS88n3A8XToh6RvrCA4N8VqOu0,2759
5
+ gptdiff-0.1.31.dist-info/LICENSE.txt,sha256=zCJk7yUYpMjFvlipi1dKtaljF8WdZ2NASndBYYbU8BY,1228
6
+ gptdiff-0.1.31.dist-info/METADATA,sha256=YPye_cME35OcmzklE__dMLncBYY9bhdjKf7j9wczChU,9343
7
+ gptdiff-0.1.31.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
8
+ gptdiff-0.1.31.dist-info/entry_points.txt,sha256=0VlVNr-gc04a3SZD5_qKIBbtg_L5P2x3xlKE5ftcdkc,82
9
+ gptdiff-0.1.31.dist-info/top_level.txt,sha256=XNkQkQGINaDndEwRxg8qToOrJ9coyfAb-EHrSUXzdCE,8
10
+ gptdiff-0.1.31.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.8.0)
2
+ Generator: setuptools (76.0.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,10 +0,0 @@
1
- gptdiff/__init__.py,sha256=o1hrK4GFvbfKcHPlLVArz4OunE3euIicEBYaLrdDo0k,198
2
- gptdiff/applydiff.py,sha256=_11ITFMcigwvVptaIpEtyfLUTIy_mYPWExcXUqCBfOs,11200
3
- gptdiff/gptdiff.py,sha256=Lf3utjFM0T-39YyfMXUHZmVzukJk1DxR9941KNcYyIs,33373
4
- gptdiff/gptpatch.py,sha256=Vqk2vliYs_BxtuTpwdS88n3A8XToh6RvrCA4N8VqOu0,2759
5
- gptdiff-0.1.28.dist-info/LICENSE.txt,sha256=zCJk7yUYpMjFvlipi1dKtaljF8WdZ2NASndBYYbU8BY,1228
6
- gptdiff-0.1.28.dist-info/METADATA,sha256=iqKGOSe6vN62loiZ6vA2UZG-fAE80I_ZxHjMHs8CC_E,8723
7
- gptdiff-0.1.28.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
8
- gptdiff-0.1.28.dist-info/entry_points.txt,sha256=0VlVNr-gc04a3SZD5_qKIBbtg_L5P2x3xlKE5ftcdkc,82
9
- gptdiff-0.1.28.dist-info/top_level.txt,sha256=XNkQkQGINaDndEwRxg8qToOrJ9coyfAb-EHrSUXzdCE,8
10
- gptdiff-0.1.28.dist-info/RECORD,,