code-review-ai-cli 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.
- code_review_ai_cli-1.0.0.dist-info/METADATA +441 -0
- code_review_ai_cli-1.0.0.dist-info/RECORD +13 -0
- code_review_ai_cli-1.0.0.dist-info/WHEEL +5 -0
- code_review_ai_cli-1.0.0.dist-info/entry_points.txt +2 -0
- code_review_ai_cli-1.0.0.dist-info/top_level.txt +1 -0
- src/__init__.py +7 -0
- src/ai_review.py +946 -0
- src/config.py +361 -0
- src/formatter.py +474 -0
- src/git_utils.py +487 -0
- src/llm_client.py +1008 -0
- src/prompts/config.yaml.template +124 -0
- src/tfs_client.py +751 -0
src/config.py
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration Module - AI Code Review
|
|
3
|
+
=======================================
|
|
4
|
+
Manages system configuration exclusively from config.yaml.
|
|
5
|
+
|
|
6
|
+
Configuration priority:
|
|
7
|
+
1. CLI arguments (highest priority)
|
|
8
|
+
2. config.yaml file
|
|
9
|
+
3. Default values
|
|
10
|
+
|
|
11
|
+
Supported LLM providers:
|
|
12
|
+
- openai (GPT-4, GPT-4-turbo, GPT-4o)
|
|
13
|
+
- gemini (Google Gemini Pro, Gemini 1.5 Pro)
|
|
14
|
+
- claude (Anthropic Claude 3 Opus, Sonnet, Haiku)
|
|
15
|
+
- ollama (Local models via Ollama)
|
|
16
|
+
- azure_openai (Azure OpenAI Service)
|
|
17
|
+
- copilot (GitHub Copilot - GPT-4o, Claude, etc. via GitHub)
|
|
18
|
+
- bedrock (AWS Bedrock)
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import os
|
|
22
|
+
from dataclasses import dataclass, field
|
|
23
|
+
from typing import Optional
|
|
24
|
+
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
# Optional dependencies – loaded with safe fallback
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
try:
|
|
29
|
+
import yaml
|
|
30
|
+
_HAS_YAML = True
|
|
31
|
+
except ImportError:
|
|
32
|
+
_HAS_YAML = False
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
# Default models per provider
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
DEFAULT_MODELS = {
|
|
39
|
+
"openai": "gpt-4o",
|
|
40
|
+
"azure_openai": "gpt-4o",
|
|
41
|
+
"gemini": "gemini-1.5-pro",
|
|
42
|
+
"claude": "claude-3-5-sonnet-latest",
|
|
43
|
+
"ollama": "llama3",
|
|
44
|
+
"copilot": "gpt-4o",
|
|
45
|
+
"bedrock": "anthropic.claude-3-5-sonnet-20240620-v1:0",
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
VALID_PROVIDERS = list(DEFAULT_MODELS.keys())
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# ---------------------------------------------------------------------------
|
|
52
|
+
# Main configuration dataclass
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
@dataclass
|
|
55
|
+
class ReviewConfig:
|
|
56
|
+
"""Stores all configuration needed for AI Code Review."""
|
|
57
|
+
|
|
58
|
+
# --- LLM Provider -------------------------------------------------
|
|
59
|
+
llm_provider: str = "openai" # LLM provider to use
|
|
60
|
+
api_key: str = "" # Provider API key
|
|
61
|
+
api_base_url: str = "" # Base URL for APIs (override)
|
|
62
|
+
model: str = "" # Model to use (empty = provider default)
|
|
63
|
+
max_tokens: int = 4096
|
|
64
|
+
temperature: float = 0.3
|
|
65
|
+
|
|
66
|
+
# --- Provider-specific keys (alternatives) -------------------------
|
|
67
|
+
openai_api_key: str = ""
|
|
68
|
+
gemini_api_key: str = ""
|
|
69
|
+
anthropic_api_key: str = ""
|
|
70
|
+
ollama_base_url: str = "" # E.g., http://localhost:11434
|
|
71
|
+
github_token: str = "" # GitHub token for Copilot
|
|
72
|
+
bedrock_region: str = "" # E.g., us-east-1
|
|
73
|
+
bedrock_access_key_id: str = ""
|
|
74
|
+
bedrock_secret_access_key: str = ""
|
|
75
|
+
bedrock_session_token: str = ""
|
|
76
|
+
bedrock_profile: str = "" # Optional profile ~/.aws/credentials
|
|
77
|
+
|
|
78
|
+
# --- Azure DevOps / TFS ------------------------------------------
|
|
79
|
+
tfs_base_url: str = "" # E.g., https://tfs.company.com/tfs
|
|
80
|
+
tfs_collection: str = "DefaultCollection"
|
|
81
|
+
tfs_project: str = ""
|
|
82
|
+
tfs_pat: str = "" # Personal Access Token
|
|
83
|
+
tfs_verify_ssl: bool = True # Verify TLS certificates
|
|
84
|
+
tfs_ca_bundle: str = "" # Path to corporate CA bundle (.pem)
|
|
85
|
+
tfs_repository: str = "" # Default repository (empty = all)
|
|
86
|
+
|
|
87
|
+
# --- Review -------------------------------------------------------
|
|
88
|
+
review_language: str = "pt" # Review language (pt/en)
|
|
89
|
+
verbosity: str = "detailed" # "quick" | "detailed" | "security"
|
|
90
|
+
review_scope: str = "diff_only" # "diff_only" | "full_code"
|
|
91
|
+
max_diff_files: int = 50 # Max diff files sent to LLM
|
|
92
|
+
max_diff_lines: int = 2000 # Max diff lines
|
|
93
|
+
custom_prompt_file: str = "review_prompt.md" # Markdown file with extra rules/context
|
|
94
|
+
file_extensions_filter: list = field(default_factory=list)
|
|
95
|
+
|
|
96
|
+
# --- PR Review ----------------------------------------------------
|
|
97
|
+
auto_post_comments: bool = False # Post comments automatically
|
|
98
|
+
dry_run: bool = False # Review without posting
|
|
99
|
+
pr_comment_mode: str = "structured" # "structured" | "general"
|
|
100
|
+
|
|
101
|
+
# --- Output -------------------------------------------------------
|
|
102
|
+
output_format: str = "terminal" # "terminal" | "markdown" | "json"
|
|
103
|
+
output_file: str = "" # Path to save output
|
|
104
|
+
color_output: bool = True # Terminal colors
|
|
105
|
+
|
|
106
|
+
def get_effective_model(self) -> str:
|
|
107
|
+
"""Returns the effective model (configured or provider default)."""
|
|
108
|
+
if self.model:
|
|
109
|
+
return self.model
|
|
110
|
+
return DEFAULT_MODELS.get(self.llm_provider, "gpt-4o")
|
|
111
|
+
|
|
112
|
+
def get_effective_api_key(self) -> str:
|
|
113
|
+
"""Returns the effective API key for the current provider."""
|
|
114
|
+
if self.api_key:
|
|
115
|
+
return self.api_key
|
|
116
|
+
|
|
117
|
+
provider = self.llm_provider.lower()
|
|
118
|
+
if provider == "openai" or provider == "azure_openai":
|
|
119
|
+
return self.openai_api_key
|
|
120
|
+
elif provider == "gemini":
|
|
121
|
+
return self.gemini_api_key
|
|
122
|
+
elif provider == "claude":
|
|
123
|
+
return self.anthropic_api_key
|
|
124
|
+
elif provider == "copilot":
|
|
125
|
+
return self.github_token
|
|
126
|
+
elif provider == "ollama":
|
|
127
|
+
return "" # Ollama does not require an API key
|
|
128
|
+
elif provider == "bedrock":
|
|
129
|
+
return "" # Bedrock uses AWS credentials in dedicated fields
|
|
130
|
+
return ""
|
|
131
|
+
|
|
132
|
+
def get_effective_base_url(self) -> str:
|
|
133
|
+
"""Returns the effective base URL for the current provider."""
|
|
134
|
+
if self.api_base_url:
|
|
135
|
+
return self.api_base_url
|
|
136
|
+
|
|
137
|
+
provider = self.llm_provider.lower()
|
|
138
|
+
if provider == "ollama":
|
|
139
|
+
return self.ollama_base_url or "http://localhost:11434"
|
|
140
|
+
elif provider == "copilot":
|
|
141
|
+
return "https://models.github.ai/inference"
|
|
142
|
+
return ""
|
|
143
|
+
|
|
144
|
+
@classmethod
|
|
145
|
+
def load(cls, config_path: Optional[str] = None) -> "ReviewConfig":
|
|
146
|
+
"""
|
|
147
|
+
Loads configuration with the following priority:
|
|
148
|
+
1. config.yaml file
|
|
149
|
+
2. Default values
|
|
150
|
+
"""
|
|
151
|
+
cfg = cls()
|
|
152
|
+
|
|
153
|
+
# --- Load config.yaml if it exists ---
|
|
154
|
+
yaml_path = config_path or _find_file("config.yaml")
|
|
155
|
+
if yaml_path and os.path.isfile(yaml_path):
|
|
156
|
+
cfg._load_yaml(yaml_path)
|
|
157
|
+
|
|
158
|
+
# --- Resolve effective model and API key ---
|
|
159
|
+
if not cfg.model:
|
|
160
|
+
cfg.model = cfg.get_effective_model()
|
|
161
|
+
if not cfg.api_key:
|
|
162
|
+
cfg.api_key = cfg.get_effective_api_key()
|
|
163
|
+
if not cfg.api_base_url:
|
|
164
|
+
cfg.api_base_url = cfg.get_effective_base_url()
|
|
165
|
+
|
|
166
|
+
return cfg
|
|
167
|
+
|
|
168
|
+
# ------------------------------------------------------------------
|
|
169
|
+
# Private methods
|
|
170
|
+
# ------------------------------------------------------------------
|
|
171
|
+
def _load_yaml(self, path: str) -> None:
|
|
172
|
+
"""Loads values from the YAML file."""
|
|
173
|
+
if not _HAS_YAML:
|
|
174
|
+
print("[WARNING] PyYAML not installed. Ignoring config.yaml.")
|
|
175
|
+
return
|
|
176
|
+
try:
|
|
177
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
178
|
+
data = yaml.safe_load(f) or {}
|
|
179
|
+
except Exception as exc:
|
|
180
|
+
print(f"[WARNING] Error reading {path}: {exc}")
|
|
181
|
+
return
|
|
182
|
+
|
|
183
|
+
mapping = {
|
|
184
|
+
"llm_provider": ("llm", "provider"),
|
|
185
|
+
"api_key": ("llm", "api_key"),
|
|
186
|
+
"api_base_url": ("llm", "api_base_url"),
|
|
187
|
+
"model": ("llm", "model"),
|
|
188
|
+
"max_tokens": ("llm", "max_tokens"),
|
|
189
|
+
"temperature": ("llm", "temperature"),
|
|
190
|
+
# Provider-specific
|
|
191
|
+
"openai_api_key": ("openai", "api_key"),
|
|
192
|
+
"gemini_api_key": ("gemini", "api_key"),
|
|
193
|
+
"anthropic_api_key": ("claude", "api_key"),
|
|
194
|
+
"ollama_base_url": ("ollama", "base_url"),
|
|
195
|
+
"github_token": ("copilot", "github_token"),
|
|
196
|
+
"bedrock_region": ("bedrock", "region"),
|
|
197
|
+
"bedrock_access_key_id": ("bedrock", "access_key_id"),
|
|
198
|
+
"bedrock_secret_access_key": ("bedrock", "secret_access_key"),
|
|
199
|
+
"bedrock_session_token": ("bedrock", "session_token"),
|
|
200
|
+
"bedrock_profile": ("bedrock", "profile"),
|
|
201
|
+
# TFS
|
|
202
|
+
"tfs_base_url": ("tfs", "base_url"),
|
|
203
|
+
"tfs_collection": ("tfs", "collection"),
|
|
204
|
+
"tfs_project": ("tfs", "project"),
|
|
205
|
+
"tfs_pat": ("tfs", "pat"),
|
|
206
|
+
"tfs_verify_ssl": ("tfs", "verify_ssl"),
|
|
207
|
+
"tfs_ca_bundle": ("tfs", "ca_bundle"),
|
|
208
|
+
"tfs_repository": ("tfs", "repository"),
|
|
209
|
+
# Review
|
|
210
|
+
"review_language": ("review", "language"),
|
|
211
|
+
"verbosity": ("review", "verbosity"),
|
|
212
|
+
"review_scope": ("review", "scope"),
|
|
213
|
+
"max_diff_files": ("review", "max_diff_files"),
|
|
214
|
+
"max_diff_lines": ("review", "max_diff_lines"),
|
|
215
|
+
"custom_prompt_file": ("review", "custom_prompt_file"),
|
|
216
|
+
"file_extensions_filter": ("review", "file_extensions_filter"),
|
|
217
|
+
# PR
|
|
218
|
+
"auto_post_comments": ("pr", "auto_post_comments"),
|
|
219
|
+
"dry_run": ("pr", "dry_run"),
|
|
220
|
+
"pr_comment_mode": ("pr", "comment_mode"),
|
|
221
|
+
# Output
|
|
222
|
+
"output_format": ("output", "format"),
|
|
223
|
+
"output_file": ("output", "file"),
|
|
224
|
+
"color_output": ("output", "color"),
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
for attr, keys in mapping.items():
|
|
228
|
+
val = data
|
|
229
|
+
for k in keys:
|
|
230
|
+
if isinstance(val, dict):
|
|
231
|
+
val = val.get(k)
|
|
232
|
+
else:
|
|
233
|
+
val = None
|
|
234
|
+
break
|
|
235
|
+
if val is not None:
|
|
236
|
+
setattr(self, attr, val)
|
|
237
|
+
|
|
238
|
+
def validate(self) -> list[str]:
|
|
239
|
+
"""Validates the configuration and returns a list of warnings/errors."""
|
|
240
|
+
issues: list[str] = []
|
|
241
|
+
|
|
242
|
+
provider = self.llm_provider.lower()
|
|
243
|
+
|
|
244
|
+
if provider not in VALID_PROVIDERS:
|
|
245
|
+
issues.append(
|
|
246
|
+
f"Unknown provider: '{provider}'.\n"
|
|
247
|
+
f" Valid providers: {', '.join(VALID_PROVIDERS)}"
|
|
248
|
+
)
|
|
249
|
+
return issues
|
|
250
|
+
|
|
251
|
+
# Validate API key per provider
|
|
252
|
+
if provider == "ollama":
|
|
253
|
+
# Ollama does not need an API key, but needs a URL
|
|
254
|
+
pass
|
|
255
|
+
elif provider in ("openai", "azure_openai"):
|
|
256
|
+
if not self.api_key and not self.openai_api_key:
|
|
257
|
+
issues.append(
|
|
258
|
+
f"Provider '{provider}' requires an API key in config.yaml.\n"
|
|
259
|
+
" Configure llm.api_key or openai.api_key."
|
|
260
|
+
)
|
|
261
|
+
if provider == "azure_openai" and not self.api_base_url:
|
|
262
|
+
issues.append(
|
|
263
|
+
"Azure OpenAI requires API_BASE_URL to be configured.\n"
|
|
264
|
+
" E.g., https://your-resource.openai.azure.com/openai/deployments/your-deploy"
|
|
265
|
+
)
|
|
266
|
+
elif provider == "gemini":
|
|
267
|
+
if not self.api_key and not self.gemini_api_key:
|
|
268
|
+
issues.append(
|
|
269
|
+
"Provider 'gemini' requires an API key in config.yaml.\n"
|
|
270
|
+
" Get it at: https://aistudio.google.com/app/apikey\n"
|
|
271
|
+
" Configure llm.api_key or gemini.api_key."
|
|
272
|
+
)
|
|
273
|
+
elif provider == "claude":
|
|
274
|
+
if not self.api_key and not self.anthropic_api_key:
|
|
275
|
+
issues.append(
|
|
276
|
+
"Provider 'claude' requires an API key in config.yaml.\n"
|
|
277
|
+
" Get it at: https://console.anthropic.com/settings/keys\n"
|
|
278
|
+
" Configure llm.api_key or claude.api_key."
|
|
279
|
+
)
|
|
280
|
+
elif provider == "copilot":
|
|
281
|
+
if not self.api_key and not self.github_token:
|
|
282
|
+
issues.append(
|
|
283
|
+
"Provider 'copilot' requires a GitHub token in config.yaml.\n"
|
|
284
|
+
" Create at: https://github.com/settings/tokens\n"
|
|
285
|
+
" Configure llm.api_key or copilot.github_token.\n"
|
|
286
|
+
" Requires an active GitHub Copilot subscription."
|
|
287
|
+
)
|
|
288
|
+
elif provider == "bedrock":
|
|
289
|
+
if not self.bedrock_region:
|
|
290
|
+
issues.append(
|
|
291
|
+
"Provider 'bedrock' requires an AWS region.\n"
|
|
292
|
+
" Configure bedrock.region in config.yaml (e.g., us-east-1)."
|
|
293
|
+
)
|
|
294
|
+
if self.bedrock_access_key_id and not self.bedrock_secret_access_key:
|
|
295
|
+
issues.append(
|
|
296
|
+
"Provider 'bedrock': secret_access_key is missing.\n"
|
|
297
|
+
" Configure bedrock.secret_access_key when access_key_id is set."
|
|
298
|
+
)
|
|
299
|
+
if self.bedrock_secret_access_key and not self.bedrock_access_key_id:
|
|
300
|
+
issues.append(
|
|
301
|
+
"Provider 'bedrock': access_key_id is missing.\n"
|
|
302
|
+
" Configure bedrock.access_key_id when secret_access_key is set."
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
if self.verbosity not in ("quick", "detailed", "security"):
|
|
306
|
+
issues.append(
|
|
307
|
+
f"Invalid verbosity: '{self.verbosity}'. "
|
|
308
|
+
"Use 'quick', 'detailed' or 'security'."
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
if self.review_scope not in ("diff_only", "full_code"):
|
|
312
|
+
issues.append(
|
|
313
|
+
f"Invalid review scope: '{self.review_scope}'. "
|
|
314
|
+
"Use 'diff_only' or 'full_code'."
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
if self.max_diff_files <= 0:
|
|
318
|
+
issues.append(
|
|
319
|
+
f"Invalid max_diff_files: '{self.max_diff_files}'. "
|
|
320
|
+
"Use an integer greater than 0."
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
if self.max_diff_lines <= 0:
|
|
324
|
+
issues.append(
|
|
325
|
+
f"Invalid max_diff_lines: '{self.max_diff_lines}'. "
|
|
326
|
+
"Use an integer greater than 0."
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
return issues
|
|
330
|
+
|
|
331
|
+
def get_provider_info(self) -> str:
|
|
332
|
+
"""Returns formatted information about the configured provider."""
|
|
333
|
+
provider = self.llm_provider
|
|
334
|
+
model = self.get_effective_model()
|
|
335
|
+
has_key = bool(self.api_key or self.get_effective_api_key())
|
|
336
|
+
|
|
337
|
+
if provider == "ollama":
|
|
338
|
+
url = self.get_effective_base_url()
|
|
339
|
+
return f"{provider} | {model} | URL: {url}"
|
|
340
|
+
if provider == "bedrock":
|
|
341
|
+
creds_mode = "profile" if self.bedrock_profile else "explicit/default"
|
|
342
|
+
region = self.bedrock_region or "(not configured)"
|
|
343
|
+
return f"{provider} | {model} | Region: {region} | Credentials: {creds_mode}"
|
|
344
|
+
else:
|
|
345
|
+
key_status = "✅ Configured" if has_key else "❌ Not configured"
|
|
346
|
+
return f"{provider} | {model} | API Key: {key_status}"
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
# ---------------------------------------------------------------------------
|
|
350
|
+
# Helpers
|
|
351
|
+
# ---------------------------------------------------------------------------
|
|
352
|
+
def _find_file(name: str) -> Optional[str]:
|
|
353
|
+
"""Searches for a file in the current directory and the script directory."""
|
|
354
|
+
candidates = [
|
|
355
|
+
os.path.join(os.getcwd(), name),
|
|
356
|
+
os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", name),
|
|
357
|
+
]
|
|
358
|
+
for c in candidates:
|
|
359
|
+
if os.path.isfile(c):
|
|
360
|
+
return os.path.abspath(c)
|
|
361
|
+
return None
|