gptdiff 0.1.27__py3-none-any.whl → 0.1.30__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
@@ -326,15 +429,16 @@ prepended to the system prompt.
326
429
 
327
430
  diff_tag = "```diff"
328
431
  system_prompt = prepend + f"Output a git diff into a \"{diff_tag}\" block."
329
- _, diff_text, _, _, _, _ = call_llm_for_diff(
432
+ _, diff_text, _, _, _ = call_llm_for_diff(
330
433
  system_prompt,
331
434
  goal,
332
435
  environment,
333
- model=model,
436
+ model,
437
+ temperature=temperature,
438
+ max_tokens=max_tokens,
334
439
  api_key=api_key,
335
440
  base_url=base_url,
336
- max_tokens=max_tokens,
337
- temperature=temperature
441
+ budget_tokens=anthropic_budget_tokens
338
442
  )
339
443
  return diff_text
340
444
 
@@ -416,11 +520,12 @@ def parse_arguments():
416
520
  parser.add_argument('--call', action='store_true',
417
521
  help='Call the GPT-4 API. Writes the full prompt to prompt.txt if not specified.')
418
522
  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)')
523
+ parser.add_argument('--temperature', type=float, default=1.0, help='Temperature parameter for model creativity (0.0 to 2.0)')
420
524
  parser.add_argument('--max_tokens', type=int, default=30000, help='Temperature parameter for model creativity (0.0 to 2.0)')
421
525
  parser.add_argument('--model', type=str, default=None, help='Model to use for the API call.')
422
526
  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
527
  parser.add_argument('--nowarn', action='store_true', help='Disable large token warning')
528
+ parser.add_argument('--anthropic_budget_tokens', type=int, default=None, help='Budget tokens for Anthropic extended thinking')
424
529
  parser.add_argument('--verbose', action='store_true', help='Enable verbose output with detailed information')
425
530
  return parser.parse_args()
426
531
 
@@ -613,7 +718,7 @@ def smart_apply_patch(project_dir, diff_text, user_prompt, args):
613
718
  else:
614
719
  base_url = os.getenv("GPTDIFF_LLM_BASE_URL", "https://nano-gpt.com/api/v1/")
615
720
 
616
- print(f"Running smartapply in parallel using model '{green}{model}{reset}' from '{blue}{domain_for_url(base_url)}{reset}'...")
721
+ print(f"Running smartapply in parallel for '{file_path}' using model '{green}{model}{reset}' from '{blue}{domain_for_url(base_url)}{reset}'...")
617
722
  try:
618
723
  updated_content = call_llm_for_apply_with_think_tool_available(
619
724
  file_path, original_content, file_diff, model,
@@ -768,20 +873,13 @@ def main():
768
873
  if confirmation != 'y':
769
874
  print("Request canceled")
770
875
  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}")
876
+ full_text, diff_text, prompt_tokens, completion_tokens, total_tokens = call_llm_for_diff(system_prompt, user_prompt, files_content, args.model,
877
+ temperature=args.temperature,
878
+ api_key=os.getenv('GPTDIFF_LLM_API_KEY'),
879
+ base_url=os.getenv('GPTDIFF_LLM_BASE_URL', "https://nano-gpt.com/api/v1/"),
880
+ max_tokens=args.max_tokens,
881
+ budget_tokens=args.anthropic_budget_tokens
882
+ )
785
883
 
786
884
  if(diff_text.strip() == ""):
787
885
  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")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: gptdiff
3
- Version: 0.1.27
3
+ Version: 0.1.30
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
@@ -0,0 +1,10 @@
1
+ gptdiff/__init__.py,sha256=o1hrK4GFvbfKcHPlLVArz4OunE3euIicEBYaLrdDo0k,198
2
+ gptdiff/applydiff.py,sha256=FMLkEtNnOdnU7c8LgYafGuROsDz3utpyXKJVFRs_BvI,11473
3
+ gptdiff/gptdiff.py,sha256=i2CYCzfIGn5knvNA7Q5mc4EpGR9akka2l3-aMphK0eU,37362
4
+ gptdiff/gptpatch.py,sha256=Vqk2vliYs_BxtuTpwdS88n3A8XToh6RvrCA4N8VqOu0,2759
5
+ gptdiff-0.1.30.dist-info/LICENSE.txt,sha256=zCJk7yUYpMjFvlipi1dKtaljF8WdZ2NASndBYYbU8BY,1228
6
+ gptdiff-0.1.30.dist-info/METADATA,sha256=6pyrzVyjWTsHWggNr7LkjcGefR24P11oXD9w3w_sZFI,8723
7
+ gptdiff-0.1.30.dist-info/WHEEL,sha256=jB7zZ3N9hIM9adW7qlTAyycLYW9npaWKLRzaoVcLKcM,91
8
+ gptdiff-0.1.30.dist-info/entry_points.txt,sha256=0VlVNr-gc04a3SZD5_qKIBbtg_L5P2x3xlKE5ftcdkc,82
9
+ gptdiff-0.1.30.dist-info/top_level.txt,sha256=XNkQkQGINaDndEwRxg8qToOrJ9coyfAb-EHrSUXzdCE,8
10
+ gptdiff-0.1.30.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.8.0)
2
+ Generator: setuptools (75.8.2)
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=sG0tPku3d5agx9F8EqKTl914S5ZkRi_8oMIBhFiz-nI,33355
4
- gptdiff/gptpatch.py,sha256=Vqk2vliYs_BxtuTpwdS88n3A8XToh6RvrCA4N8VqOu0,2759
5
- gptdiff-0.1.27.dist-info/LICENSE.txt,sha256=zCJk7yUYpMjFvlipi1dKtaljF8WdZ2NASndBYYbU8BY,1228
6
- gptdiff-0.1.27.dist-info/METADATA,sha256=l5oPGbwCn731KeLRT3xEMElWuE1fr15c_pGLGrEzzA8,8723
7
- gptdiff-0.1.27.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
8
- gptdiff-0.1.27.dist-info/entry_points.txt,sha256=0VlVNr-gc04a3SZD5_qKIBbtg_L5P2x3xlKE5ftcdkc,82
9
- gptdiff-0.1.27.dist-info/top_level.txt,sha256=XNkQkQGINaDndEwRxg8qToOrJ9coyfAb-EHrSUXzdCE,8
10
- gptdiff-0.1.27.dist-info/RECORD,,