gptdiff 0.1.0__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/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .gptdiff import generate_diff, smartapply
2
+
3
+ __all__ = ['generate_diff', 'smartapply']
gptdiff/gptdiff.py ADDED
@@ -0,0 +1,604 @@
1
+ #!/usr/bin/env python3
2
+
3
+ import openai
4
+ from openai import OpenAI
5
+
6
+ import tiktoken
7
+
8
+ import os
9
+ import json
10
+ import subprocess
11
+ from pathlib import Path
12
+ import sys
13
+ import fnmatch
14
+ import argparse
15
+ import pkgutil
16
+ import re
17
+ import contextvars
18
+ from ai_agent_toolbox import FlatXMLParser, FlatXMLPromptFormatter, Toolbox
19
+ import threading
20
+ from pkgutil import get_data
21
+
22
+ diff_context = contextvars.ContextVar('diffcontent', default="")
23
+ def create_toolbox():
24
+ toolbox = Toolbox()
25
+
26
+ def diff(content: str):
27
+ diff_context.set(content)
28
+ return content
29
+
30
+ toolbox.add_tool(
31
+ name="diff",
32
+ fn=diff,
33
+ args={
34
+ "content": {
35
+ "type": "string",
36
+ "description": "Complete diff."
37
+ }
38
+ },
39
+ description="Save the calculated diff as used in 'git apply'"
40
+ )
41
+ return toolbox
42
+
43
+
44
+ def load_gitignore_patterns(gitignore_path):
45
+ with open(gitignore_path, 'r') as f:
46
+ patterns = [line.strip() for line in f if line.strip() and not line.startswith('#')]
47
+ return patterns
48
+
49
+ def is_ignored(filepath, gitignore_patterns):
50
+ filepath = Path(filepath).resolve()
51
+ ignored = False
52
+
53
+ for pattern in gitignore_patterns:
54
+ if pattern.startswith('!'):
55
+ negated_pattern = pattern[1:]
56
+ if fnmatch.fnmatch(str(filepath), negated_pattern) or fnmatch.fnmatch(str(filepath.relative_to(Path.cwd())), negated_pattern):
57
+ ignored = False
58
+ else:
59
+ relative_path = str(filepath.relative_to(Path.cwd()))
60
+ if fnmatch.fnmatch(str(filepath), pattern) or fnmatch.fnmatch(relative_path, pattern):
61
+ ignored = True
62
+ break
63
+ if pattern in relative_path:
64
+ ignored = True
65
+ break
66
+
67
+ # Ensure .gitignore itself is not ignored unless explicitly mentioned
68
+ if filepath.name == ".gitignore" and not any(pattern == ".gitignore" for pattern in gitignore_patterns):
69
+ ignored = False
70
+
71
+ return ignored
72
+
73
+ def list_files_and_dirs(path, ignore_list=None):
74
+ if ignore_list is None:
75
+ ignore_list = []
76
+
77
+ result = []
78
+
79
+ # List all items in the current directory
80
+ for item in os.listdir(path):
81
+ item_path = os.path.join(path, item)
82
+
83
+ if is_ignored(item_path, ignore_list):
84
+ continue
85
+
86
+ # Add the item to the result list
87
+ result.append(item_path)
88
+
89
+ # If it's a directory, recurse into it
90
+ if os.path.isdir(item_path):
91
+ result.extend(list_files_and_dirs(item_path, ignore_list))
92
+
93
+ return result
94
+
95
+ # Function to load project files considering .gitignore
96
+ def load_project_files(project_dir, cwd):
97
+ """Load project files while respecting .gitignore and .gptignore rules.
98
+
99
+ Recursively scans directories, skipping:
100
+ - Files/directories matching patterns in .gitignore/.gptignore
101
+ - Binary files that can't be decoded as UTF-8 text
102
+
103
+ Args:
104
+ project_dir: Root directory to scan for files
105
+ cwd: Base directory for resolving ignore files
106
+
107
+ Returns:
108
+ List of (absolute_path, file_content) tuples
109
+
110
+ Note:
111
+ Prints skipped files to stdout for visibility
112
+ """
113
+ ignore_paths = [Path(cwd) / ".gitignore", Path(cwd) / ".gptignore"]
114
+ gitignore_patterns = [".gitignore", "diff.patch", "prompt.txt", ".gptignore", "*.pdf", "*.docx", ".git", "*.orig", "*.rej"]
115
+
116
+ for p in ignore_paths:
117
+ if p.exists():
118
+ with open(p, 'r') as f:
119
+ gitignore_patterns.extend([line.strip() for line in f if line.strip() and not line.startswith('#')])
120
+
121
+ project_files = []
122
+ for file in list_files_and_dirs(project_dir, gitignore_patterns):
123
+ if os.path.isfile(file):
124
+ try:
125
+ with open(file, 'r') as f:
126
+ content = f.read()
127
+ print(file)
128
+ project_files.append((file, content))
129
+ except UnicodeDecodeError:
130
+ print(f"Skipping file {file} due to UnicodeDecodeError")
131
+ continue
132
+
133
+ print("")
134
+ return project_files
135
+
136
+ def load_prepend_file(file):
137
+ with open(file, 'r') as f:
138
+ return f.read()
139
+
140
+ # Function to call GPT-4 API and calculate the cost
141
+ def call_gpt4_api(system_prompt, user_prompt, files_content, model, temperature=0.7, max_tokens=2500, api_key=None, base_url=None):
142
+
143
+ parser = FlatXMLParser("diff")
144
+ formatter = FlatXMLPromptFormatter(tag="diff")
145
+ toolbox = create_toolbox()
146
+ tool_prompt = formatter.usage_prompt(toolbox)
147
+ system_prompt += "\n"+tool_prompt
148
+
149
+ if model == "gemini-2.0-flash-thinking-exp-01-21":
150
+ user_prompt = system_prompt+"\n"+user_prompt
151
+
152
+ messages = [
153
+ {"role": "system", "content": system_prompt},
154
+ {"role": "user", "content": user_prompt + "\n"+files_content},
155
+ ]
156
+ print("Using", model)
157
+ print("SYSTEM PROMPT")
158
+ print(system_prompt)
159
+ print("USER PROMPT")
160
+ print(user_prompt, "+", len(files_content), "characters of file content")
161
+
162
+ if api_key is None:
163
+ api_key = os.getenv('NANOGPT_API_KEY')
164
+ if base_url is None:
165
+ base_url = os.getenv('NANOGPT_BASE_URL', "https://nano-gpt.com/api/v1/")
166
+ client = OpenAI(api_key=api_key, base_url=base_url)
167
+ response = client.chat.completions.create(model=model,
168
+ messages=messages,
169
+ max_tokens=max_tokens,
170
+ temperature=temperature)
171
+
172
+ prompt_tokens = response.usage.prompt_tokens
173
+ completion_tokens = response.usage.completion_tokens
174
+ total_tokens = response.usage.total_tokens
175
+
176
+ # Now, these rates are updated to per million tokens
177
+ cost_per_million_prompt_tokens = 30
178
+ cost_per_million_completion_tokens = 60
179
+ cost = (prompt_tokens / 1_000_000 * cost_per_million_prompt_tokens) + (completion_tokens / 1_000_000 * cost_per_million_completion_tokens)
180
+
181
+ full_response = response.choices[0].message.content.strip()
182
+
183
+
184
+ events = parser.parse(full_response)
185
+ for event in events:
186
+ toolbox.use(event)
187
+ diff_response = diff_context.get()
188
+
189
+ return full_response, diff_response, prompt_tokens, completion_tokens, total_tokens, cost
190
+
191
+ # New API functions
192
+ def build_environment(files_dict):
193
+ """Rebuild environment string from file dictionary"""
194
+ env = []
195
+ for path, content in files_dict.items():
196
+ env.append(f"File: {path}")
197
+ env.append("Content:")
198
+ env.append(content)
199
+ return '\n'.join(env)
200
+
201
+ def generate_diff(environment, goal, model='deepseek-reasoner', temperature=0.7, max_tokens=32000, api_key=None, base_url=None, prepend=None):
202
+ """API: Generate diff from environment and goal"""
203
+ if prepend:
204
+ prepend = load_prepend_file(args.prepend)
205
+ print("Including prepend",len(enc.encode(json.dumps(prepend))), "tokens")
206
+ else:
207
+ prepend = ""
208
+
209
+ system_prompt = f"Output a git diff into a <diff> block."
210
+ _, diff_text, _, _, _, _ = call_gpt4_api(
211
+ system_prompt,
212
+ goal,
213
+ environment,
214
+ model=model,
215
+ api_key=api_key,
216
+ base_url=base_url,
217
+ max_tokens=max_tokens,
218
+ temperature=temperature
219
+ )
220
+ return diff_text
221
+
222
+ def smartapply(diff_text, files, model='deepseek-reasoner', api_key=None, base_url=None):
223
+ """Applies unified diffs to file contents with AI-powered conflict resolution.
224
+
225
+ Key features:
226
+ - Handles file creations, modifications, and deletions
227
+ - Maintains idempotency - reapplying same diff produces same result
228
+ - Uses LLM to resolve ambiguous changes while preserving context
229
+ - Returns new files dictionary without modifying input
230
+
231
+ Args:
232
+ diff_text: Unified diff string compatible with git apply
233
+ files: Dictionary of {file_path: content} to modify
234
+ model: LLM to use for conflict resolution (default: deepseek-reasoner)
235
+ api_key: Optional API key override
236
+ base_url: Optional API base URL override
237
+
238
+ Returns:
239
+ New dictionary with updated file contents. Deleted files are omitted.
240
+
241
+ Raises:
242
+ APIError: If LLM API calls fail
243
+
244
+ Example:
245
+ >>> original = {"file.py": "def old():\n pass"}
246
+ >>> diff = '''diff --git a/file.py b/file.py
247
+ ... --- a/file.py
248
+ ... +++ b/file.py
249
+ ... @@ -1,2 +1,2 @@
250
+ ... -def old():
251
+ ... +def new():'''
252
+ >>> updated = smartapply(diff, original)
253
+ >>> print(updated["file.py"])
254
+ def new():
255
+ pass
256
+ """
257
+ parsed_diffs = parse_diff_per_file(diff_text)
258
+ print("SMARTAPPLY", diff_text)
259
+
260
+ def process_file(path, patch):
261
+ original = files.get(path, '')
262
+ # Handle file deletions
263
+ if '+++ /dev/null' in patch:
264
+ if path in files:
265
+ del files[path]
266
+ else:
267
+ updated = call_llm_for_apply(path, original, patch, model, api_key=api_key, base_url=base_url)
268
+ files[path] = updated.strip()
269
+
270
+ for path, patch in parsed_diffs:
271
+ process_file(path, patch)
272
+
273
+ return files
274
+
275
+ # Function to apply diff to project files
276
+ def apply_diff(project_dir, diff_text):
277
+ diff_file = Path(project_dir) / "diff.patch"
278
+ with open(diff_file, 'w') as f:
279
+ f.write(diff_text)
280
+
281
+ return False
282
+ result = subprocess.run(["patch", "-p1", "--remove-empty-files", "--input", str(diff_file)], cwd=project_dir, capture_output=True, text=True)
283
+ if result.returncode != 0:
284
+ return False
285
+ else:
286
+ return True
287
+
288
+ def parse_arguments():
289
+ parser = argparse.ArgumentParser(description='Generate and optionally apply git diffs using GPT-4.')
290
+ parser.add_argument('prompt', type=str, help='Prompt that runs on the codebase.')
291
+ parser.add_argument('--apply', action='store_true', help='Attempt to apply the generated git diff. Uses smartapply if applying the patch fails.')
292
+ parser.add_argument('--prepend', type=str, default=None, help='Path to content prepended to system prompt')
293
+
294
+ parser.add_argument('--nobeep', action='store_false', dest='beep', default=True, help='Disable completion notification beep')
295
+ # New flag --prompt that does not call the API but instead writes the full prompt to prompt.txt
296
+ parser.add_argument('--call', action='store_true',
297
+ help='Call the GPT-4 API. Writes the full prompt to prompt.txt if not specified.')
298
+ parser.add_argument('files', nargs='*', default=[], help='Specify additional files or directories to include.')
299
+ parser.add_argument('--temperature', type=float, default=0.7, help='Temperature parameter for model creativity (0.0 to 2.0)')
300
+ parser.add_argument('--model', type=str, default='deepseek-reasoner', help='Model to use for the API call.')
301
+
302
+ parser.add_argument('--nowarn', action='store_true', help='Disable large token warning')
303
+
304
+ return parser.parse_args()
305
+
306
+ def absolute_to_relative(absolute_path):
307
+ cwd = os.getcwd()
308
+ relative_path = os.path.relpath(absolute_path, cwd)
309
+ return relative_path
310
+
311
+ def parse_diff_per_file(diff_text):
312
+ """Parse unified diff text into individual file patches.
313
+
314
+ Splits a multi-file diff into per-file entries for processing. Handles:
315
+ - File creations (+++ /dev/null)
316
+ - File deletions (--- /dev/null)
317
+ - Standard modifications
318
+
319
+ Args:
320
+ diff_text: Unified diff string as generated by `git diff`
321
+
322
+ Returns:
323
+ List of tuples (file_path, patch) where:
324
+ - file_path: Relative path to modified file
325
+ - patch: Full diff fragment for this file
326
+
327
+ Note:
328
+ Uses 'b/' prefix detection from git diffs to determine target paths
329
+ """
330
+ diffs = []
331
+ file_path = None
332
+ current_diff = []
333
+ from_path = None
334
+
335
+ for line in diff_text.split('\n'):
336
+ if line.startswith('diff --git'):
337
+ if current_diff and file_path is not None:
338
+ diffs.append((file_path, '\n'.join(current_diff)))
339
+ current_diff = [line]
340
+ file_path = None
341
+ from_path = None
342
+ parts = line.split()
343
+ if len(parts) >= 4:
344
+ b_path = parts[3]
345
+ file_path = b_path[2:] if b_path.startswith('b/') else b_path
346
+ else:
347
+ current_diff.append(line)
348
+ if line.startswith('--- '):
349
+ from_path = line[4:].strip()
350
+ elif line.startswith('+++ '):
351
+ to_path = line[4:].strip()
352
+ if to_path == '/dev/null':
353
+ if from_path:
354
+ # For deletions, use from_path after stripping 'a/' prefix
355
+ file_path = from_path[2:] if from_path.startswith('a/') else from_path
356
+ else:
357
+ # For normal cases, use to_path after stripping 'b/' prefix
358
+ file_path = to_path[2:] if to_path.startswith('b/') else to_path
359
+
360
+ # Handle remaining diff content after loop
361
+ if current_diff and file_path is not None:
362
+ diffs.append((file_path, '\n'.join(current_diff)))
363
+
364
+ return diffs
365
+
366
+ def call_llm_for_apply(file_path, original_content, file_diff, model, api_key=None, base_url=None):
367
+ """AI-powered diff application with conflict resolution.
368
+
369
+ Internal workhorse for smartapply that handles individual file patches.
370
+ Uses LLM to reconcile diffs while preserving code structure and context.
371
+
372
+ Args:
373
+ file_path: Target file path (used for context/error messages)
374
+ original_content: Current file content as string
375
+ file_diff: Unified diff snippet to apply
376
+ model: LLM identifier for processing
377
+ api_key: Optional override for LLM API credentials
378
+ base_url: Optional override for LLM API endpoint
379
+
380
+ Returns:
381
+ Updated file content as string with diff applied
382
+
383
+ Raises:
384
+ APIError: If LLM processing fails
385
+
386
+ Example:
387
+ >>> updated = call_llm_for_apply(
388
+ ... file_path='utils.py',
389
+ ... original_content='def old(): pass',
390
+ ... file_diff='''@@ -1 +1 @@
391
+ ... -def old()
392
+ ... +def new()''',
393
+ ... model='deepseek-reasoner'
394
+ ... )
395
+ >>> print(updated)
396
+ def new(): pass"""
397
+
398
+ system_prompt = """Please apply the diff to this file. Return the result in a block. Write the entire file.
399
+
400
+ 1. Carefully apply all changes from the diff
401
+ 2. Preserve surrounding context that isn't changed
402
+ 3. Only return the final file content, do not add any additional markup and do not add a code block"""
403
+
404
+ user_prompt = f"""File: {file_path}
405
+ File contents:
406
+ <filecontents>
407
+ {original_content}
408
+ </filecontents>
409
+
410
+ Diff to apply:
411
+ <diff>
412
+ {file_diff}
413
+ </diff>"""
414
+
415
+ if model == "gemini-2.0-flash-thinking-exp-01-21":
416
+ user_prompt = system_prompt+"\n"+user_prompt
417
+ messages = [
418
+ {"role": "system", "content": system_prompt},
419
+ {"role": "user", "content": user_prompt},
420
+ ]
421
+
422
+ if api_key is None:
423
+ api_key = os.getenv('NANOGPT_API_KEY')
424
+ if base_url is None:
425
+ base_url = os.getenv('NANOGPT_BASE_URL', "https://nano-gpt.com/api/v1/")
426
+ client = OpenAI(api_key=api_key, base_url=base_url)
427
+ response = client.chat.completions.create(model=model,
428
+ messages=messages,
429
+ temperature=0.0,
430
+ max_tokens=30000)
431
+
432
+ return response.choices[0].message.content
433
+
434
+ def build_environment_from_filelist(file_list, cwd):
435
+ """Build environment string from list of file paths"""
436
+ files_dict = {}
437
+ for file_path in file_list:
438
+ relative_path = os.path.relpath(file_path, cwd)
439
+ try:
440
+ with open(file_path, 'r') as f:
441
+ content = f.read()
442
+ files_dict[relative_path] = content
443
+ except UnicodeDecodeError:
444
+ print(f"Skipping file {file_path} due to UnicodeDecodeError")
445
+ continue
446
+ except IOError as e:
447
+ print(f"Error reading {file_path}: {e}")
448
+ continue
449
+ return build_environment(files_dict)
450
+
451
+ def main():
452
+ # Adding color support for Windows CMD
453
+ if os.name == 'nt':
454
+ os.system('color')
455
+
456
+ args = parse_arguments()
457
+
458
+ # TODO: The 'openai.api_base' option isn't read in the client API. You will need to pass it when you instantiate the client, e.g. 'OpenAI(base_url="https://nano-gpt.com/api/v1/")'
459
+ # openai.api_base = "https://nano-gpt.com/api/v1/"
460
+ if len(sys.argv) < 2:
461
+ print("Usage: python script.py '<user_prompt>' [--apply]")
462
+ sys.exit(1)
463
+
464
+ user_prompt = sys.argv[1]
465
+ project_dir = os.getcwd()
466
+ enc = tiktoken.get_encoding("o200k_base")
467
+
468
+
469
+ # Load project files, defaulting to current working directory if no additional paths are specified
470
+ if not args.files:
471
+ project_files = load_project_files(project_dir, project_dir)
472
+ else:
473
+ project_files = []
474
+ for additional_path in args.files:
475
+ if os.path.isfile(additional_path):
476
+ with open(additional_path, 'r') as f:
477
+ project_files.append((additional_path, f.read()))
478
+ elif os.path.isdir(additional_path):
479
+ project_files.extend(load_project_files(additional_path, project_dir))
480
+
481
+ if args.prepend:
482
+ prepend = load_prepend_file(args.prepend)
483
+ print("Including prepend",len(enc.encode(json.dumps(prepend))), "tokens")
484
+ else:
485
+ prepend = ""
486
+
487
+ # Prepare system prompt
488
+ system_prompt = prepend + f"Output a git diff into a <diff> block."
489
+
490
+ files_content = ""
491
+ for file, content in project_files:
492
+ print(f"Including {len(enc.encode(content)):5d} tokens", absolute_to_relative(file))
493
+
494
+ # Prepare the prompt for GPT-4
495
+ files_content += f"File: {absolute_to_relative(file)}\nContent:\n{content}\n"
496
+
497
+ full_prompt = f"{system_prompt}\n\n{user_prompt}\n\n{files_content}"
498
+ token_count = len(enc.encode(full_prompt))
499
+
500
+ if not args.call and not args.apply:
501
+ with open('prompt.txt', 'w') as f:
502
+ f.write(full_prompt)
503
+ print(f"Total tokens: {token_count:5d}")
504
+ print(f"\033[1;32mNot calling GPT-4.\033[0m") # Green color for success message
505
+ print('Instead, wrote full prompt to prompt.txt. Use `xclip -selection clipboard < prompt.txt` then paste into chatgpt')
506
+ print(f"Total cost: ${0.0:.4f}")
507
+ exit(0)
508
+ else:
509
+ # Validate API key presence before any API operations
510
+ if not os.getenv('NANOGPT_API_KEY'):
511
+ print("\033[1;31mError: NANOGPT_API_KEY environment variable required\033[0m")
512
+ print("Set it with: export NANOGPT_API_KEY='your-key'")
513
+ sys.exit(1)
514
+
515
+ # Confirm large requests without specified files
516
+ if (not args.nowarn) and (not args.files) and token_count > 10000 and (args.call or args.apply):
517
+ print(f"\033[1;33mThis is a larger request ({token_count} tokens). Are you sure you want to send it? [y/N]\033[0m")
518
+ confirmation = input().strip().lower()
519
+ if confirmation != 'y':
520
+ print("Request canceled")
521
+ sys.exit(0)
522
+ full_text, diff_text, prompt_tokens, completion_tokens, total_tokens, cost = call_gpt4_api(system_prompt, user_prompt, files_content, args.model,
523
+ temperature=args.temperature,
524
+ api_key=os.getenv('NANOGPT_API_KEY'),
525
+ base_url=os.getenv('NANOGPT_BASE_URL', "https://nano-gpt.com/api/v1/")
526
+ )
527
+
528
+ if(diff_text.strip() == ""):
529
+ print(f"\033[1;33mThere was no data in this diff. The LLM may have returned something invalid.\033[0m")
530
+ print("Unable to parse diff text. Full response:", full_text)
531
+ if args.beep:
532
+ print("\a") # Terminal bell for completion notification
533
+ return
534
+
535
+ # Output result
536
+ elif args.apply:
537
+ print("\nAttempting apply with the following diff:")
538
+ print("\n<diff>")
539
+ print(diff_text)
540
+ print("\n</diff>")
541
+ print("Saved to patch.diff")
542
+ if apply_diff(project_dir, diff_text):
543
+ print(f"\033[1;32mPatch applied successfully with 'git apply'.\033[0m") # Green color for success message
544
+ else:
545
+ print("Apply failed, attempting smart apply.")
546
+ parsed_diffs = parse_diff_per_file(diff_text)
547
+ print("Found", len(parsed_diffs), " files in diff, calling smartdiff for each file concurrently:")
548
+
549
+ if(len(parsed_diffs) == 0):
550
+ print(f"\033[1;33mThere were no entries in this diff. The LLM may have returned something invalid.\033[0m")
551
+ if args.beep:
552
+ print("\a") # Terminal bell for completion notification
553
+ return
554
+
555
+ threads = []
556
+
557
+ def process_file(file_path, file_diff):
558
+ full_path = Path(project_dir) / file_path
559
+ print(f"Processing file: {file_path}")
560
+
561
+ # Handle file deletions from diff
562
+ if '+++ /dev/null' in file_diff:
563
+ if full_path.exists():
564
+ full_path.unlink()
565
+ print(f"\033[1;32mDeleted file {file_path}.\033[0m")
566
+ else:
567
+ print(f"\033[1;33mFile {file_path} not found - skipping deletion\033[0m")
568
+ return
569
+
570
+ original_content = ''
571
+ if full_path.exists():
572
+ try:
573
+ original_content = full_path.read_text()
574
+ except UnicodeDecodeError:
575
+ print(f"Skipping binary file {file_path}")
576
+ return
577
+
578
+ try:
579
+ updated_content = call_llm_for_apply(file_path, original_content, file_diff, args.model)
580
+ full_path.parent.mkdir(parents=True, exist_ok=True)
581
+ full_path.write_text(updated_content)
582
+ print(f"\033[1;32mSuccessful 'smartapply' update {file_path}.\033[0m")
583
+ except Exception as e:
584
+ print(f"\033[1;31mFailed to process {file_path}: {str(e)}\033[0m")
585
+
586
+ threads = []
587
+ for file_path, file_diff in parsed_diffs:
588
+ thread = threading.Thread(
589
+ target=process_file,
590
+ args=(file_path, file_diff)
591
+ )
592
+ thread.start()
593
+ threads.append(thread)
594
+ for thread in threads:
595
+ thread.join()
596
+
597
+
598
+ if args.beep:
599
+ print("\a") # Terminal bell for completion notification
600
+
601
+ print(f"Prompt tokens: {prompt_tokens}")
602
+ print(f"Completion tokens: {completion_tokens}")
603
+ print(f"Total tokens: {total_tokens}")
604
+ #print(f"Total cost: ${cost:.4f}")
@@ -0,0 +1,18 @@
1
+ Metadata-Version: 2.2
2
+ Name: gptdiff
3
+ Version: 0.1.0
4
+ Summary: A tool to generate git diffs using GPT-4
5
+ Author: 255labs
6
+ Requires-Dist: openai>=1.0.0
7
+ Requires-Dist: tiktoken>=0.5.0
8
+ Requires-Dist: ai_agent_toolbox>=0.1.0
9
+ Provides-Extra: test
10
+ Requires-Dist: pytest; extra == "test"
11
+ Requires-Dist: pytest-mock; extra == "test"
12
+ Provides-Extra: docs
13
+ Requires-Dist: mkdocs; extra == "docs"
14
+ Requires-Dist: mkdocs-material; extra == "docs"
15
+ Dynamic: author
16
+ Dynamic: provides-extra
17
+ Dynamic: requires-dist
18
+ Dynamic: summary
@@ -0,0 +1,7 @@
1
+ gptdiff/__init__.py,sha256=yGjgwv7tNvH1ZLPsQyoo1CxpTOl1iCAwwDBp-_17ksQ,89
2
+ gptdiff/gptdiff.py,sha256=MBSZgt43Qq3SRqGzlKwBawwIv1Uewm2NaA_DBx_IPB8,22965
3
+ gptdiff-0.1.0.dist-info/METADATA,sha256=pf6BzIRLbLCeqVX3u_dwAG91WmPXKmVReTkwUWagPyk,508
4
+ gptdiff-0.1.0.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
5
+ gptdiff-0.1.0.dist-info/entry_points.txt,sha256=0yvXYEVAZFI-p32kQ4-h3qKVWS0a86jsM9FAwF89t9w,49
6
+ gptdiff-0.1.0.dist-info/top_level.txt,sha256=XNkQkQGINaDndEwRxg8qToOrJ9coyfAb-EHrSUXzdCE,8
7
+ gptdiff-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (75.8.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ gptdiff = gptdiff.gptdiff:main
@@ -0,0 +1 @@
1
+ gptdiff