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.
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