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 +9 -2
- gptdiff/gptdiff.py +128 -30
- {gptdiff-0.1.27.dist-info → gptdiff-0.1.30.dist-info}/METADATA +1 -1
- gptdiff-0.1.30.dist-info/RECORD +10 -0
- {gptdiff-0.1.27.dist-info → gptdiff-0.1.30.dist-info}/WHEEL +1 -1
- gptdiff-0.1.27.dist-info/RECORD +0 -10
- {gptdiff-0.1.27.dist-info → gptdiff-0.1.30.dist-info}/LICENSE.txt +0 -0
- {gptdiff-0.1.27.dist-info → gptdiff-0.1.30.dist-info}/entry_points.txt +0 -0
- {gptdiff-0.1.27.dist-info → gptdiff-0.1.30.dist-info}/top_level.txt +0 -0
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
|
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
|
-
|
263
|
-
|
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
|
-
|
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
|
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, _, _, _
|
432
|
+
_, diff_text, _, _, _ = call_llm_for_diff(
|
330
433
|
system_prompt,
|
331
434
|
goal,
|
332
435
|
environment,
|
333
|
-
model
|
436
|
+
model,
|
437
|
+
temperature=temperature,
|
438
|
+
max_tokens=max_tokens,
|
334
439
|
api_key=api_key,
|
335
440
|
base_url=base_url,
|
336
|
-
|
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
|
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
|
-
|
772
|
-
|
773
|
-
|
774
|
-
|
775
|
-
|
776
|
-
|
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")
|
@@ -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,,
|
gptdiff-0.1.27.dist-info/RECORD
DELETED
@@ -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,,
|
File without changes
|
File without changes
|
File without changes
|