pr-generator-agent 1.0.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.
aipr/__init__.py ADDED
@@ -0,0 +1,18 @@
1
+ """
2
+ AIPR - AI-powered Merge Request Description Generator
3
+ """
4
+
5
+ import tomllib
6
+ from pathlib import Path
7
+
8
+
9
+ def get_version():
10
+ try:
11
+ pyproject_path = Path(__file__).parent.parent / "pyproject.toml"
12
+ with open(pyproject_path, "rb") as f:
13
+ return tomllib.load(f)["project"]["version"]
14
+ except Exception:
15
+ return "unknown"
16
+
17
+
18
+ __version__ = get_version()
aipr/main.py ADDED
@@ -0,0 +1,634 @@
1
+ import argparse
2
+ import json
3
+ import os
4
+ import subprocess
5
+ import sys
6
+ from typing import Any, Dict, Optional, Tuple
7
+
8
+ import git
9
+ import tiktoken
10
+
11
+ from .prompts import InvalidPromptError, PromptManager
12
+ from .providers import generate_with_anthropic, generate_with_azure_openai, generate_with_openai
13
+
14
+ # ANSI color codes
15
+ BLUE = "\033[94m"
16
+ GREEN = "\033[92m"
17
+ YELLOW = "\033[93m"
18
+ RED = "\033[91m"
19
+ ENDC = "\033[0m"
20
+ BOLD = "\033[1m"
21
+
22
+
23
+ def detect_provider_and_model(model: Optional[str]) -> Tuple[str, str]:
24
+ """Detect which provider and model to use based on environment and args."""
25
+ if model:
26
+ # Handle simple aliases first
27
+ if model == "claude":
28
+ return "anthropic", "claude-3-sonnet-20240229"
29
+ if model == "azure":
30
+ return "azure", "gpt-4o-mini"
31
+ if model == "openai":
32
+ return "openai", "gpt-4o"
33
+
34
+ # Handle Azure models
35
+ if model.startswith("azure/"):
36
+ _, model_name = model.split("/", 1)
37
+ # Map azure model names to deployment names
38
+ azure_models = {
39
+ "o1-mini": "o1-mini",
40
+ "gpt-4o": "gpt-4o",
41
+ "gpt-4o-mini": "gpt-4o-mini",
42
+ "gpt-4": "gpt-4o", # Alias for gpt-4o
43
+ }
44
+ return "azure", azure_models.get(model_name, model_name)
45
+
46
+ # Handle OpenAI models
47
+ if model.startswith("gpt") or model == "gpt4":
48
+ # Only use Azure if explicitly configured with endpoint
49
+ if os.getenv("AZURE_OPENAI_ENDPOINT") and model.startswith("azure/"):
50
+ openai_models = {
51
+ "gpt4": "gpt-4",
52
+ "gpt-4-turbo": "gpt-4-turbo",
53
+ "gpt-4o": "gpt-4",
54
+ }
55
+ return "azure", openai_models.get(model, model)
56
+ else:
57
+ openai_models = {
58
+ "gpt4": "gpt-4",
59
+ "gpt-4-turbo": "gpt-4-turbo",
60
+ "gpt-4o": "gpt-4",
61
+ }
62
+ return "openai", openai_models.get(model, model)
63
+
64
+ # Handle Anthropic models
65
+ if model.startswith("claude"):
66
+ anthropic_models = {
67
+ "claude-3": "claude-3-opus-20240229",
68
+ "claude-3-opus": "claude-3-opus-20240229",
69
+ "claude-3.5-sonnet": "claude-3-5-sonnet-20241022",
70
+ "claude-3-sonnet": "claude-3-sonnet-20240229",
71
+ "claude-3.5-haiku": "claude-3-5-haiku-20241022",
72
+ "claude-3-haiku": "claude-3-haiku-20240307",
73
+ }
74
+ return "anthropic", anthropic_models.get(model, model)
75
+
76
+ # No model specified, check environment for default
77
+ if os.getenv("ANTHROPIC_API_KEY"):
78
+ return "anthropic", "claude-3-sonnet-20240229"
79
+ if os.getenv("AZURE_OPENAI_ENDPOINT") and os.getenv("AZURE_API_KEY"):
80
+ return "azure", "gpt-4"
81
+ if os.getenv("OPENAI_API_KEY"):
82
+ return "openai", "gpt-4"
83
+
84
+ raise Exception(
85
+ "No API key found. Please set ANTHROPIC_API_KEY, AZURE_API_KEY, or OPENAI_API_KEY"
86
+ )
87
+
88
+
89
+ def count_tokens(text: str, model: str) -> int:
90
+ """Count the number of tokens in a text string."""
91
+ try:
92
+ if model.startswith(("gpt-3", "gpt-4")):
93
+ encoding = tiktoken.encoding_for_model(model)
94
+ else:
95
+ # Default to cl100k_base for other models (including Azure and Claude)
96
+ encoding = tiktoken.get_encoding("cl100k_base")
97
+ return len(encoding.encode(text))
98
+ except Exception:
99
+ # If we can't get a token count, return an estimate
100
+ return len(text) // 4 # Rough estimate of tokens
101
+
102
+
103
+ def print_token_info(user_prompt: str, system_prompt: str, verbose: bool):
104
+ """Print token information for the prompts."""
105
+ if verbose:
106
+ print(f"System Prompt:\n{system_prompt}\n")
107
+ print(f"User Prompt:\n{user_prompt}\n")
108
+ # Add actual token counting if needed
109
+
110
+
111
+ def print_separator(char="─", color=GREEN):
112
+ """Print a separator line with the given character and color."""
113
+ terminal_width = os.get_terminal_size().columns
114
+ print(f"{color}{char * terminal_width}{ENDC}")
115
+
116
+
117
+ def print_header(text: str, level: int = 1):
118
+ """Print a header with the given text and level."""
119
+ if level == 1:
120
+ print(f"\n{text}")
121
+ print("=" * len(text))
122
+ else:
123
+ print(f"\n{text}")
124
+ print("-" * len(text))
125
+
126
+
127
+ def run_trivy_scan(path: str, silent: bool = False, verbose: bool = False) -> Dict[str, Any]:
128
+ """Run trivy filesystem scan and return the results as a dictionary."""
129
+ try:
130
+ # Determine project type and scanning approach
131
+ is_java = os.path.exists(os.path.join(path, "pom.xml"))
132
+ is_node = os.path.exists(os.path.join(path, "package.json"))
133
+
134
+ # Enhanced Python project detection
135
+ python_files = [
136
+ "requirements.txt",
137
+ "setup.py",
138
+ "pyproject.toml",
139
+ "poetry.lock",
140
+ "Pipfile",
141
+ "Pipfile.lock",
142
+ ]
143
+ is_python = any(os.path.exists(os.path.join(path, f)) for f in python_files)
144
+
145
+ trivy_args = ["trivy", "fs", "--format", "json", "--scanners", "vuln,secret,config"]
146
+
147
+ # Add dependency scanning for supported project types
148
+ if is_java:
149
+ try:
150
+ if not silent:
151
+ print(
152
+ f"{BLUE}Detected Java project, " f"resolving Maven dependencies...{ENDC}",
153
+ file=sys.stderr,
154
+ )
155
+ subprocess.run(
156
+ ["mvn", "dependency:resolve", "-DskipTests"],
157
+ cwd=path,
158
+ check=True,
159
+ capture_output=True,
160
+ )
161
+ trivy_args.append("--dependency-tree")
162
+ except subprocess.CalledProcessError as e:
163
+ print(
164
+ f"{YELLOW}Warning: Maven dependency resolution failed: {e}{ENDC}",
165
+ file=sys.stderr,
166
+ )
167
+ elif is_node:
168
+ if not silent:
169
+ print(
170
+ f"{BLUE}Detected Node.js project, including dependency scanning...{ENDC}",
171
+ file=sys.stderr,
172
+ )
173
+ trivy_args.append("--dependency-tree")
174
+ elif is_python:
175
+ if not silent:
176
+ print(
177
+ f"{BLUE}Detected Python project, including enhanced scanning...{ENDC}",
178
+ file=sys.stderr,
179
+ )
180
+
181
+ # Check which package management files exist
182
+ pkg_files = []
183
+ has_poetry = os.path.exists(os.path.join(path, "poetry.lock"))
184
+ has_pipenv = os.path.exists(os.path.join(path, "Pipfile.lock"))
185
+ has_pip = os.path.exists(os.path.join(path, "requirements.txt"))
186
+ has_setup = os.path.exists(os.path.join(path, "setup.py"))
187
+ has_pyproject = os.path.exists(os.path.join(path, "pyproject.toml"))
188
+
189
+ if has_poetry:
190
+ pkg_files.append("poetry.lock")
191
+ if has_pyproject:
192
+ pkg_files.append("pyproject.toml")
193
+ if not silent:
194
+ print(
195
+ f"{BLUE}Using Poetry for dependency scanning "
196
+ f"(transitive dependencies, excludes dev)...{ENDC}",
197
+ file=sys.stderr,
198
+ )
199
+ elif has_pipenv:
200
+ pkg_files.append("Pipfile.lock")
201
+ if not silent:
202
+ print(
203
+ f"{BLUE}Using Pipenv for dependency scanning "
204
+ f"(transitive dependencies, includes dev)...{ENDC}",
205
+ file=sys.stderr,
206
+ )
207
+ elif has_pip:
208
+ pkg_files.append("requirements.txt")
209
+ if not silent:
210
+ print(
211
+ f"{BLUE}Using pip requirements "
212
+ f"(direct dependencies only, includes dev)...{ENDC}",
213
+ file=sys.stderr,
214
+ )
215
+ elif has_setup or has_pyproject:
216
+ if has_setup:
217
+ pkg_files.append("setup.py")
218
+ if has_pyproject:
219
+ pkg_files.append("pyproject.toml")
220
+ if not silent:
221
+ print(f"{BLUE}Using Python package metadata files...{ENDC}", file=sys.stderr)
222
+
223
+ if pkg_files and not silent:
224
+ print(f"{BLUE}Found package files: {', '.join(pkg_files)}{ENDC}", file=sys.stderr)
225
+
226
+ # Add Python-specific scanning options
227
+ trivy_args.append("--dependency-tree")
228
+ else:
229
+ if not silent:
230
+ print(
231
+ f"{BLUE}No specific package manager detected, "
232
+ f"performing filesystem scan...{ENDC}",
233
+ file=sys.stderr,
234
+ )
235
+
236
+ # Add the path as the last argument
237
+ trivy_args.append(path)
238
+
239
+ if not silent and verbose:
240
+ print(f"{BLUE}Running trivy with args: {' '.join(trivy_args)}{ENDC}", file=sys.stderr)
241
+
242
+ # Run trivy scan
243
+ result = subprocess.run(trivy_args, capture_output=True, text=True, check=True)
244
+ return json.loads(result.stdout)
245
+ except subprocess.CalledProcessError as e:
246
+ print(f"{RED}Error running trivy scan: {e}{ENDC}", file=sys.stderr)
247
+ if e.stderr:
248
+ print(f"{RED}Trivy error details: {e.stderr}{ENDC}", file=sys.stderr)
249
+ return {}
250
+ except json.JSONDecodeError as e:
251
+ print(f"{RED}Error parsing trivy output: {e}{ENDC}", file=sys.stderr)
252
+ return {}
253
+
254
+
255
+ def compare_vulnerabilities(
256
+ current_scan: Dict[str, Any], target_scan: Dict[str, Any]
257
+ ) -> Tuple[str, str]:
258
+ """
259
+ Compare vulnerability scans between branches and return a tuple of
260
+ (markdown_report, analysis_data)
261
+ """
262
+ if not current_scan or not target_scan:
263
+ return "Error: Unable to generate vulnerability comparison", ""
264
+
265
+ report = ["## Vulnerability Comparison\n"]
266
+ analysis_data = ["### Security Analysis Data\n"]
267
+
268
+ # Get vulnerabilities from both scans
269
+ current_vulns = []
270
+ target_vulns = []
271
+
272
+ def extract_vulns(scan_data: Dict[str, Any]) -> list:
273
+ vulns = []
274
+ for result in scan_data.get("Results", []):
275
+ target = result.get("Target", "")
276
+ type = result.get("Type", "")
277
+ for vuln in result.get("Vulnerabilities", []):
278
+ vulns.append(
279
+ {
280
+ "id": vuln.get("VulnerabilityID"),
281
+ "pkg": vuln.get("PkgName"),
282
+ "version": vuln.get("InstalledVersion"),
283
+ "severity": vuln.get("Severity"),
284
+ "description": vuln.get("Description"),
285
+ "fix_version": vuln.get("FixedVersion"),
286
+ "target": target,
287
+ "type": type,
288
+ "title": vuln.get("Title"),
289
+ "references": vuln.get("References", []),
290
+ }
291
+ )
292
+ return vulns
293
+
294
+ current_vulns = extract_vulns(current_scan)
295
+ target_vulns = extract_vulns(target_scan)
296
+
297
+ # Create unique identifiers for comparison
298
+ def create_vuln_key(v: Dict[str, Any]) -> str:
299
+ return f"{v['id']}:{v['pkg']}:{v['version']}:{v['target']}"
300
+
301
+ current_vuln_keys = {create_vuln_key(v) for v in current_vulns}
302
+ target_vuln_keys = {create_vuln_key(v) for v in target_vulns}
303
+
304
+ # Find new and fixed vulnerabilities
305
+ new_vulns = current_vuln_keys - target_vuln_keys
306
+ fixed_vulns = target_vuln_keys - current_vuln_keys
307
+
308
+ # Group vulnerabilities by severity for analysis
309
+ severity_order = {"CRITICAL": 0, "HIGH": 1, "MEDIUM": 2, "LOW": 3, "UNKNOWN": 4}
310
+
311
+ def group_by_severity(vulns_keys: set, vuln_list: list) -> Dict[str, list]:
312
+ grouped = {}
313
+ for vuln in vuln_list:
314
+ key = create_vuln_key(vuln)
315
+ if key in vulns_keys:
316
+ sev = vuln["severity"] or "UNKNOWN"
317
+ if sev not in grouped:
318
+ grouped[sev] = []
319
+ grouped[sev].append(vuln)
320
+ return {
321
+ k: grouped[k] for k in sorted(grouped.keys(), key=lambda x: severity_order.get(x, 999))
322
+ }
323
+
324
+ # Prepare detailed analysis data
325
+ if new_vulns:
326
+ analysis_data.append("\nNew Vulnerabilities Details:")
327
+ grouped_new = group_by_severity(new_vulns, current_vulns)
328
+ for severity, vulns in grouped_new.items():
329
+ analysis_data.append(f"\n{severity} Severity:")
330
+ for vuln in sorted(vulns, key=lambda x: x["id"]):
331
+ analysis_data.append(f"\n- {vuln['id']} ({vuln['type']}):")
332
+ analysis_data.append(f" - Package: {vuln['pkg']} {vuln['version']}")
333
+ analysis_data.append(f" - In: {vuln['target']}")
334
+ analysis_data.append(f" - Title: {vuln['title']}")
335
+ analysis_data.append(f" - Description: {vuln['description']}")
336
+ if vuln["fix_version"]:
337
+ fix_info = f" - Fix available in version: " f"{vuln['fix_version']}"
338
+ analysis_data.append(fix_info)
339
+ if vuln["references"]:
340
+ analysis_data.append(" - References:")
341
+ for ref in vuln["references"][:3]: # Limit to first 3 references
342
+ analysis_data.append(f" * {ref}")
343
+
344
+ if fixed_vulns:
345
+ analysis_data.append("\nFixed Vulnerabilities Details:")
346
+ grouped_fixed = group_by_severity(fixed_vulns, target_vulns)
347
+ for severity, vulns in grouped_fixed.items():
348
+ analysis_data.append(f"\n{severity} Severity:")
349
+ for vuln in sorted(vulns, key=lambda x: x["id"]):
350
+ analysis_data.append(f"\n- {vuln['id']} ({vuln['type']}):")
351
+ analysis_data.append(f" - Package: {vuln['pkg']} {vuln['version']}")
352
+ analysis_data.append(f" - In: {vuln['target']}")
353
+ analysis_data.append(f" - Title: {vuln['title']}")
354
+
355
+ # Generate markdown report
356
+ if new_vulns:
357
+ report.append("\n### New Vulnerabilities\n")
358
+ grouped_new = group_by_severity(new_vulns, current_vulns)
359
+ for severity, vulns in grouped_new.items():
360
+ report.append(f"\n#### {severity}\n")
361
+ for vuln in sorted(vulns, key=lambda x: x["id"]):
362
+ vuln_line = (
363
+ f"- {vuln['id']} in {vuln['pkg']} " f"{vuln['version']} ({vuln['target']})"
364
+ )
365
+ report.append(vuln_line)
366
+
367
+ if fixed_vulns:
368
+ report.append("\n### Fixed Vulnerabilities\n")
369
+ grouped_fixed = group_by_severity(fixed_vulns, target_vulns)
370
+ for severity, vulns in grouped_fixed.items():
371
+ report.append(f"\n#### {severity}\n")
372
+ for vuln in sorted(vulns, key=lambda x: x["id"]):
373
+ vuln_line = (
374
+ f"- {vuln['id']} in {vuln['pkg']} " f"{vuln['version']} ({vuln['target']})"
375
+ )
376
+ report.append(vuln_line)
377
+
378
+ if not new_vulns and not fixed_vulns:
379
+ report.append("\nNo vulnerability changes detected between branches.")
380
+ analysis_data.append("\nNo security changes to analyze.")
381
+
382
+ return "\n".join(report), "\n".join(analysis_data)
383
+
384
+
385
+ class ColorHelpFormatter(argparse.RawDescriptionHelpFormatter):
386
+ """Custom formatter to preserve colors in help text."""
387
+
388
+ def _split_lines(self, text, width):
389
+ return text.splitlines()
390
+
391
+
392
+ def parse_args(args=None):
393
+ """Parse command line arguments."""
394
+ parser = argparse.ArgumentParser(
395
+ description="Generate PR description from git diff",
396
+ formatter_class=ColorHelpFormatter,
397
+ epilog=f"""
398
+ recommended models:
399
+ {YELLOW}claude-3.5-sonnet{ENDC} (default) Anthropic's Claude 3.5 Sonnet
400
+ {YELLOW}azure/o1-mini{ENDC} Azure OpenAI o1-mini
401
+ {YELLOW}azure/gpt-4o{ENDC} Azure OpenAI GPT-4
402
+ {GREEN}gpt-4{ENDC} OpenAI GPT-4
403
+
404
+ prompt templates:
405
+ {BLUE}meta{ENDC} Default XML prompt template for merge requests""",
406
+ )
407
+ parser.add_argument("-s", "--silent", action="store_true", help="Silent mode")
408
+ parser.add_argument(
409
+ "-d",
410
+ "--debug",
411
+ action="store_true",
412
+ help="Debug mode - show prompts without sending to LLM",
413
+ )
414
+ parser.add_argument(
415
+ "-v",
416
+ "--verbose",
417
+ action="store_true",
418
+ help="Verbose mode - show detailed API interaction",
419
+ )
420
+ parser.add_argument("-t", "--target", help="Target branch for comparison")
421
+ parser.add_argument("--vulns", action="store_true", help="Include vulnerability scan")
422
+ parser.add_argument("--working-tree", action="store_true", help="Use working tree")
423
+ parser.add_argument(
424
+ "-m",
425
+ "--model",
426
+ help="AI model to use (see recommended models below)",
427
+ )
428
+ parser.add_argument(
429
+ "-p",
430
+ "--prompt",
431
+ help=(
432
+ "Specify either a built-in prompt name (e.g., 'meta') or "
433
+ "a path to a custom XML prompt file (e.g., '~/prompts/custom.xml')"
434
+ ),
435
+ )
436
+ return parser.parse_args(args)
437
+
438
+
439
+ def detect_default_branch(repo: git.Repo) -> str:
440
+ """Detect the default branch of the repository."""
441
+ for branch in ["main", "master", "develop"]:
442
+ try:
443
+ repo.git.rev_parse("--verify", branch)
444
+ return branch
445
+ except git.exc.GitCommandError:
446
+ continue
447
+ raise Exception("Could not detect default branch")
448
+
449
+
450
+ def get_vulnerability_data() -> Optional[str]:
451
+ """Get vulnerability scan data using trivy."""
452
+ try:
453
+ result = subprocess.run(
454
+ ["trivy", "fs", "--quiet", "--severity", "HIGH,CRITICAL", "."],
455
+ capture_output=True,
456
+ text=True,
457
+ )
458
+ return result.stdout if result.stdout.strip() else None
459
+ except FileNotFoundError:
460
+ print("Warning: trivy not found. Skipping vulnerability scan.")
461
+ return None
462
+
463
+
464
+ def generate_description(
465
+ diff: str,
466
+ vuln_data: Optional[str],
467
+ provider: str,
468
+ model: str,
469
+ system_prompt: str,
470
+ verbose: bool = False,
471
+ prompt_manager: Optional[PromptManager] = None,
472
+ ) -> str:
473
+ """Generate description using the specified provider."""
474
+ # Get the user prompt from the prompt manager
475
+ if prompt_manager is None:
476
+ prompt_manager = PromptManager()
477
+ user_prompt = prompt_manager.get_user_prompt(diff, vuln_data)
478
+
479
+ if provider == "anthropic":
480
+ return generate_with_anthropic(user_prompt, vuln_data, model, system_prompt, verbose)
481
+ if provider == "azure":
482
+ return generate_with_azure_openai(user_prompt, vuln_data, model, system_prompt, verbose)
483
+ if provider == "openai":
484
+ return generate_with_openai(user_prompt, vuln_data, model, system_prompt, verbose)
485
+ raise ValueError(f"Unknown provider: {provider}")
486
+
487
+
488
+ def main(args=None):
489
+ """Main entry point for AIPR"""
490
+ args = parse_args(args)
491
+
492
+ try:
493
+ repo = git.Repo(os.getcwd(), search_parent_directories=True)
494
+ except git.InvalidGitRepositoryError:
495
+ print(f"{RED}Error: Directory is not a valid Git repository{ENDC}", file=sys.stderr)
496
+ sys.exit(1)
497
+
498
+ try:
499
+ prompt_manager = PromptManager(args.prompt)
500
+ except InvalidPromptError as e:
501
+ print(f"{RED}Error: {str(e)}{ENDC}")
502
+ sys.exit(1)
503
+
504
+ provider, model = detect_provider_and_model(args.model)
505
+
506
+ # Get the diff based on the state
507
+ diff = ""
508
+ if args.target == "-" or repo.is_dirty():
509
+ # Show working tree changes
510
+ if not args.silent:
511
+ print(f"{BLUE}Showing working tree changes...{ENDC}", file=sys.stderr)
512
+ diff = repo.git.diff("HEAD", "--cached") + "\n" + repo.git.diff()
513
+ else:
514
+ # Compare with target branch
515
+ target = args.target
516
+ if not target:
517
+ # Try to find default branch
518
+ for branch in ["main", "master", "develop"]:
519
+ if branch in [h.name for h in repo.heads]:
520
+ target = branch
521
+ break
522
+
523
+ if target:
524
+ if not args.silent:
525
+ print(f"{BLUE}Comparing with {target}...{ENDC}", file=sys.stderr)
526
+ diff = repo.git.diff(f"{target}...{repo.active_branch.name}")
527
+ else:
528
+ print(f"{YELLOW}No suitable target branch found.{ENDC}", file=sys.stderr)
529
+ sys.exit(1)
530
+
531
+ if not diff.strip():
532
+ print("No changes found in the Git repository.", file=sys.stderr)
533
+ sys.exit(0)
534
+
535
+ # Get vulnerability data if requested
536
+ vuln_data = None
537
+ if args.vulns:
538
+ if not args.silent:
539
+ print(f"{BLUE}Running vulnerability scan...{ENDC}", file=sys.stderr)
540
+ vuln_data = run_trivy_scan(repo.working_dir, args.silent, False)
541
+
542
+ # Generate the description
543
+ if not args.silent:
544
+ print_header("\nGenerating Description")
545
+ print(f"Using {provider} ({model})...")
546
+
547
+ try:
548
+ # In debug mode, show the prompts that would be sent
549
+ if args.debug:
550
+ # Get the prompts first
551
+ system_prompt = prompt_manager.get_system_prompt()
552
+ user_prompt = prompt_manager.get_user_prompt(diff, vuln_data)
553
+
554
+ # Prepare the API parameters
555
+ if provider == "azure" and model == "o1-mini":
556
+ combined_prompt = (
557
+ f"System Instructions:\n{system_prompt}\n\n" f"User Request:\n{user_prompt}"
558
+ )
559
+ messages = [{"role": "user", "content": combined_prompt}]
560
+ params = {
561
+ "model": model,
562
+ "messages": messages,
563
+ "max_completion_tokens": 1000,
564
+ }
565
+ elif provider == "anthropic":
566
+ params = {
567
+ "model": model,
568
+ "system": system_prompt,
569
+ "messages": [{"role": "user", "content": user_prompt}],
570
+ "max_tokens": 1000,
571
+ "temperature": 0.2,
572
+ }
573
+ else:
574
+ params = {
575
+ "model": model,
576
+ "messages": [
577
+ {"role": "system", "content": system_prompt},
578
+ {"role": "user", "content": user_prompt},
579
+ ],
580
+ "max_tokens": 1000,
581
+ "temperature": 0.2,
582
+ }
583
+
584
+ # Print debug information in a structured way
585
+ print_header("Debug Information")
586
+
587
+ print_header("API Call Structure", level=2)
588
+ print(f"Provider: {provider}")
589
+ print(f"Model: {model}")
590
+ print(
591
+ f"Endpoint: {os.getenv('AZURE_OPENAI_ENDPOINT', 'Not Set')}"
592
+ if provider == "azure"
593
+ else ""
594
+ )
595
+ print("\nParameters:")
596
+ print("─" * 40)
597
+ print(json.dumps({k: v for k, v in params.items() if k != "messages"}, indent=2))
598
+ print("\nMessages:")
599
+ print("─" * 40)
600
+ for msg in params["messages"]:
601
+ print(f"\n{msg['role'].upper()} MESSAGE:")
602
+ print(msg["content"])
603
+ print()
604
+ sys.exit(0)
605
+
606
+ # Generate the description
607
+ description = generate_description(
608
+ diff,
609
+ vuln_data,
610
+ provider,
611
+ model,
612
+ (
613
+ prompt_manager.get_system_prompt()
614
+ if prompt_manager
615
+ else PromptManager().get_system_prompt()
616
+ ),
617
+ args.verbose,
618
+ prompt_manager or PromptManager(), # Ensure we always pass a valid PromptManager
619
+ )
620
+
621
+ if args.verbose:
622
+ print("\nAPI Response:")
623
+ print("─" * 40)
624
+ print(description)
625
+
626
+ except Exception as e:
627
+ print(f"{RED}Error: {provider.title()} API - {e}{ENDC}", file=sys.stderr)
628
+ sys.exit(1)
629
+
630
+ sys.exit(0)
631
+
632
+
633
+ if __name__ == "__main__":
634
+ main()
@@ -0,0 +1,5 @@
1
+ """Prompt templates for AIPR."""
2
+
3
+ from .prompts import InvalidPromptError, PromptManager
4
+
5
+ __all__ = ["PromptManager", "InvalidPromptError"]
aipr/prompts/meta.xml ADDED
@@ -0,0 +1,49 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <prompt>
3
+ <purpose>
4
+ Given a change-set and optional vulnerability-set generate a merge request description.
5
+ Strictly follow the instructions.
6
+ Format output like the example provided.
7
+ Always include a Security Analysis section.
8
+ </purpose>
9
+
10
+ <instructions>
11
+ <always>Focus only on the specific changes shown in the diff and vulnerability comparison.</always>
12
+ <always>Directly tie each point to actual code changes or security findings.</always>
13
+ <always>Highlight critical security changes when analyzing vulnerabilities.</always>
14
+ <always>Explain the impact of new vulnerabilities when provided.</always>
15
+ <always>Acknowledge any fixed vulnerabilities.</always>
16
+ <always>If tempted to write a concluding statement after the Security Analysis, do not continue.</always>
17
+ <never>Do not include generic concluding statements (e.g., "This improves the overall system").</never>
18
+ <never>Do not include broad claims about improvements (e.g., "This enhances development processes").</never>
19
+ <never>Do not include value judgments about the changes (e.g., "This is a significant improvement").</never>
20
+ <never>Do not include future benefits or implications.</never>
21
+ </instructions>
22
+
23
+ <example>
24
+ Change Summary:
25
+
26
+ 1. **Added User Authentication**
27
+ - Implemented JWT middleware
28
+ - Added login/register endpoints
29
+ - Updated bcrypt to v5.1.1
30
+
31
+ 2. **Security Updates**
32
+ - Fixed 2 medium severity vulnerabilities
33
+ - Updated deprecated crypto functions
34
+
35
+ Security Analysis:
36
+ ✓ No new vulnerabilities introduced
37
+ - Previous medium severity vulnerabilities in bcrypt have been resolved
38
+ - All crypto functions now use current best practices
39
+ </example>
40
+
41
+ <changes-set>
42
+ {{change_set}}
43
+ </changes-set>
44
+
45
+ <vulnerabilities-set>
46
+ {{vulnerability_set}}
47
+ </vulnerabilities-set>
48
+
49
+ </prompt>
@@ -0,0 +1,189 @@
1
+ """Prompt management for AIPR."""
2
+
3
+ import importlib.resources
4
+ import json
5
+ import os
6
+ import xml.etree.ElementTree as ET
7
+ from pathlib import Path
8
+ from typing import Union
9
+
10
+
11
+ class InvalidPromptError(Exception):
12
+ """Raised when a prompt file is invalid."""
13
+
14
+ pass
15
+
16
+
17
+ class PromptManager:
18
+ """Manages system and user prompts for AI models."""
19
+
20
+ REQUIRED_XML_ELEMENTS = ["changes-set", "vulnerabilities-set"]
21
+
22
+ def __init__(self, prompt_name: str = None):
23
+ self._default_system_prompt = (
24
+ "You are a helpful assistant for generating Merge Requests.\n"
25
+ "Your task is to analyze Git changes and vulnerability comparison data to create "
26
+ "clear, well-structured merge request descriptions.\n"
27
+ "Response should end with the last specific change or security finding discussed.\n"
28
+ "If you find yourself wanting to write a concluding statement, stop writing instead."
29
+ )
30
+
31
+ self.prompt_name = prompt_name
32
+ self._xml_prompt = None
33
+
34
+ if prompt_name:
35
+ self._load_prompt(prompt_name)
36
+
37
+ def _load_prompt(self, prompt_name: str) -> None:
38
+ """Load a prompt from either a file path or a built-in prompt name."""
39
+ # First check if this is a file path (has .xml extension)
40
+ path = Path(os.path.expanduser(prompt_name))
41
+ if path.suffix == ".xml":
42
+ if path.exists():
43
+ self._load_xml_prompt(str(path))
44
+ return
45
+ # Try local prompts directory
46
+ local_path = Path("prompts") / path.name
47
+ if local_path.exists():
48
+ self._load_xml_prompt(str(local_path))
49
+ return
50
+ raise InvalidPromptError(f"Prompt file not found: {path}")
51
+
52
+ # If no .xml extension, treat as a built-in prompt name
53
+ available_prompts = self._get_available_prompts()
54
+ try:
55
+ with (
56
+ importlib.resources.files("aipr.prompts").joinpath(f"{prompt_name}.xml").open("r")
57
+ ) as f:
58
+ self._load_xml_prompt_from_string(f.read())
59
+ except Exception as e:
60
+ raise InvalidPromptError(
61
+ f"Could not load prompt '{prompt_name}'. "
62
+ f"Error: {e}\n\n"
63
+ f"Available built-in prompts: {', '.join(available_prompts)}"
64
+ )
65
+
66
+ def _validate_xml_prompt(self, root: ET.Element) -> None:
67
+ """Validate that the XML prompt has all required elements."""
68
+ for element in self.REQUIRED_XML_ELEMENTS:
69
+ if root.find(f".//{element}") is None:
70
+ raise InvalidPromptError(
71
+ f"Invalid prompt file: Missing required element '{element}'"
72
+ )
73
+
74
+ def _load_xml_prompt(self, file_path: str) -> None:
75
+ """Load and parse the XML prompt template from a file."""
76
+ try:
77
+ tree = ET.parse(file_path)
78
+ root = tree.getroot()
79
+ self._validate_xml_prompt(root)
80
+ self._xml_prompt = root
81
+ except ET.ParseError as e:
82
+ raise InvalidPromptError(f"Error parsing XML prompt file: {e}")
83
+ except FileNotFoundError:
84
+ raise InvalidPromptError(f"Prompt file not found: {file_path}")
85
+ except PermissionError:
86
+ raise InvalidPromptError(f"Permission denied reading prompt file: {file_path}")
87
+
88
+ def _load_xml_prompt_from_string(self, xml_content: str) -> None:
89
+ """Load and parse the XML prompt template from a string."""
90
+ try:
91
+ root = ET.fromstring(xml_content)
92
+ self._validate_xml_prompt(root)
93
+ self._xml_prompt = root
94
+ except ET.ParseError as e:
95
+ raise InvalidPromptError(f"Error parsing XML prompt content: {e}")
96
+
97
+ def _get_available_prompts(self) -> list[str]:
98
+ """Get list of available built-in prompts."""
99
+ try:
100
+ prompts_dir = importlib.resources.files("aipr.prompts")
101
+ return [
102
+ f.stem for f in prompts_dir.iterdir() if f.suffix == ".xml" and f.stem != "__init__"
103
+ ]
104
+ except Exception:
105
+ return []
106
+
107
+ def get_system_prompt(self) -> str:
108
+ """Get the system prompt."""
109
+ return self._default_system_prompt
110
+
111
+ def get_user_prompt(self, diff: str, vuln_data: Union[str, dict, None] = None) -> str:
112
+ """Get the user prompt with diff and optional vulnerability data."""
113
+ if self.prompt_name:
114
+ if self._xml_prompt is None:
115
+ raise ValueError("XML prompt file was specified but could not be loaded")
116
+
117
+ # Find the changes-set and vulnerabilities-set elements
118
+ changes_set = self._xml_prompt.find(".//changes-set")
119
+ vulns_set = self._xml_prompt.find(".//vulnerabilities-set")
120
+
121
+ # Update the content
122
+ if changes_set is not None:
123
+ changes_set.text = "\n" + diff + "\n" # Add newlines for better formatting
124
+ if vulns_set is not None:
125
+ if vuln_data:
126
+ vulns_set.text = (
127
+ "\n"
128
+ + (
129
+ json.dumps(vuln_data, indent=2)
130
+ if isinstance(vuln_data, dict)
131
+ else str(vuln_data)
132
+ )
133
+ + "\n"
134
+ )
135
+ else:
136
+ vulns_set.text = ""
137
+
138
+ # Get the example section
139
+ example = self._xml_prompt.find(".//example")
140
+ if example is not None:
141
+ example.tail = "\n" # Add newline after example
142
+
143
+ # Convert to string while preserving formatting and remove XML declaration
144
+ xml_str = ET.tostring(self._xml_prompt, encoding="unicode", method="xml")
145
+ if xml_str.startswith("<?xml"):
146
+ xml_str = xml_str[xml_str.find("?>") + 2 :]
147
+
148
+ # Return the XML structure directly without wrapping it in additional text
149
+ return xml_str.strip()
150
+
151
+ # Default format when no XML prompt file is specified
152
+ prompt = [
153
+ "Please include:",
154
+ "- A concise summary of the changes",
155
+ "- Key modifications and their purpose",
156
+ "- Any notable technical details",
157
+ "- Security impact analysis (when vulnerability data is provided)",
158
+ "",
159
+ "Important Guidelines:",
160
+ "1. Focus only on the specific changes shown in the diff and vulnerability comparison",
161
+ "2. Each point must be directly tied to actual code changes or security findings",
162
+ "3. When analyzing vulnerabilities:",
163
+ " - Highlight critical security changes",
164
+ " - Explain the impact of new vulnerabilities",
165
+ " - Acknowledge fixed vulnerabilities",
166
+ "4. DO NOT include any of the following:",
167
+ ' - Generic concluding statements (e.g., "This improves the overall system")',
168
+ ' - Broad claims about improvements (e.g., "This enhances development processes")',
169
+ ' - Value judgments about the changes (e.g., "This is a significant improvement")',
170
+ " - Future benefits or implications",
171
+ "",
172
+ "Git Diff:",
173
+ diff,
174
+ ]
175
+
176
+ if vuln_data:
177
+ prompt.extend(
178
+ [
179
+ "",
180
+ "Vulnerability Analysis:",
181
+ (
182
+ json.dumps(vuln_data, indent=2)
183
+ if isinstance(vuln_data, dict)
184
+ else str(vuln_data)
185
+ ),
186
+ ]
187
+ )
188
+
189
+ return "\n".join(prompt)
aipr/providers.py ADDED
@@ -0,0 +1,197 @@
1
+ import json
2
+ import os
3
+ from typing import Any, Dict, Optional
4
+
5
+ import anthropic
6
+ from openai import AzureOpenAI, OpenAI
7
+
8
+
9
+ def generate_with_anthropic(
10
+ diff: str,
11
+ vuln_data: Optional[Dict[str, Any]],
12
+ model: str,
13
+ system_prompt: str,
14
+ verbose: bool = False,
15
+ ) -> str:
16
+ """Generate description using Anthropic's Claude."""
17
+ if verbose:
18
+ print("\nInitializing Anthropic client...")
19
+
20
+ client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))
21
+
22
+ if verbose:
23
+ print("\nSending request to Anthropic API:")
24
+ print(f" Model: {model}")
25
+ print(" Parameters:", json.dumps({"max_tokens": 1000, "temperature": 0.2}, indent=2))
26
+ print("\nRequest Messages:")
27
+ print("\nSYSTEM MESSAGE:")
28
+ print(system_prompt)
29
+ print("\nUSER MESSAGE:")
30
+ if len(diff) > 500:
31
+ print(diff[:500] + "...")
32
+ else:
33
+ print(diff)
34
+ print("\nMaking API call...")
35
+
36
+ try:
37
+ response = client.messages.create(
38
+ model=model,
39
+ max_tokens=1000,
40
+ temperature=0.2,
41
+ system=system_prompt,
42
+ messages=[{"role": "user", "content": diff}],
43
+ )
44
+ if verbose:
45
+ print("\nRaw API Response:")
46
+ print(f" Model: {response.model}")
47
+ print(f" Usage: {response.usage.model_dump() if response.usage else 'N/A'}")
48
+ print("\nResponse Content:")
49
+ return response.content[0].text
50
+ except Exception as e:
51
+ if verbose:
52
+ print(f"\nAPI Error: {str(e)}")
53
+ raise ValueError(f"Anthropic API error: {str(e)}")
54
+
55
+
56
+ def generate_with_azure_openai(
57
+ diff: str,
58
+ vuln_data: Optional[Dict[str, Any]],
59
+ model: str,
60
+ system_prompt: str,
61
+ verbose: bool = False,
62
+ ) -> str:
63
+ """Generate description using Azure OpenAI."""
64
+ # Check required environment variables
65
+ endpoint = os.getenv("AZURE_OPENAI_ENDPOINT")
66
+ api_key = os.getenv("AZURE_API_KEY")
67
+
68
+ if not endpoint or not api_key:
69
+ raise ValueError(
70
+ "Missing Azure OpenAI configuration. "
71
+ "Please set AZURE_OPENAI_ENDPOINT and AZURE_API_KEY environment variables."
72
+ )
73
+
74
+ try:
75
+ if verbose:
76
+ print("\nInitializing Azure OpenAI client with:")
77
+ print(f" Endpoint: {endpoint}")
78
+ print(" API Version: 2024-02-15-preview")
79
+
80
+ client = AzureOpenAI(
81
+ api_key=api_key,
82
+ api_version="2024-02-15-preview",
83
+ azure_endpoint=endpoint,
84
+ )
85
+
86
+ # For models that don't support system messages (like o1-mini), prepend it to user message
87
+ messages = []
88
+ if model in ["o1-mini"]:
89
+ combined_prompt = f"System Instructions:\n{system_prompt}\n\n{diff}"
90
+ messages = [{"role": "user", "content": combined_prompt}]
91
+ else:
92
+ messages = [
93
+ {"role": "system", "content": system_prompt},
94
+ {"role": "user", "content": diff},
95
+ ]
96
+
97
+ # o1-mini has special parameter requirements:
98
+ # - doesn't support temperature parameter
99
+ kwargs = {
100
+ "model": model,
101
+ "messages": messages,
102
+ }
103
+ if model not in ["o1-mini"]: # Only set these for non-o1-mini models
104
+ kwargs["max_tokens"] = 1000
105
+ kwargs["temperature"] = 0.2
106
+
107
+ if verbose:
108
+ print("\nSending request to Azure OpenAI API:")
109
+ print(f" Model: {model}")
110
+ print(
111
+ " Parameters:",
112
+ json.dumps({k: v for k, v in kwargs.items() if k != "messages"}, indent=2),
113
+ )
114
+ print("\nRequest Messages:")
115
+ for msg in messages:
116
+ print(f"\n{msg['role'].upper()} MESSAGE:")
117
+ content = msg["content"]
118
+ if len(content) > 500:
119
+ print(content[:500] + "...")
120
+ else:
121
+ print(content)
122
+
123
+ try:
124
+ if verbose:
125
+ print("\nMaking API call...")
126
+ response = client.chat.completions.create(**kwargs)
127
+ if not response.choices:
128
+ raise ValueError("No completion choices returned from the API")
129
+ if verbose:
130
+ print("\nRaw API Response:")
131
+ print(f" Model: {response.model}")
132
+ print(f" Usage: {response.usage.model_dump() if response.usage else 'N/A'}")
133
+ print("\nResponse Content:")
134
+ return response.choices[0].message.content
135
+ except Exception as api_error:
136
+ error_msg = str(api_error)
137
+ if verbose:
138
+ print(f"\nAPI Error: {error_msg}")
139
+ if "status" in error_msg:
140
+ raise ValueError(f"Azure OpenAI API error (HTTP {error_msg})")
141
+ raise ValueError(f"Azure OpenAI API error: {error_msg}")
142
+
143
+ except Exception as e:
144
+ if verbose:
145
+ print(f"\nProvider Error: {str(e)}")
146
+ raise ValueError(f"Azure OpenAI provider error: {str(e)}")
147
+
148
+
149
+ def generate_with_openai(
150
+ diff: str,
151
+ vuln_data: Optional[Dict[str, Any]],
152
+ model: str,
153
+ system_prompt: str,
154
+ verbose: bool = False,
155
+ ) -> str:
156
+ """Generate description using OpenAI."""
157
+ if verbose:
158
+ print("\nInitializing OpenAI client...")
159
+
160
+ client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
161
+
162
+ messages = [
163
+ {"role": "system", "content": system_prompt},
164
+ {"role": "user", "content": diff},
165
+ ]
166
+
167
+ if verbose:
168
+ print("\nSending request to OpenAI API:")
169
+ print(f" Model: {model}")
170
+ print(" Parameters:", json.dumps({"max_tokens": 1000, "temperature": 0.2}, indent=2))
171
+ print("\nRequest Messages:")
172
+ for msg in messages:
173
+ print(f"\n{msg['role'].upper()} MESSAGE:")
174
+ content = msg["content"]
175
+ if len(content) > 500:
176
+ print(content[:500] + "...")
177
+ else:
178
+ print(content)
179
+ print("\nMaking API call...")
180
+
181
+ try:
182
+ response = client.chat.completions.create(
183
+ model=model,
184
+ messages=messages,
185
+ max_tokens=1000,
186
+ temperature=0.2,
187
+ )
188
+ if verbose:
189
+ print("\nRaw API Response:")
190
+ print(f" Model: {response.model}")
191
+ print(f" Usage: {response.usage.model_dump() if response.usage else 'N/A'}")
192
+ print("\nResponse Content:")
193
+ return response.choices[0].message.content
194
+ except Exception as e:
195
+ if verbose:
196
+ print(f"\nAPI Error: {str(e)}")
197
+ raise ValueError(f"OpenAI API error: {str(e)}")
@@ -0,0 +1,152 @@
1
+ Metadata-Version: 2.4
2
+ Name: pr-generator-agent
3
+ Version: 1.0.0
4
+ Summary: AI-powered Pull Request Description Generator
5
+ Author-email: Daniel Scholl <daniel.scholl@microsoft.com>
6
+ License: MIT
7
+ License-File: LICENSE
8
+ Requires-Python: >=3.11
9
+ Requires-Dist: anthropic>=0.18.1
10
+ Requires-Dist: gitpython>=3.1.42
11
+ Requires-Dist: openai>=1.12.0
12
+ Requires-Dist: tiktoken>=0.6.0
13
+ Provides-Extra: dev
14
+ Requires-Dist: black>=24.2.0; extra == 'dev'
15
+ Requires-Dist: build>=1.0.3; extra == 'dev'
16
+ Requires-Dist: flake8>=7.0.0; extra == 'dev'
17
+ Requires-Dist: isort>=5.13.2; extra == 'dev'
18
+ Requires-Dist: pytest-cov>=4.1.0; extra == 'dev'
19
+ Requires-Dist: pytest>=8.0.2; extra == 'dev'
20
+ Description-Content-Type: text/markdown
21
+
22
+ # AIPR - Agentic Pull Request Description Generator
23
+
24
+ Automatically analyze git diffs and vulnerabilities to generate comprehensive, well-structured pull request descriptions. By intelligently detecting changes, performing security scans, and leveraging state-of-the-art AI models, AIPR helps teams save time while maintaining high-quality, consistent pull request descriptions.
25
+
26
+ ```bash
27
+ # Install with pipx (recommended)
28
+ pipx install pr-generator-agent
29
+
30
+ # Or with pip
31
+ pip install pr-generator-agent
32
+
33
+ # Set the environment variable for the API key
34
+ export ANTHROPIC_API_KEY="your-api-key"
35
+
36
+ # Generate a PR description
37
+ aipr
38
+
39
+ # Custom usage - Analyze changes against main branch
40
+ # Include: Vulnerability scanning
41
+ # Use: Azure OpenAI o1-mini model
42
+ # Prompt: meta template
43
+ # Ouptut: Verbose
44
+ aipr -t main --vulns -p meta -m azure/o1-mini -v
45
+
46
+ # Inline with merge request creation
47
+ gh pr create -b "$(aipr -s)" -t "feat: New Feature"
48
+ ```
49
+
50
+ ## Key Features
51
+
52
+ - 🔍 **Smart Detection**: Automatically analyzes working tree changes or compares branches
53
+ - 🛡️ **Security First**: Optional vulnerability scanning between branches using Trivy
54
+ - 🤖 **AI-Powered**: Multiple AI providers (Azure OpenAI, OpenAI, Anthropic) for optimal results
55
+ - 🔄 **CI/CD Ready**: Seamless integration with GitLab and GitHub workflows
56
+
57
+ ## Example Output
58
+
59
+ ```
60
+ Change Summary:
61
+
62
+ 1. **Added User Authentication**
63
+ - Implemented JWT middleware
64
+ - Added login/register endpoints
65
+ - Updated bcrypt to v5.1.1
66
+
67
+ 2. **Security Updates**
68
+ - Fixed 2 medium severity vulnerabilities
69
+ - Updated deprecated crypto functions
70
+
71
+ Security Analysis:
72
+ ✓ No new vulnerabilities introduced
73
+ ```
74
+
75
+ ## Requirements
76
+
77
+ - Python 3.10 or higher (3.10, 3.11 officially supported)
78
+ - Git
79
+ - LLM API Key (Anthropic, OpenAI, or Azure OpenAI)
80
+ - [Trivy](https://aquasecurity.github.io/trivy/latest/getting-started/installation/) (used for `--vulns` scanning)
81
+
82
+ ## Environment Variables
83
+
84
+ #### Anthropic (Default)
85
+ - `ANTHROPIC_API_KEY`: Anthropic API key
86
+
87
+ #### Azure OpenAI
88
+ - `AZURE_API_KEY`: Azure OpenAI API key
89
+ - `AZURE_API_BASE`: Azure endpoint URL
90
+ - `AZURE_API_VERSION`: API version (default: "2024-02-15-preview")
91
+
92
+ #### OpenAI
93
+ - `OPENAI_API_KEY`: OpenAI API key
94
+
95
+ ## Usage
96
+
97
+ ### Command Options
98
+ - `-t, --target`: Compare changes with specific branch (default: auto-detects main/master)
99
+ - `-p, --prompt`: Select prompt template (e.g., 'meta')
100
+ - `-v, --verbose`: Show API interaction details
101
+ - `-d, --debug`: Preview prompts without API calls
102
+ - `-s, --silent`: Output only the description
103
+ - `--vulns`: Include vulnerability scanning
104
+ - `-m, --model`: Specify AI model to use
105
+
106
+ The tool intelligently detects changes by:
107
+ 1. Using staged/unstaged changes if present
108
+ 2. Comparing against target branch if working tree is clean
109
+
110
+ ## Supported AI Models
111
+
112
+ Choose from multiple AI providers:
113
+
114
+ | Provider | Model | Notes |
115
+ |----------|--------|-------|
116
+ | Anthropic | `claude-3-sonnet` | default |
117
+ | | `claude-3.5-sonnet` | latest |
118
+ | | `claude-3.5-haiku` | latest |
119
+ | | `claude-3-opus` | latest |
120
+ | | `claude-3-haiku` | |
121
+ | | `claude` | alias for `claude-3-sonnet` |
122
+ | Azure OpenAI | `azure/o1-mini` | |
123
+ | | `azure/gpt-4o-mini` | |
124
+ | | `azure/gpt-4o` | |
125
+ | | `azure` | alias for `azure/gpt-4o-mini` |
126
+ | OpenAI | `gpt-4o` | |
127
+ | | `gpt-4-turbo` | |
128
+ | | `gpt-3.5-turbo` | |
129
+ | | `openai` | alias for `gpt-4o` |
130
+
131
+ ## Custom Prompts
132
+
133
+ AIPR supports custom prompt templates that allow you to tailor merge request descriptions to your team's specific needs. Custom prompts enable you to:
134
+ - Define consistent formatting across your team
135
+ - Include organization-specific requirements
136
+ - Add custom sections and validation rules
137
+ - Provide examples that match your team's standards
138
+
139
+ For detailed information on creating and using custom prompts, see our [Custom Prompts Tutorial](docs/custom_prompts.md).
140
+
141
+ ## Contributing
142
+
143
+ We welcome contributions! See our [Contributing Guide](CONTRIBUTING.md) for details on:
144
+ - Setting up your development environment
145
+ - Our development workflow
146
+ - Code style guidelines
147
+ - Pull request process
148
+ - Running tests
149
+
150
+ ## License
151
+
152
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
@@ -0,0 +1,11 @@
1
+ aipr/__init__.py,sha256=PW5IiHykwROknlAhWnyBPW5oPI01M3SwgKXrRX_oAv0,386
2
+ aipr/main.py,sha256=VcYm3zXEAe_kT2DvReWmjtwaNMcQdF2Um24qXm6GEUo,24102
3
+ aipr/providers.py,sha256=gyrGVFQYUhfoq3aTBnZEkcP9LY9e6mITZ39qt278yDk,6635
4
+ aipr/prompts/__init__.py,sha256=VR8pQpDV0l-929daoPTAWa5zTdXfZ-_dmV2rZXhOu3s,140
5
+ aipr/prompts/meta.xml,sha256=VLnvS85KEUG4Td0iqu4Bxs_qachMlHD1qqwFFuNx45M,2053
6
+ aipr/prompts/prompts.py,sha256=SZYlSGDbJ__-_EJQTBLWJCHdDSj0ekws1ujR9trygXE,7693
7
+ pr_generator_agent-1.0.0.dist-info/METADATA,sha256=tWo4MJYyPIAIr83nBQW_TDGPhg_53dc2tyOF1b5wKeE,4857
8
+ pr_generator_agent-1.0.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
9
+ pr_generator_agent-1.0.0.dist-info/entry_points.txt,sha256=Nx7jYZTtBlWrDKhHkPYD-UBs4xA-rr4Q-KTDzcpeIFU,40
10
+ pr_generator_agent-1.0.0.dist-info/licenses/LICENSE,sha256=IeC3i1PYZv6yj4Pj6rp2Nzs2-F70buj7UTFsmLUJvO8,1070
11
+ pr_generator_agent-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ aipr = aipr.main:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Daniel Scholl
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.