code-review-ai-cli 2.2.4__py3-none-any.whl → 2.2.6__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-2.2.4.dist-info → code_review_ai_cli-2.2.6.dist-info}/METADATA +3 -4
- code_review_ai_cli-2.2.6.dist-info/RECORD +17 -0
- src/config.py +2 -2
- src/context_extractor.py +96 -190
- src/git_utils.py +23 -11
- src/llm_client.py +121 -106
- src/prompts/config.yaml.template +4 -4
- src/prompts/review_prompt.md.template +8 -72
- src/tfs_client.py +9 -13
- code_review_ai_cli-2.2.4.dist-info/RECORD +0 -17
- {code_review_ai_cli-2.2.4.dist-info → code_review_ai_cli-2.2.6.dist-info}/WHEEL +0 -0
- {code_review_ai_cli-2.2.4.dist-info → code_review_ai_cli-2.2.6.dist-info}/entry_points.txt +0 -0
- {code_review_ai_cli-2.2.4.dist-info → code_review_ai_cli-2.2.6.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: code-review-ai-cli
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.6
|
|
4
4
|
Summary: Automated AI-powered code review CLI for Azure DevOps / TFS Pull Requests
|
|
5
5
|
License: MIT
|
|
6
6
|
Keywords: code-review,ai,azure-devops,tfs,pull-request,llm
|
|
@@ -19,6 +19,8 @@ Requires-Dist: requests>=2.28.0
|
|
|
19
19
|
Requires-Dist: PyYAML>=6.0
|
|
20
20
|
Requires-Dist: litellm>=1.40.0
|
|
21
21
|
Requires-Dist: tiktoken>=0.7.0
|
|
22
|
+
Requires-Dist: tree-sitter<0.22,>=0.20
|
|
23
|
+
Requires-Dist: tree-sitter-languages>=1.10.2
|
|
22
24
|
Provides-Extra: bedrock
|
|
23
25
|
Requires-Dist: boto3>=1.34.0; extra == "bedrock"
|
|
24
26
|
Provides-Extra: openai
|
|
@@ -27,8 +29,6 @@ Provides-Extra: gemini
|
|
|
27
29
|
Requires-Dist: google-generativeai>=0.3.0; extra == "gemini"
|
|
28
30
|
Provides-Extra: claude
|
|
29
31
|
Requires-Dist: anthropic>=0.18.0; extra == "claude"
|
|
30
|
-
Provides-Extra: tree-sitter
|
|
31
|
-
Requires-Dist: tree-sitter-languages>=1.10.2; extra == "tree-sitter"
|
|
32
32
|
Provides-Extra: all
|
|
33
33
|
Requires-Dist: boto3>=1.34.0; extra == "all"
|
|
34
34
|
Requires-Dist: litellm>=1.40.0; extra == "all"
|
|
@@ -36,7 +36,6 @@ Requires-Dist: tiktoken>=0.7.0; extra == "all"
|
|
|
36
36
|
Requires-Dist: openai>=1.0.0; extra == "all"
|
|
37
37
|
Requires-Dist: google-generativeai>=0.3.0; extra == "all"
|
|
38
38
|
Requires-Dist: anthropic>=0.18.0; extra == "all"
|
|
39
|
-
Requires-Dist: tree-sitter-languages>=1.10.2; extra == "all"
|
|
40
39
|
Provides-Extra: dev
|
|
41
40
|
Requires-Dist: pytest>=8.3.0; extra == "dev"
|
|
42
41
|
Requires-Dist: pytest-cov>=5.0.0; extra == "dev"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
src/__init__.py,sha256=BNfzb1HoMlgLc9MNEzc8Ne7USol7NHRUcMn7ISQ41sY,259
|
|
2
|
+
src/ai_review.py,sha256=Wfeo72uTsjuoKMW5ToqFi6edfVIWTMgqPIbv9_W8Z-Y,34482
|
|
3
|
+
src/config.py,sha256=GFqGfjnhjY0O6EiDTsFFCM3qvSCVZMPd3IbmWd4SOWk,16419
|
|
4
|
+
src/context_extractor.py,sha256=SyuTtau1eIVmjOkqmV_8ijawqDiGK_M7G9May99oBAo,20928
|
|
5
|
+
src/formatter.py,sha256=68kz1U6tMndLcmtJRImN4jAIpxXtog3cKtiJ-zc3S0M,19027
|
|
6
|
+
src/git_utils.py,sha256=sJPV-NpSx0up-KDvFK1Pn73mvGdSNiCqTjEj6I8kzpM,10218
|
|
7
|
+
src/llm_client.py,sha256=dVVkJ1glf1K-I_oWZ-qxMABaBLQ4gq4blOiM5lvFEp0,54393
|
|
8
|
+
src/prompt_utils.py,sha256=FnWgHQ7UW8dEKW-4R7ngpL6DNqbW7xFEKi5JsZ7348E,2045
|
|
9
|
+
src/tfs_client.py,sha256=Y7MIu5dutZ2J223yLJJPai3pbux5dnB-MoK_SQe-08I,37459
|
|
10
|
+
src/token_estimator.py,sha256=4_4TXhefaNi7KHomwhhWbt7GB6KfubCQVBEbD3YoK5Q,3192
|
|
11
|
+
src/prompts/config.yaml.template,sha256=p5ZolxDPmht_RXaS1iLcPqpK1zle20i5eAnWeKuKT2U,4878
|
|
12
|
+
src/prompts/review_prompt.md.template,sha256=OWlSqCIkJA6Q8fUuNNrI61B7YyfrRhDLy7nUfw46rJQ,2903
|
|
13
|
+
code_review_ai_cli-2.2.6.dist-info/METADATA,sha256=VQr3a3CZ9eIhkvjJ8Bt_Y3k3SNJRAoNlsIwvCkRiNYY,4966
|
|
14
|
+
code_review_ai_cli-2.2.6.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
15
|
+
code_review_ai_cli-2.2.6.dist-info/entry_points.txt,sha256=hzKqoXIQVpiCe4H-XV0DZcKwJY6Uj3M-WvBMEQtwr3o,49
|
|
16
|
+
code_review_ai_cli-2.2.6.dist-info/top_level.txt,sha256=74rtVfumQlgAPzR5_2CgYN24MB0XARCg0t-gzk6gTrM,4
|
|
17
|
+
code_review_ai_cli-2.2.6.dist-info/RECORD,,
|
src/config.py
CHANGED
|
@@ -92,8 +92,8 @@ class ReviewConfig:
|
|
|
92
92
|
review_scope: str = "diff_only" # "diff_only" | "full_code"
|
|
93
93
|
max_diff_files: int = 50 # Max diff files sent to LLM
|
|
94
94
|
max_diff_lines: int = 2000 # Max diff lines
|
|
95
|
-
max_scope_blocks: int =
|
|
96
|
-
max_scope_lines: int = 250 # Max lines in a
|
|
95
|
+
max_scope_blocks: int = 8 # Max changed blocks for Enclosing Scopes strategy
|
|
96
|
+
max_scope_lines: int = 250 # Max lines in a scope before Adaptive Diff fallback
|
|
97
97
|
adaptive_context_lines: int = 10 # Context lines for Adaptive Diff fallback
|
|
98
98
|
custom_prompt_file: str = "review_prompt.md" # Markdown file with extra rules/context
|
|
99
99
|
file_extensions_filter: list = field(default_factory=list) # File extensions to include (empty = all)
|
src/context_extractor.py
CHANGED
|
@@ -7,20 +7,19 @@ Context Extractor Module - AI Code Review
|
|
|
7
7
|
Provides AST-based context extraction for diff payloads sent to the LLM.
|
|
8
8
|
|
|
9
9
|
Strategy selection per file:
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
|
|
13
|
-
- Tree-sitter unavailable / language unsupported → Adaptive Diff fallback
|
|
10
|
+
- AST scopes within the configured limits → Enclosing Scopes
|
|
11
|
+
- Tree-sitter unavailable, unsupported language, parse failure, or scopes
|
|
12
|
+
outside the configured limits → Adaptive Diff fallback
|
|
14
13
|
|
|
15
14
|
Output is always wrapped in XML tags so the LLM can distinguish code context
|
|
16
15
|
from the actual diff:
|
|
17
16
|
|
|
18
17
|
``<enclosing_scopes file="src/auth.py">...</enclosing_scopes>``
|
|
19
|
-
``<file_skeleton file="src/module.py">...</file_skeleton>``
|
|
20
18
|
``<adaptive_diff file="src/other.py">...</adaptive_diff>``
|
|
21
19
|
|
|
22
|
-
Tree-sitter is
|
|
23
|
-
|
|
20
|
+
Tree-sitter is installed as a core dependency. The availability probe remains
|
|
21
|
+
defensive: reduced or broken environments fall back to
|
|
22
|
+
:func:`_build_adaptive_diff` instead of failing the review.
|
|
24
23
|
"""
|
|
25
24
|
|
|
26
25
|
from __future__ import annotations
|
|
@@ -68,13 +67,12 @@ except Exception: # pragma: no cover — optional dependency
|
|
|
68
67
|
# Constants
|
|
69
68
|
# ---------------------------------------------------------------------------
|
|
70
69
|
|
|
71
|
-
#: Maximum number of distinct logical blocks
|
|
72
|
-
#:
|
|
73
|
-
|
|
74
|
-
MAX_SCOPE_BLOCKS: int = 3
|
|
70
|
+
#: Maximum number of distinct logical blocks included as Enclosing Scopes.
|
|
71
|
+
#: When more blocks are detected the extractor uses Adaptive Diff instead.
|
|
72
|
+
MAX_SCOPE_BLOCKS: int = 8
|
|
75
73
|
|
|
76
74
|
#: Maximum number of lines in a single enclosing scope. When a scope
|
|
77
|
-
#: exceeds this limit the strategy falls back to
|
|
75
|
+
#: exceeds this limit the strategy falls back to Adaptive Diff to avoid
|
|
78
76
|
#: sending large class bodies to the LLM.
|
|
79
77
|
MAX_SCOPE_LINES: int = 250
|
|
80
78
|
|
|
@@ -370,147 +368,76 @@ def _extract_enclosing_scopes_ts(
|
|
|
370
368
|
return scopes
|
|
371
369
|
|
|
372
370
|
|
|
373
|
-
def
|
|
374
|
-
"""
|
|
375
|
-
lines: list[str] = []
|
|
376
|
-
for offset, src_line in enumerate(scope.source_lines):
|
|
377
|
-
abs_line = scope.start_line + offset
|
|
378
|
-
lines.append(f"{abs_line:4d}: {src_line}")
|
|
379
|
-
return "\n".join(lines)
|
|
380
|
-
|
|
371
|
+
def _non_whitespace_changed_lines(content: str, changed_lines: list[int]) -> list[int]:
|
|
372
|
+
"""Return changed lines that contain meaningful code in the new file.
|
|
381
373
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
#: Regex patterns used to identify structural lines in each language.
|
|
387
|
-
_SKELETON_PATTERNS: dict[str, list[str]] = {
|
|
388
|
-
"python": [
|
|
389
|
-
r"^\s*(?:import|from)\s+", # imports
|
|
390
|
-
r"^\s*(?:def|async\s+def|class)\s+", # definitions
|
|
391
|
-
r"^\s*@\w", # decorators
|
|
392
|
-
],
|
|
393
|
-
"javascript": [
|
|
394
|
-
r"^\s*(?:import|export|require)\s*",
|
|
395
|
-
r"^\s*(?:function|class|const|let|var)\s+\w+\s*(?:=\s*(?:function|\(|\()|\()",
|
|
396
|
-
r"^\s*(?:async\s+function|export\s+(?:default\s+)?(?:function|class))",
|
|
397
|
-
],
|
|
398
|
-
"typescript": [
|
|
399
|
-
r"^\s*(?:import|export|require)\s*",
|
|
400
|
-
r"^\s*(?:function|class|const|let|var|interface|type|enum|abstract)\s+\w+",
|
|
401
|
-
r"^\s*(?:async\s+function|export\s+(?:default\s+)?(?:function|class|interface))",
|
|
402
|
-
],
|
|
403
|
-
"tsx": [
|
|
404
|
-
r"^\s*(?:import|export|require)\s*",
|
|
405
|
-
r"^\s*(?:function|class|const|let|var|interface|type|enum|abstract)\s+\w+",
|
|
406
|
-
],
|
|
407
|
-
"c_sharp": [
|
|
408
|
-
r"^\s*(?:using)\s+",
|
|
409
|
-
r"^\s*(?:namespace|public|private|protected|internal|static|sealed|abstract|partial|override|virtual)\s+",
|
|
410
|
-
r"^\s*(?:\[[\w\.\(\)\"]+\])", # attributes
|
|
411
|
-
],
|
|
412
|
-
"java": [
|
|
413
|
-
r"^\s*(?:import|package)\s+",
|
|
414
|
-
r"^\s*(?:public|private|protected|static|final|abstract|synchronized|@)\s*",
|
|
415
|
-
],
|
|
416
|
-
"sql": [], # SQL skeleton falls back to individual statements
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
# Body-start patterns (lines after which we can insert "...")
|
|
420
|
-
_BODY_START: dict[str, list[str]] = {
|
|
421
|
-
"python": [r":\s*$"],
|
|
422
|
-
"javascript": [r"\{\s*$"],
|
|
423
|
-
"typescript": [r"\{\s*$"],
|
|
424
|
-
"tsx": [r"\{\s*$"],
|
|
425
|
-
"c_sharp": [r"\{\s*$"],
|
|
426
|
-
"java": [r"\{\s*$"],
|
|
427
|
-
"sql": [],
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
def _build_file_skeleton(
|
|
432
|
-
content: str,
|
|
433
|
-
changed_lines: list[int],
|
|
434
|
-
lang: str,
|
|
435
|
-
) -> str:
|
|
436
|
-
"""Build a file skeleton: structural lines + changed blocks + ``...`` placeholders.
|
|
437
|
-
|
|
438
|
-
The skeleton includes:
|
|
439
|
-
- All structural/declaration lines (imports, signatures, class headers)
|
|
440
|
-
- Full content of lines inside changed regions (± ADAPTIVE_CONTEXT_LINES)
|
|
441
|
-
- ``...`` for bodies of unchanged functions/methods
|
|
442
|
-
|
|
443
|
-
Args:
|
|
444
|
-
content: Full source file content.
|
|
445
|
-
changed_lines: 1-based line numbers that were changed.
|
|
446
|
-
lang: Tree-sitter language name for pattern selection.
|
|
447
|
-
|
|
448
|
-
Returns:
|
|
449
|
-
Formatted skeleton string with 1-based line numbers.
|
|
374
|
+
Whitespace-only additions must not influence AST scope selection. For
|
|
375
|
+
example, a blank line immediately after a method belongs to its containing
|
|
376
|
+
class and would otherwise cause the entire class to be selected.
|
|
450
377
|
"""
|
|
451
378
|
all_lines = content.splitlines()
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
always: set[int] = set()
|
|
459
|
-
for ln in changed_lines:
|
|
460
|
-
lo = max(1, ln - ADAPTIVE_CONTEXT_LINES)
|
|
461
|
-
hi = min(total, ln + ADAPTIVE_CONTEXT_LINES)
|
|
462
|
-
always.update(range(lo, hi + 1))
|
|
463
|
-
|
|
464
|
-
# Structural patterns for this language
|
|
465
|
-
struct_patterns = [re.compile(p) for p in _SKELETON_PATTERNS.get(lang, [])]
|
|
466
|
-
|
|
467
|
-
result_lines: list[str] = []
|
|
468
|
-
skipping = False
|
|
469
|
-
prev_shown: Optional[int] = None
|
|
470
|
-
|
|
471
|
-
for idx, src in enumerate(all_lines, start=1):
|
|
472
|
-
is_structural = any(p.search(src) for p in struct_patterns)
|
|
473
|
-
in_changed = idx in always
|
|
474
|
-
|
|
475
|
-
if in_changed or is_structural:
|
|
476
|
-
if skipping and prev_shown is not None:
|
|
477
|
-
result_lines.append(" ...")
|
|
478
|
-
skipping = False
|
|
479
|
-
result_lines.append(f"{idx:4d}: {src}")
|
|
480
|
-
prev_shown = idx
|
|
481
|
-
else:
|
|
482
|
-
skipping = True
|
|
483
|
-
|
|
484
|
-
if skipping:
|
|
485
|
-
result_lines.append(" ...")
|
|
379
|
+
return [
|
|
380
|
+
line_number
|
|
381
|
+
for line_number in changed_lines
|
|
382
|
+
if 1 <= line_number <= len(all_lines)
|
|
383
|
+
and all_lines[line_number - 1].strip()
|
|
384
|
+
]
|
|
486
385
|
|
|
487
|
-
return "\n".join(result_lines)
|
|
488
386
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
# SQL-specific extraction
|
|
492
|
-
# ---------------------------------------------------------------------------
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
def _extract_sql_statement(
|
|
496
|
-
content: str,
|
|
387
|
+
def _remove_redundant_ancestor_scopes(
|
|
388
|
+
scopes: list[ScopeInfo],
|
|
497
389
|
changed_lines: list[int],
|
|
498
|
-
lang: str,
|
|
499
390
|
) -> list[ScopeInfo]:
|
|
500
|
-
"""
|
|
391
|
+
"""Drop ancestor scopes whose changed lines are covered by child scopes.
|
|
501
392
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
393
|
+
An ancestor is retained when it contains at least one meaningful changed
|
|
394
|
+
line outside every selected descendant. This avoids duplicate class and
|
|
395
|
+
method bodies without losing class-level changes.
|
|
396
|
+
"""
|
|
397
|
+
retained: list[ScopeInfo] = []
|
|
398
|
+
|
|
399
|
+
for scope in scopes:
|
|
400
|
+
descendants = [
|
|
401
|
+
candidate
|
|
402
|
+
for candidate in scopes
|
|
403
|
+
if candidate is not scope
|
|
404
|
+
and scope.start_line <= candidate.start_line
|
|
405
|
+
and candidate.end_line <= scope.end_line
|
|
406
|
+
and (
|
|
407
|
+
scope.start_line < candidate.start_line
|
|
408
|
+
or candidate.end_line < scope.end_line
|
|
409
|
+
)
|
|
410
|
+
]
|
|
411
|
+
scope_changes = [
|
|
412
|
+
line_number
|
|
413
|
+
for line_number in changed_lines
|
|
414
|
+
if scope.start_line <= line_number <= scope.end_line
|
|
415
|
+
]
|
|
416
|
+
changes_covered_by_descendants = (
|
|
417
|
+
bool(scope_changes)
|
|
418
|
+
and bool(descendants)
|
|
419
|
+
and all(
|
|
420
|
+
any(
|
|
421
|
+
child.start_line <= line_number <= child.end_line
|
|
422
|
+
for child in descendants
|
|
423
|
+
)
|
|
424
|
+
for line_number in scope_changes
|
|
425
|
+
)
|
|
426
|
+
)
|
|
506
427
|
|
|
507
|
-
|
|
508
|
-
|
|
428
|
+
if not changes_covered_by_descendants:
|
|
429
|
+
retained.append(scope)
|
|
509
430
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
431
|
+
return retained
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def _format_scope_block(scope: ScopeInfo) -> str:
|
|
435
|
+
"""Render a :class:`ScopeInfo` with absolute 1-based line numbers."""
|
|
436
|
+
lines: list[str] = []
|
|
437
|
+
for offset, src_line in enumerate(scope.source_lines):
|
|
438
|
+
abs_line = scope.start_line + offset
|
|
439
|
+
lines.append(f"{abs_line:4d}: {src_line}")
|
|
440
|
+
return "\n".join(lines)
|
|
514
441
|
|
|
515
442
|
|
|
516
443
|
# ---------------------------------------------------------------------------
|
|
@@ -533,9 +460,10 @@ class ContextExtractor:
|
|
|
533
460
|
The returned *xml_block* is ready to be embedded in the LLM user message.
|
|
534
461
|
|
|
535
462
|
Attributes:
|
|
536
|
-
max_scope_blocks:
|
|
463
|
+
max_scope_blocks: Maximum number of AST scopes before falling back to
|
|
464
|
+
adaptive_diff.
|
|
537
465
|
max_scope_lines: Maximum lines per scope block before falling back to
|
|
538
|
-
|
|
466
|
+
adaptive_diff (avoids sending entire large classes to the LLM).
|
|
539
467
|
adaptive_context_lines: Lines of context for the fallback strategy.
|
|
540
468
|
"""
|
|
541
469
|
|
|
@@ -565,9 +493,8 @@ class ContextExtractor:
|
|
|
565
493
|
|
|
566
494
|
1. If Tree-sitter is unavailable → ``adaptive_diff``
|
|
567
495
|
2. If the language is not supported → ``adaptive_diff``
|
|
568
|
-
3. If the
|
|
569
|
-
4.
|
|
570
|
-
5. Otherwise → ``file_skeleton``
|
|
496
|
+
3. If the extracted scopes fit the configured limits → ``enclosing_scopes``
|
|
497
|
+
4. Otherwise → ``adaptive_diff``
|
|
571
498
|
|
|
572
499
|
Args:
|
|
573
500
|
file_path: Path (or name) of the file being reviewed.
|
|
@@ -576,9 +503,8 @@ class ContextExtractor:
|
|
|
576
503
|
|
|
577
504
|
Returns:
|
|
578
505
|
A tuple ``(strategy, xml_block)`` where *strategy* is one of
|
|
579
|
-
``"enclosing_scopes"
|
|
580
|
-
|
|
581
|
-
ready for the LLM payload.
|
|
506
|
+
``"enclosing_scopes"`` or ``"adaptive_diff"``, and *xml_block*
|
|
507
|
+
is the XML-tagged string ready for the LLM payload.
|
|
582
508
|
"""
|
|
583
509
|
if not changed_lines:
|
|
584
510
|
return "adaptive_diff", ""
|
|
@@ -592,24 +518,18 @@ class ContextExtractor:
|
|
|
592
518
|
return self._adaptive(file_path, new_content, changed_lines)
|
|
593
519
|
|
|
594
520
|
# ------------------------------------------------------------------
|
|
595
|
-
#
|
|
521
|
+
# Generic AST path
|
|
596
522
|
# ------------------------------------------------------------------
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
scopes = _extract_sql_statement(new_content, changed_lines, lang)
|
|
600
|
-
if scopes:
|
|
601
|
-
return self._format_enclosing_scopes(
|
|
602
|
-
file_path, scopes, strategy="sql_statement"
|
|
603
|
-
)
|
|
604
|
-
except Exception:
|
|
605
|
-
pass
|
|
523
|
+
ast_changed_lines = _non_whitespace_changed_lines(new_content, changed_lines)
|
|
524
|
+
if not ast_changed_lines:
|
|
606
525
|
return self._adaptive(file_path, new_content, changed_lines)
|
|
607
526
|
|
|
608
|
-
# ------------------------------------------------------------------
|
|
609
|
-
# Generic AST path
|
|
610
|
-
# ------------------------------------------------------------------
|
|
611
527
|
try:
|
|
612
|
-
scopes = _extract_enclosing_scopes_ts(
|
|
528
|
+
scopes = _extract_enclosing_scopes_ts(
|
|
529
|
+
new_content,
|
|
530
|
+
ast_changed_lines,
|
|
531
|
+
lang,
|
|
532
|
+
)
|
|
613
533
|
except Exception:
|
|
614
534
|
return self._adaptive(file_path, new_content, changed_lines)
|
|
615
535
|
|
|
@@ -617,29 +537,17 @@ class ContextExtractor:
|
|
|
617
537
|
# No enclosing scope found (top-level code) → adaptive diff
|
|
618
538
|
return self._adaptive(file_path, new_content, changed_lines)
|
|
619
539
|
|
|
620
|
-
|
|
621
|
-
# Check if any single scope is too large — if so, fall back to
|
|
622
|
-
# file_skeleton to avoid sending a 900-line class body to the LLM.
|
|
623
|
-
oversized = any(
|
|
624
|
-
(s.end_line - s.start_line) > self.max_scope_lines
|
|
625
|
-
for s in scopes
|
|
626
|
-
)
|
|
627
|
-
if not oversized:
|
|
628
|
-
# Scenario A: Enclosing Scopes
|
|
629
|
-
return self._format_enclosing_scopes(file_path, scopes)
|
|
540
|
+
scopes = _remove_redundant_ancestor_scopes(scopes, ast_changed_lines)
|
|
630
541
|
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
542
|
+
too_many_scopes = len(scopes) > self.max_scope_blocks
|
|
543
|
+
oversized_scope = any(
|
|
544
|
+
(s.end_line - s.start_line + 1) > self.max_scope_lines
|
|
545
|
+
for s in scopes
|
|
546
|
+
)
|
|
547
|
+
if too_many_scopes or oversized_scope:
|
|
635
548
|
return self._adaptive(file_path, new_content, changed_lines)
|
|
636
549
|
|
|
637
|
-
|
|
638
|
-
f'<file_skeleton file="{file_path}">\n'
|
|
639
|
-
f"{skeleton}\n"
|
|
640
|
-
f"</file_skeleton>"
|
|
641
|
-
)
|
|
642
|
-
return "file_skeleton", xml
|
|
550
|
+
return self._format_enclosing_scopes(file_path, scopes)
|
|
643
551
|
|
|
644
552
|
# ------------------------------------------------------------------
|
|
645
553
|
# Internal formatters
|
|
@@ -666,7 +574,6 @@ class ContextExtractor:
|
|
|
666
574
|
self,
|
|
667
575
|
file_path: str,
|
|
668
576
|
scopes: list[ScopeInfo],
|
|
669
|
-
strategy: str = "enclosing_scopes",
|
|
670
577
|
) -> tuple[str, str]:
|
|
671
578
|
"""Render a list of :class:`ScopeInfo` into an XML block."""
|
|
672
579
|
parts: list[str] = []
|
|
@@ -679,13 +586,12 @@ class ContextExtractor:
|
|
|
679
586
|
parts.append(_format_scope_block(scope))
|
|
680
587
|
|
|
681
588
|
body = "\n".join(parts)
|
|
682
|
-
tag = "enclosing_scopes" if strategy == "enclosing_scopes" else strategy
|
|
683
589
|
xml = (
|
|
684
|
-
f'<
|
|
590
|
+
f'<enclosing_scopes file="{file_path}">\n'
|
|
685
591
|
f"{body}\n"
|
|
686
|
-
f"</
|
|
592
|
+
f"</enclosing_scopes>"
|
|
687
593
|
)
|
|
688
|
-
return
|
|
594
|
+
return "enclosing_scopes", xml
|
|
689
595
|
|
|
690
596
|
# ------------------------------------------------------------------
|
|
691
597
|
# Utility: extract changed lines from a diff hunk for one file
|
src/git_utils.py
CHANGED
|
@@ -100,7 +100,7 @@ class GitUtils:
|
|
|
100
100
|
- ``@@ ... @@``
|
|
101
101
|
- ``+ <content>`` (added lines)
|
|
102
102
|
- `` <content>`` (context lines — unchanged surrounding code)
|
|
103
|
-
- Lines inside XML context blocks (``<enclosing_scopes
|
|
103
|
+
- Lines inside XML context blocks (``<enclosing_scopes>`` or ``<adaptive_diff>``)
|
|
104
104
|
|
|
105
105
|
Lines removed:
|
|
106
106
|
- ``- <content>`` (deleted lines)
|
|
@@ -114,8 +114,8 @@ class GitUtils:
|
|
|
114
114
|
"""
|
|
115
115
|
result = []
|
|
116
116
|
in_xml_block = False
|
|
117
|
-
_xml_start_tags = ("<enclosing_scopes", "<
|
|
118
|
-
_xml_end_tags = ("</enclosing_scopes>", "</
|
|
117
|
+
_xml_start_tags = ("<enclosing_scopes", "<adaptive_diff")
|
|
118
|
+
_xml_end_tags = ("</enclosing_scopes>", "</adaptive_diff>")
|
|
119
119
|
|
|
120
120
|
for line in diff.split("\n"):
|
|
121
121
|
if any(line.startswith(tag) for tag in _xml_start_tags):
|
|
@@ -204,7 +204,7 @@ class GitUtils:
|
|
|
204
204
|
|
|
205
205
|
def truncate_diff_per_file(self, diff: str, max_lines: int = 2000) -> tuple[str, bool]:
|
|
206
206
|
"""
|
|
207
|
-
Truncates
|
|
207
|
+
Truncates diff lines per file while preserving complete XML context blocks.
|
|
208
208
|
Falls back to global truncation if no file sections are present.
|
|
209
209
|
|
|
210
210
|
Returns:
|
|
@@ -227,18 +227,30 @@ class GitUtils:
|
|
|
227
227
|
truncated_any = False
|
|
228
228
|
output_sections: list[str] = []
|
|
229
229
|
for section in sections:
|
|
230
|
-
|
|
230
|
+
xml_start = next(
|
|
231
|
+
(
|
|
232
|
+
index
|
|
233
|
+
for index, line in enumerate(section)
|
|
234
|
+
if line.startswith(("<enclosing_scopes", "<adaptive_diff"))
|
|
235
|
+
),
|
|
236
|
+
len(section),
|
|
237
|
+
)
|
|
238
|
+
diff_lines = section[:xml_start]
|
|
239
|
+
context_lines = section[xml_start:]
|
|
240
|
+
|
|
241
|
+
if len(diff_lines) <= max_lines:
|
|
231
242
|
output_sections.append("\n".join(section))
|
|
232
243
|
continue
|
|
233
244
|
|
|
234
245
|
truncated_any = True
|
|
235
|
-
omitted = len(
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
f"
|
|
239
|
-
f"Original section: {len(
|
|
246
|
+
omitted = len(diff_lines) - max_lines
|
|
247
|
+
truncated_section = diff_lines[:max_lines]
|
|
248
|
+
truncated_section.append(
|
|
249
|
+
f"... [TRUNCATED IN THIS FILE: {omitted} diff lines omitted. "
|
|
250
|
+
f"Original diff section: {len(diff_lines)} lines] ..."
|
|
240
251
|
)
|
|
241
|
-
|
|
252
|
+
truncated_section.extend(context_lines)
|
|
253
|
+
output_sections.append("\n".join(truncated_section))
|
|
242
254
|
|
|
243
255
|
return "\n".join(output_sections), truncated_any
|
|
244
256
|
|
src/llm_client.py
CHANGED
|
@@ -28,6 +28,26 @@ class LLMError(Exception):
|
|
|
28
28
|
"""Exception for LLM communication errors."""
|
|
29
29
|
pass
|
|
30
30
|
|
|
31
|
+
|
|
32
|
+
def _normalize_prompt_spacing(text: str) -> str:
|
|
33
|
+
"""Collapse repeated blank lines while preserving one between sections."""
|
|
34
|
+
normalized_lines: list[str] = []
|
|
35
|
+
blank_pending = False
|
|
36
|
+
|
|
37
|
+
for line in text.splitlines():
|
|
38
|
+
if not line.strip():
|
|
39
|
+
if normalized_lines:
|
|
40
|
+
blank_pending = True
|
|
41
|
+
continue
|
|
42
|
+
|
|
43
|
+
if blank_pending:
|
|
44
|
+
normalized_lines.append("")
|
|
45
|
+
blank_pending = False
|
|
46
|
+
normalized_lines.append(line)
|
|
47
|
+
|
|
48
|
+
return "\n".join(normalized_lines)
|
|
49
|
+
|
|
50
|
+
|
|
31
51
|
# ---------------------------------------------------------------------------
|
|
32
52
|
# System Prompts
|
|
33
53
|
# ---------------------------------------------------------------------------
|
|
@@ -43,32 +63,17 @@ class LLMError(Exception):
|
|
|
43
63
|
# fallback to "detailed" when the key is not recognised.
|
|
44
64
|
SYSTEM_PROMPTS = {
|
|
45
65
|
"detailed": (
|
|
46
|
-
"You are an
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"Rules:\n"
|
|
50
|
-
"- Report ONLY specific issues tied to a concrete line or block of changed code.\n"
|
|
51
|
-
"- Do NOT produce summary sections (e.g. 'Potential Bugs', 'Security Overview').\n"
|
|
52
|
-
"- Do NOT produce introductory or closing paragraphs.\n"
|
|
53
|
-
"- If no issues are found, output only: No issues found.\n"
|
|
54
|
-
"- Be specific, actionable, and concise."
|
|
66
|
+
"You are an AI Agente code reviewer. Identify concrete bugs, security risks, "
|
|
67
|
+
"performance problems, and maintainability issues introduced by the change. "
|
|
68
|
+
"Report only actionable findings tied to changed code; be specific and concise."
|
|
55
69
|
),
|
|
56
70
|
"security": (
|
|
57
|
-
"You are an application security.
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"-
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"- Hardcoded credentials or exposed secrets\n"
|
|
64
|
-
"- Authentication/authorization vulnerabilities\n"
|
|
65
|
-
"- Insecure deserialization\n"
|
|
66
|
-
"- Path traversal\n"
|
|
67
|
-
"- Command injection\n"
|
|
68
|
-
"- Dependencies with known vulnerabilities\n"
|
|
69
|
-
"- Logging of sensitive information\n"
|
|
70
|
-
"- Insecure configurations\n\n"
|
|
71
|
-
"Provide fix recommendations for each issue."
|
|
71
|
+
"You are an AI Agente application security reviewer. Identify actionable security vulnerabilities "
|
|
72
|
+
"introduced by the change, including injection, XSS, CSRF, exposed secrets, broken "
|
|
73
|
+
"authentication or authorization, insecure deserialization, path traversal, vulnerable "
|
|
74
|
+
"dependencies, sensitive-data logging, and insecure configuration. Consider other "
|
|
75
|
+
"security risks when relevant. Report only findings tied to changed code and include "
|
|
76
|
+
"a concise fix recommendation for each."
|
|
72
77
|
),
|
|
73
78
|
}
|
|
74
79
|
|
|
@@ -91,15 +96,20 @@ PR_COMMENT_PROMPT = (
|
|
|
91
96
|
'something like "No issues found."\n'
|
|
92
97
|
'- "comments": an array of objects, one per issue found, with:\n'
|
|
93
98
|
' - "file": file path (e.g., "src/auth.py")\n'
|
|
94
|
-
' - "line": line number in
|
|
99
|
+
' - "line": positive absolute line number in the new file (integer)\n'
|
|
95
100
|
' - "type": issue type ("bug", "security", "performance", "style", "suggestion", "praise")\n'
|
|
96
101
|
' - "comment": direct description of the issue, with no greetings and no emojis\n'
|
|
97
|
-
' - "suggestion": fix
|
|
98
|
-
' - "reference":
|
|
99
|
-
'
|
|
100
|
-
"
|
|
101
|
-
"
|
|
102
|
-
"
|
|
102
|
+
' - "suggestion": concise, concrete fix recommendation (string)\n'
|
|
103
|
+
' - "reference": relevant trusted source, standard, or documentation URL; use an empty string '
|
|
104
|
+
'when no reliable reference applies\n\n'
|
|
105
|
+
"The summary is a brief narrative of the overall review, not a heading or a duplicate list "
|
|
106
|
+
"of comments. Use an empty comments array when no issues are found. Each comment must refer "
|
|
107
|
+
"to a specific actionable issue in changed code, and its line must be a positive absolute "
|
|
108
|
+
"line number from the new file. Every comment must include a concise, concrete fix "
|
|
109
|
+
"recommendation in 'suggestion'; do not leave it empty for an actionable issue. Use a short, "
|
|
110
|
+
"objective tone with no greetings or emojis. "
|
|
111
|
+
"Include a relevant trusted source in 'reference' when one applies; otherwise use an empty "
|
|
112
|
+
"string. Never invent a citation.\n\n"
|
|
103
113
|
"Respond ONLY with a valid JSON object. Example:\n"
|
|
104
114
|
'{\n'
|
|
105
115
|
' "summary": "Short narrative summary of the review...",\n'
|
|
@@ -120,14 +130,12 @@ PR_COMMENT_PROMPT = (
|
|
|
120
130
|
def get_system_prompt(verbosity: str) -> str:
|
|
121
131
|
"""Returns the system prompt for the given verbosity level.
|
|
122
132
|
|
|
123
|
-
Selects
|
|
133
|
+
Selects review-focus guidance from ``SYSTEM_PROMPTS`` keyed by *verbosity*. If the
|
|
124
134
|
requested verbosity is not found, the function falls back to
|
|
125
135
|
``"detailed"``.
|
|
126
136
|
|
|
127
|
-
The
|
|
128
|
-
|
|
129
|
-
prohibiting summary sections and introductory/closing paragraphs.
|
|
130
|
-
When no issues are found the expected output is ``No issues found.``
|
|
137
|
+
The selected prompt controls review focus only. The PR JSON output contract
|
|
138
|
+
is defined separately by ``PR_COMMENT_PROMPT``.
|
|
131
139
|
|
|
132
140
|
Args:
|
|
133
141
|
verbosity: Review depth key — one of ``"detailed"``
|
|
@@ -145,21 +153,17 @@ def get_scope_guidance(review_scope: str, structured: bool = False) -> str:
|
|
|
145
153
|
|
|
146
154
|
For ``diff_only`` scope, the instructions inform the LLM that:
|
|
147
155
|
|
|
148
|
-
* Added lines are prefixed with
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
156
|
+
* Added lines are prefixed with ``+``. Each hunk includes up to five
|
|
157
|
+
unchanged context lines before and after the changes, prefixed with a
|
|
158
|
+
single space. Deleted lines are not included.
|
|
159
|
+
* When source content is available, a changed file may include an XML
|
|
160
|
+
context block containing either:
|
|
152
161
|
|
|
153
162
|
- ``<enclosing_scopes file="...">`` — the complete function(s) or
|
|
154
|
-
|
|
155
|
-
- ``<file_skeleton file="...">`` — a structural skeleton of the file
|
|
156
|
-
with unchanged function bodies replaced by ``...`` (Scenario B,
|
|
157
|
-
>3 blocks).
|
|
158
|
-
- ``<sql_statement file="...">`` — the full SQL statement containing
|
|
159
|
-
the changed lines.
|
|
163
|
+
methods/statements that enclose the changed lines.
|
|
160
164
|
- ``<adaptive_diff file="...">`` — N lines of context around the
|
|
161
|
-
changed lines when
|
|
162
|
-
|
|
165
|
+
changed lines when AST extraction is unavailable or exceeds its
|
|
166
|
+
configured limits.
|
|
163
167
|
|
|
164
168
|
* Line numbers inside the XML blocks are **1-based absolute** line
|
|
165
169
|
numbers from the new version of the file and should be used as the
|
|
@@ -193,13 +197,13 @@ def get_scope_guidance(review_scope: str, structured: bool = False) -> str:
|
|
|
193
197
|
if structured:
|
|
194
198
|
return (
|
|
195
199
|
"Review scope: diff_only. "
|
|
196
|
-
"The <diff> block contains added lines (
|
|
197
|
-
"
|
|
198
|
-
"
|
|
199
|
-
"
|
|
200
|
-
"<
|
|
201
|
-
"<
|
|
202
|
-
"
|
|
200
|
+
"The <diff> block contains added lines (prefixed with +) and up to five unchanged "
|
|
201
|
+
"context lines before and after each hunk (prefixed with a single space). "
|
|
202
|
+
"Deleted lines were removed. When source content is available, a changed file may "
|
|
203
|
+
"include an XML context block: "
|
|
204
|
+
"<enclosing_scopes> contains the complete AST scope(s) enclosing the changes; "
|
|
205
|
+
"<adaptive_diff> contains surrounding context lines when AST extraction is unavailable "
|
|
206
|
+
"or exceeds its configured limits. "
|
|
203
207
|
"Line numbers inside XML context blocks are 1-based absolute line numbers from the new file. "
|
|
204
208
|
"Use those line numbers as the authoritative reference when reporting issue positions. "
|
|
205
209
|
"Focus your review EXCLUSIVELY on the changed lines (marked + in <diff>). "
|
|
@@ -210,10 +214,11 @@ def get_scope_guidance(review_scope: str, structured: bool = False) -> str:
|
|
|
210
214
|
|
|
211
215
|
return (
|
|
212
216
|
"Review scope: diff_only. "
|
|
213
|
-
"The <diff> block contains added lines (
|
|
214
|
-
"
|
|
215
|
-
"
|
|
216
|
-
"
|
|
217
|
+
"The <diff> block contains added lines (prefixed with +) and up to five unchanged "
|
|
218
|
+
"context lines before and after each hunk (prefixed with a single space). "
|
|
219
|
+
"Deleted lines were removed. When source content is available, a changed file may "
|
|
220
|
+
"include an XML context block "
|
|
221
|
+
"(<enclosing_scopes> or <adaptive_diff>) "
|
|
217
222
|
"as read-only structural background. "
|
|
218
223
|
"Line numbers inside XML context blocks are 1-based absolute line numbers from the new file. "
|
|
219
224
|
"Use those line numbers as the authoritative reference when reporting issue positions. "
|
|
@@ -263,7 +268,7 @@ def build_user_message(diff: str, files_summary: list[dict], context: str = "")
|
|
|
263
268
|
parts.append("<context>\n" + "\n".join(context_inner) + "\n</context>")
|
|
264
269
|
|
|
265
270
|
# --- Split diff lines from XML context blocks ---
|
|
266
|
-
# XML context blocks (enclosing_scopes
|
|
271
|
+
# XML context blocks (enclosing_scopes or adaptive_diff) are already
|
|
267
272
|
# embedded inside the diff string by TFSClient._build_unified_diff_part.
|
|
268
273
|
# We extract them here and render them separately so the LLM sees:
|
|
269
274
|
# <diff> ... pure unified diff ...</diff>
|
|
@@ -275,14 +280,10 @@ def build_user_message(diff: str, files_summary: list[dict], context: str = "")
|
|
|
275
280
|
_in_xml = False
|
|
276
281
|
_xml_tags = (
|
|
277
282
|
"<enclosing_scopes ",
|
|
278
|
-
"<file_skeleton ",
|
|
279
|
-
"<sql_statement ",
|
|
280
283
|
"<adaptive_diff ",
|
|
281
284
|
)
|
|
282
285
|
_xml_close = (
|
|
283
286
|
"</enclosing_scopes>",
|
|
284
|
-
"</file_skeleton>",
|
|
285
|
-
"</sql_statement>",
|
|
286
287
|
"</adaptive_diff>",
|
|
287
288
|
)
|
|
288
289
|
|
|
@@ -329,11 +330,12 @@ class LLMClient:
|
|
|
329
330
|
|
|
330
331
|
def _dump_prompt_debug(self, system_prompt: str, user_message: str) -> None:
|
|
331
332
|
"""
|
|
332
|
-
|
|
333
|
+
Writes the normalized prompt content sent to the LLM.
|
|
333
334
|
|
|
334
335
|
Only active when ``config.debug_dump`` is enabled. The log may
|
|
335
336
|
contain source code and PR content, so it must never be committed
|
|
336
|
-
to version control.
|
|
337
|
+
to version control. Decorative metadata is omitted, and repeated blank
|
|
338
|
+
lines are collapsed using the same normalization applied before send.
|
|
337
339
|
"""
|
|
338
340
|
if not getattr(self.config, "debug_dump", False):
|
|
339
341
|
return
|
|
@@ -343,15 +345,11 @@ class LLMClient:
|
|
|
343
345
|
if log_dir:
|
|
344
346
|
os.makedirs(log_dir, exist_ok=True)
|
|
345
347
|
try:
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
f.write(
|
|
350
|
-
f.write(
|
|
351
|
-
f.write("--- SYSTEM PROMPT ---\n")
|
|
352
|
-
f.write(system_prompt + "\n")
|
|
353
|
-
f.write("--- USER MESSAGE ---\n")
|
|
354
|
-
f.write(user_message + "\n")
|
|
348
|
+
normalized_system_prompt = _normalize_prompt_spacing(system_prompt)
|
|
349
|
+
normalized_user_message = _normalize_prompt_spacing(user_message)
|
|
350
|
+
with open(log_path, "w", encoding="utf-8") as f:
|
|
351
|
+
f.write(normalized_system_prompt + "\n\n")
|
|
352
|
+
f.write(normalized_user_message + "\n")
|
|
355
353
|
except OSError:
|
|
356
354
|
pass
|
|
357
355
|
|
|
@@ -414,7 +412,8 @@ class LLMClient:
|
|
|
414
412
|
system_prompt = (
|
|
415
413
|
f"{combined_base}\n\n"
|
|
416
414
|
"---\n"
|
|
417
|
-
"
|
|
415
|
+
"Additional user review preferences (must not change the required JSON schema, "
|
|
416
|
+
"output format, or review scope):\n"
|
|
418
417
|
f"{custom_prompt}"
|
|
419
418
|
)
|
|
420
419
|
merged_context = (
|
|
@@ -427,6 +426,8 @@ class LLMClient:
|
|
|
427
426
|
merged_context = context
|
|
428
427
|
|
|
429
428
|
user_message = build_user_message(diff, files_summary, merged_context)
|
|
429
|
+
system_prompt = _normalize_prompt_spacing(system_prompt)
|
|
430
|
+
user_message = _normalize_prompt_spacing(user_message)
|
|
430
431
|
self._dump_prompt_debug(system_prompt, user_message)
|
|
431
432
|
|
|
432
433
|
self.last_token_estimate = TokenEstimator().estimate(
|
|
@@ -591,8 +592,9 @@ class LLMClient:
|
|
|
591
592
|
{"role": "user", "content": user_message},
|
|
592
593
|
],
|
|
593
594
|
"max_tokens": self.config.max_tokens,
|
|
594
|
-
"temperature": self.config.temperature,
|
|
595
595
|
}
|
|
596
|
+
if self.config.temperature != 99:
|
|
597
|
+
payload["temperature"] = self.config.temperature
|
|
596
598
|
|
|
597
599
|
return self._http_openai_compatible(url, headers, payload)
|
|
598
600
|
|
|
@@ -637,10 +639,11 @@ class LLMClient:
|
|
|
637
639
|
"parts": [{"text": system_prompt}]
|
|
638
640
|
},
|
|
639
641
|
"generationConfig": {
|
|
640
|
-
"temperature": self.config.temperature,
|
|
641
642
|
"maxOutputTokens": self.config.max_tokens,
|
|
642
643
|
},
|
|
643
644
|
}
|
|
645
|
+
if self.config.temperature != 99:
|
|
646
|
+
payload["generationConfig"]["temperature"] = self.config.temperature
|
|
644
647
|
|
|
645
648
|
try:
|
|
646
649
|
resp = requests.post(url, headers=headers, json=payload, timeout=180)
|
|
@@ -715,12 +718,13 @@ class LLMClient:
|
|
|
715
718
|
payload = {
|
|
716
719
|
"model": model,
|
|
717
720
|
"max_tokens": self.config.max_tokens,
|
|
718
|
-
"temperature": self.config.temperature,
|
|
719
721
|
"system": system_prompt,
|
|
720
722
|
"messages": [
|
|
721
723
|
{"role": "user", "content": user_message},
|
|
722
724
|
],
|
|
723
725
|
}
|
|
726
|
+
if self.config.temperature != 99:
|
|
727
|
+
payload["temperature"] = self.config.temperature
|
|
724
728
|
|
|
725
729
|
try:
|
|
726
730
|
resp = requests.post(url, headers=headers, json=payload, timeout=180)
|
|
@@ -793,9 +797,10 @@ class LLMClient:
|
|
|
793
797
|
{"role": "system", "content": system_prompt},
|
|
794
798
|
{"role": "user", "content": user_message},
|
|
795
799
|
],
|
|
796
|
-
"temperature": self.config.temperature,
|
|
797
800
|
"stream": False,
|
|
798
801
|
}
|
|
802
|
+
if self.config.temperature != 99:
|
|
803
|
+
payload["temperature"] = self.config.temperature
|
|
799
804
|
|
|
800
805
|
# Ollama does not require an API key, but we add max_tokens if configured
|
|
801
806
|
if self.config.max_tokens:
|
|
@@ -912,8 +917,9 @@ class LLMClient:
|
|
|
912
917
|
{"role": "system", "content": system_prompt},
|
|
913
918
|
{"role": "user", "content": user_message},
|
|
914
919
|
],
|
|
915
|
-
"temperature": self.config.temperature,
|
|
916
920
|
}
|
|
921
|
+
if self.config.temperature != 99:
|
|
922
|
+
payload["temperature"] = self.config.temperature
|
|
917
923
|
|
|
918
924
|
# Add max_tokens if configured (some Copilot models
|
|
919
925
|
# may not support this parameter)
|
|
@@ -1028,7 +1034,7 @@ class LLMClient:
|
|
|
1028
1034
|
system_prompt: str,
|
|
1029
1035
|
user_message: str,
|
|
1030
1036
|
) -> str:
|
|
1031
|
-
"""Calls Bedrock
|
|
1037
|
+
"""Calls Bedrock Converse using an HTTP Bearer token (long-term API key).
|
|
1032
1038
|
|
|
1033
1039
|
Args:
|
|
1034
1040
|
region: AWS region, e.g. ``us-east-1``.
|
|
@@ -1051,14 +1057,19 @@ class LLMClient:
|
|
|
1051
1057
|
|
|
1052
1058
|
# The model ARN contains ':' and '/' that must be URL-encoded in the path
|
|
1053
1059
|
model_encoded = urllib.parse.quote(self.config.model, safe="")
|
|
1054
|
-
url = f"https://bedrock-runtime.{region}.amazonaws.com/model/{model_encoded}/
|
|
1060
|
+
url = f"https://bedrock-runtime.{region}.amazonaws.com/model/{model_encoded}/converse"
|
|
1061
|
+
|
|
1062
|
+
inference_config = {"maxTokens": self.config.max_tokens}
|
|
1063
|
+
if self.config.temperature != 99:
|
|
1064
|
+
inference_config["temperature"] = self.config.temperature
|
|
1055
1065
|
|
|
1056
1066
|
payload = json.dumps({
|
|
1057
|
-
"
|
|
1058
|
-
"
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1067
|
+
"system": [{"text": system_prompt}],
|
|
1068
|
+
"messages": [{
|
|
1069
|
+
"role": "user",
|
|
1070
|
+
"content": [{"text": user_message}],
|
|
1071
|
+
}],
|
|
1072
|
+
"inferenceConfig": inference_config,
|
|
1062
1073
|
}, separators=(",", ":"))
|
|
1063
1074
|
|
|
1064
1075
|
headers = {
|
|
@@ -1084,10 +1095,9 @@ class LLMClient:
|
|
|
1084
1095
|
except ValueError as exc:
|
|
1085
1096
|
raise LLMError(f"Invalid JSON from Bedrock: {resp.text[:500]}") from exc
|
|
1086
1097
|
|
|
1087
|
-
content = data.get("content", [])
|
|
1098
|
+
content = data.get("output", {}).get("message", {}).get("content", [])
|
|
1088
1099
|
text_parts = [
|
|
1089
|
-
item["text"] for item in content
|
|
1090
|
-
if item.get("type") == "text" and "text" in item
|
|
1100
|
+
item["text"] for item in content if "text" in item
|
|
1091
1101
|
]
|
|
1092
1102
|
text = "\n".join(part for part in text_parts if part).strip()
|
|
1093
1103
|
if not text:
|
|
@@ -1103,7 +1113,7 @@ class LLMClient:
|
|
|
1103
1113
|
system_prompt: str,
|
|
1104
1114
|
user_message: str,
|
|
1105
1115
|
) -> str:
|
|
1106
|
-
"""Calls Bedrock
|
|
1116
|
+
"""Calls Bedrock Converse with manual AWS SigV4 HMAC-SHA256 signing.
|
|
1107
1117
|
|
|
1108
1118
|
Equivalent to the C# BedrockLlmClient implementation.
|
|
1109
1119
|
|
|
@@ -1135,15 +1145,20 @@ class LLMClient:
|
|
|
1135
1145
|
|
|
1136
1146
|
host = f"bedrock-runtime.{region}.amazonaws.com"
|
|
1137
1147
|
model_encoded = urllib.parse.quote(self.config.model, safe="")
|
|
1138
|
-
endpoint = f"https://{host}/model/{model_encoded}/
|
|
1148
|
+
endpoint = f"https://{host}/model/{model_encoded}/converse"
|
|
1139
1149
|
service = "bedrock"
|
|
1140
1150
|
|
|
1151
|
+
inference_config = {"maxTokens": self.config.max_tokens}
|
|
1152
|
+
if self.config.temperature != 99:
|
|
1153
|
+
inference_config["temperature"] = self.config.temperature
|
|
1154
|
+
|
|
1141
1155
|
payload = json.dumps({
|
|
1142
|
-
"
|
|
1143
|
-
"
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1156
|
+
"system": [{"text": system_prompt}],
|
|
1157
|
+
"messages": [{
|
|
1158
|
+
"role": "user",
|
|
1159
|
+
"content": [{"text": user_message}],
|
|
1160
|
+
}],
|
|
1161
|
+
"inferenceConfig": inference_config,
|
|
1147
1162
|
}, separators=(",", ":"))
|
|
1148
1163
|
|
|
1149
1164
|
now = datetime.datetime.now(datetime.timezone.utc)
|
|
@@ -1157,7 +1172,7 @@ class LLMClient:
|
|
|
1157
1172
|
# Splitting by '/' and encoding each segment mirrors the C# SigV4 implementation.
|
|
1158
1173
|
canonical_uri = "/".join(
|
|
1159
1174
|
urllib.parse.quote(seg, safe="")
|
|
1160
|
-
for seg in f"/model/{self.config.model}/
|
|
1175
|
+
for seg in f"/model/{self.config.model}/converse".split("/")
|
|
1161
1176
|
)
|
|
1162
1177
|
|
|
1163
1178
|
headers_to_sign = {
|
|
@@ -1227,10 +1242,9 @@ class LLMClient:
|
|
|
1227
1242
|
except ValueError as exc:
|
|
1228
1243
|
raise LLMError(f"Invalid JSON from Bedrock: {resp.text[:500]}") from exc
|
|
1229
1244
|
|
|
1230
|
-
content = data.get("content", [])
|
|
1245
|
+
content = data.get("output", {}).get("message", {}).get("content", [])
|
|
1231
1246
|
text_parts = [
|
|
1232
|
-
item["text"] for item in content
|
|
1233
|
-
if item.get("type") == "text" and "text" in item
|
|
1247
|
+
item["text"] for item in content if "text" in item
|
|
1234
1248
|
]
|
|
1235
1249
|
text = "\n".join(part for part in text_parts if part).strip()
|
|
1236
1250
|
if not text:
|
|
@@ -1277,6 +1291,10 @@ class LLMClient:
|
|
|
1277
1291
|
session = boto3.Session(**session_kwargs)
|
|
1278
1292
|
client = session.client("bedrock-runtime", region_name=region)
|
|
1279
1293
|
|
|
1294
|
+
inference_config = {"maxTokens": self.config.max_tokens}
|
|
1295
|
+
if self.config.temperature != 99:
|
|
1296
|
+
inference_config["temperature"] = self.config.temperature
|
|
1297
|
+
|
|
1280
1298
|
response = client.converse(
|
|
1281
1299
|
modelId=self.config.model,
|
|
1282
1300
|
system=[{"text": system_prompt}],
|
|
@@ -1286,10 +1304,7 @@ class LLMClient:
|
|
|
1286
1304
|
"content": [{"text": user_message}],
|
|
1287
1305
|
}
|
|
1288
1306
|
],
|
|
1289
|
-
inferenceConfig=
|
|
1290
|
-
"temperature": self.config.temperature,
|
|
1291
|
-
"maxTokens": self.config.max_tokens,
|
|
1292
|
-
},
|
|
1307
|
+
inferenceConfig=inference_config,
|
|
1293
1308
|
)
|
|
1294
1309
|
|
|
1295
1310
|
content = (
|
src/prompts/config.yaml.template
CHANGED
|
@@ -26,7 +26,7 @@ llm:
|
|
|
26
26
|
# Maximum tokens in response
|
|
27
27
|
max_tokens: 4096
|
|
28
28
|
|
|
29
|
-
# Temperature (0.0 = deterministic, 1.0 = creative)
|
|
29
|
+
# Temperature (0.0 = deterministic, 1.0 = creative, 99 omits it for all models that dont support this)
|
|
30
30
|
temperature: 0.0
|
|
31
31
|
|
|
32
32
|
# ---------------------------------------------------------------------------
|
|
@@ -91,9 +91,9 @@ review:
|
|
|
91
91
|
max_diff_lines: 4500
|
|
92
92
|
|
|
93
93
|
# Max changed logical blocks for Enclosing Scopes strategy (diff_only mode)
|
|
94
|
-
max_scope_blocks:
|
|
94
|
+
max_scope_blocks: 8
|
|
95
95
|
|
|
96
|
-
# Max lines in a single enclosing scope before falling back to
|
|
96
|
+
# Max lines in a single enclosing scope before falling back to Adaptive Diff
|
|
97
97
|
# (prevents sending entire large classes to the LLM)
|
|
98
98
|
max_scope_lines: 250
|
|
99
99
|
|
|
@@ -138,4 +138,4 @@ output:
|
|
|
138
138
|
|
|
139
139
|
debug:
|
|
140
140
|
dump: false
|
|
141
|
-
dump_file: "logs/debug.log"
|
|
141
|
+
dump_file: "logs/debug.log"
|
|
@@ -4,82 +4,18 @@
|
|
|
4
4
|
- Error handling: swallowed exceptions (empty catch), missing logging at critical points, generic error messages to end users.
|
|
5
5
|
- Readability: methods too long or with too many responsibilities (SRP), poor naming, dead/commented-out code.
|
|
6
6
|
- Duplication: repeated logic that should be extracted/reused.
|
|
7
|
-
- Testability: hard-to-mock dependencies, business logic mixed with I/O.
|
|
8
7
|
- Unused code: unused local variables, method parameters, private fields/methods, unreachable code, and unused imports/usings.
|
|
9
|
-
-
|
|
10
|
-
|
|
11
|
-
<!-- lang: cs,ts -->
|
|
12
|
-
## Performance
|
|
13
|
-
- Use of `StringBuilder` vs string concatenation in loops.
|
|
14
|
-
- Blocking synchronous operations where async should be used (I/O, network calls).
|
|
15
|
-
|
|
16
|
-
<!-- lang: cs,ts,sql -->
|
|
17
|
-
## Concurrency / Thread-safety
|
|
18
|
-
- Race conditions in shared code (static fields, singletons).
|
|
19
|
-
- Correct use of locks/semaphores, avoid deadlocks.
|
|
20
|
-
- Thread-safe collections where needed (`ConcurrentDictionary`, etc).
|
|
21
|
-
|
|
22
|
-
<!-- lang: all -->
|
|
23
|
-
## Configuration & Secrets
|
|
24
|
-
- Hardcoded values that should be in configuration (connection strings, URLs, timeouts).
|
|
25
|
-
- Secrets/API keys accidentally committed.
|
|
26
|
-
|
|
27
|
-
<!-- lang: cs,ts -->
|
|
28
|
-
## Dependencies & Compatibility
|
|
29
|
-
- New dependencies added without clear justification - unnecessary or outdated packages.
|
|
30
|
-
- Breaking changes when updating package versions.
|
|
8
|
+
- Mandatory language consistency: Newly added or modified identifiers, developer-facing comments, and documentation must use the same natural language already established by the surrounding code and project. Report mixed-language naming or documentation as a `style` finding on the identifier declaration or comment line. For example, a Portuguese method name in a codebase whose existing identifiers are English is a violation. Do not flag names imposed by external APIs, standard technical terms, or localized user-facing text.
|
|
31
9
|
|
|
32
10
|
<!-- lang: cs -->
|
|
33
11
|
## C#
|
|
34
|
-
-
|
|
35
|
-
- XML
|
|
12
|
+
- Mandatory naming conventions: Every newly added or modified C# identifier must follow Microsoft's official naming guidance. Report each violation as a `style` finding on the identifier's declaration line. Use PascalCase for types, namespaces, all methods and local functions, public or protected members, and constants; prefix interfaces with `I`; use camelCase for parameters and local variables; use `_camelCase` for private or internal instance fields and `s_camelCase` for private or internal static fields. Names must be meaningful and descriptive, and nonstandard abbreviations must be avoided. Reference: https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/identifier-names
|
|
13
|
+
- Mandatory XML documentation: Every newly added or modified public or protected C# type and member must have valid XML documentation comments.
|
|
14
|
+
Report missing documentation as a `style` finding on the declaration line. Require `<summary>` and, when applicable, `<param>`, `<typeparam>` and
|
|
15
|
+
`<returns>` elements. Follow Microsoft's XML documentation guidance: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/xmldoc/
|
|
16
|
+
- Mandatory null safety: Report a `bug` whenever newly added or modified
|
|
17
|
+
code dereferences an externally supplied reference-type parameter without first validating it for null, unless non-nullability is explicitly
|
|
18
|
+
established by preceding code. Recommend `ArgumentNullException.ThrowIfNull(parameter)` and report the finding on the first unsafe dereference.
|
|
36
19
|
- Async: avoid `async void` (except handlers), use `ConfigureAwait(false)` in libraries, don't block with `.Result`/`.Wait()`.
|
|
37
20
|
- Nullable refs: correct `?` usage, avoid unjustified `!`.
|
|
38
|
-
- LINQ: avoid redundant iterations, N+1 queries with EF Core.
|
|
39
|
-
- Disposal: proper use of `IDisposable`/`using` for resources (streams, connections, etc).
|
|
40
21
|
- Immutability: prefer `readonly`, records, immutable types.
|
|
41
|
-
|
|
42
|
-
<!-- lang: cs,sql -->
|
|
43
|
-
## Logging & Observability
|
|
44
|
-
- Appropriate log levels (Debug vs Info vs Warning vs Error).
|
|
45
|
-
- Don't log sensitive data (PII, passwords, tokens).
|
|
46
|
-
|
|
47
|
-
<!-- lang: sql -->
|
|
48
|
-
## Migrations / Database (given your SQL/EF stack)
|
|
49
|
-
- Destructive changes to columns/tables with existing data (data loss risk).
|
|
50
|
-
- Migrations that lock large tables in production.
|
|
51
|
-
|
|
52
|
-
<!-- lang: sql -->
|
|
53
|
-
## SQL
|
|
54
|
-
- Avoid `SELECT *`; specify columns explicitly.
|
|
55
|
-
- Check for parameterized queries (avoid string concatenation → SQL injection).
|
|
56
|
-
- Indexes: flag queries likely to cause table scans on large tables.
|
|
57
|
-
- Transactions: ensure multi-step critical operations are wrapped in a transaction.
|
|
58
|
-
- Naming conventions for tables/columns consistent with the rest of the schema.
|
|
59
|
-
|
|
60
|
-
<!-- lang: html -->
|
|
61
|
-
## HTML
|
|
62
|
-
- A11y: `alt`, `aria-*`, labels on inputs, semantic tags (`<button>` vs `<div onclick>`).
|
|
63
|
-
- Semantic tags where they apply (`<button>`, `<label>`, `<ul>`/`<li>`) over generic `<div>`/`<span>`. Landmarks (`<header>`/`<nav>`/`<main>`) apply to shell/layout, not feature components.
|
|
64
|
-
- No inline `style=""` for static styling - use the component's `.less` file. `[style.x]`/`[ngStyle]` ok for dynamic values.
|
|
65
|
-
|
|
66
|
-
<!-- lang: ts,html -->
|
|
67
|
-
## TypeScript / Angular
|
|
68
|
-
- Follow the Angular style guide (https://angular.dev/style-guide).
|
|
69
|
-
- Avoid `any`, prefer explicit types.
|
|
70
|
-
- RxJS: unsubscribe via async pipe, `takeUntil`, or `takeUntilDestroyed`.
|
|
71
|
-
- Use `OnPush` where it makes sense.
|
|
72
|
-
- Separate presentation (component) from business logic (services); avoid heavy template logic.
|
|
73
|
-
- Files: kebab-case + type suffix (`select-maintenance-plan-step.component.ts/.html/.less`).
|
|
74
|
-
- Classes: PascalCase matching file name, no type suffix (`SelectMaintenancePlanStep`, not `...Component`).
|
|
75
|
-
- Selectors: kebab-case with project prefix (`app-select-maintenance-plan-step`).
|
|
76
|
-
- `.less` files named like their component; use `:host`, avoid global style leaks.
|
|
77
|
-
|
|
78
|
-
<!-- lang: all -->
|
|
79
|
-
## Review Process Notes (meta)
|
|
80
|
-
- Distinguish blocking comments (must-fix) from suggestions (nice-to-have) - this matters a lot for an automated reviewer, so it doesn't hold up merges over trivial issues.
|
|
81
|
-
- If the change is purely formatting/whitespace, do not comment on it.
|
|
82
|
-
- Ignore auto-generated files (generated migrations, `*.designer.cs`, `node_modules`, etc.) unless they contain manually-added code.
|
|
83
|
-
- Flag only issues relevant to the diff/change being reviewed, not the entire file, unless the change requires it.
|
|
84
|
-
- Before flagging something as "missing", verify it isn't present in the surrounding unchanged context (lines without `+`/`-`). Only flag genuinely absent elements, not elements outside the shown diff hunk.
|
|
85
|
-
- Priority order: Security > Functional bugs > Maintainability > Style.
|
src/tfs_client.py
CHANGED
|
@@ -324,8 +324,7 @@ class TFSClient:
|
|
|
324
324
|
For ``diff_only`` scope (default), builds a standard unified diff for
|
|
325
325
|
each changed file and appends an XML context block produced by
|
|
326
326
|
:class:`~src.context_extractor.ContextExtractor` (one of
|
|
327
|
-
``<enclosing_scopes
|
|
328
|
-
``<adaptive_diff>``). The block is preserved verbatim by
|
|
327
|
+
``<enclosing_scopes>`` or ``<adaptive_diff>``). The block is preserved verbatim by
|
|
329
328
|
:py:meth:`GitUtils.filter_diff_additions_only` so the LLM receives
|
|
330
329
|
structured scope context without consuming the entire file.
|
|
331
330
|
|
|
@@ -500,18 +499,15 @@ class TFSClient:
|
|
|
500
499
|
|
|
501
500
|
Fetches the old version from the target branch and the new version
|
|
502
501
|
from the source branch, then generates a standard unified diff with
|
|
503
|
-
|
|
502
|
+
5 lines of context using :py:mod:`difflib`.
|
|
504
503
|
|
|
505
504
|
After the diff lines an XML context block is appended, produced by
|
|
506
505
|
:class:`~src.context_extractor.ContextExtractor`:
|
|
507
506
|
|
|
508
|
-
* ``<enclosing_scopes>`` — the
|
|
509
|
-
|
|
510
|
-
* ``<
|
|
511
|
-
|
|
512
|
-
* ``<sql_statement>`` — the full SQL statement for SQL files
|
|
513
|
-
* ``<adaptive_diff>`` — surrounding context lines when AST parsing
|
|
514
|
-
is unavailable or the language is unsupported
|
|
507
|
+
* ``<enclosing_scopes>`` — the AST scope(s) enclosing the changed
|
|
508
|
+
lines when they fit the configured block and line limits
|
|
509
|
+
* ``<adaptive_diff>`` — surrounding context lines when AST extraction
|
|
510
|
+
is unavailable, unsupported, fails, or exceeds those limits
|
|
515
511
|
|
|
516
512
|
The XML block is preserved verbatim by
|
|
517
513
|
:py:meth:`GitUtils.filter_diff_additions_only`.
|
|
@@ -565,7 +561,7 @@ class TFSClient:
|
|
|
565
561
|
fromfile=f"a{original_path}",
|
|
566
562
|
tofile=f"b{file_path}",
|
|
567
563
|
lineterm="",
|
|
568
|
-
n=
|
|
564
|
+
n=5,
|
|
569
565
|
))
|
|
570
566
|
|
|
571
567
|
if not diff:
|
|
@@ -581,8 +577,8 @@ class TFSClient:
|
|
|
581
577
|
raw_diff = list(diff)
|
|
582
578
|
result = [f"diff --git a{original_path} b{file_path}"] + raw_diff
|
|
583
579
|
|
|
584
|
-
# Build the AST-based context block (Enclosing Scopes
|
|
585
|
-
#
|
|
580
|
+
# Build the AST-based context block (Enclosing Scopes or Adaptive Diff
|
|
581
|
+
# fallback) and append it after the diff section.
|
|
586
582
|
if new_lines:
|
|
587
583
|
new_content = "\n".join(new_lines)
|
|
588
584
|
file_diff_text = "\n".join(result)
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
src/__init__.py,sha256=BNfzb1HoMlgLc9MNEzc8Ne7USol7NHRUcMn7ISQ41sY,259
|
|
2
|
-
src/ai_review.py,sha256=Wfeo72uTsjuoKMW5ToqFi6edfVIWTMgqPIbv9_W8Z-Y,34482
|
|
3
|
-
src/config.py,sha256=_k0XPY-XVO6xjgwT2zWfox1AQztbk6fkqeqmiSFcGBA,16435
|
|
4
|
-
src/context_extractor.py,sha256=qyiAUjznPfWyZw9VN34nf7OYcZrPUp92pUvR_Y7uSi0,24466
|
|
5
|
-
src/formatter.py,sha256=68kz1U6tMndLcmtJRImN4jAIpxXtog3cKtiJ-zc3S0M,19027
|
|
6
|
-
src/git_utils.py,sha256=yq31qkIXEbuv4zVGNwqW92N-X66SfyldSPCe5i0cX30,9812
|
|
7
|
-
src/llm_client.py,sha256=4WMDs23FxHnrjRDfsbCvedtILYZA00Slss0XrSVvtLE,53672
|
|
8
|
-
src/prompt_utils.py,sha256=FnWgHQ7UW8dEKW-4R7ngpL6DNqbW7xFEKi5JsZ7348E,2045
|
|
9
|
-
src/tfs_client.py,sha256=tZ7-UyNmayPWoptDntefUWDb2By98WkJr4v5MtEM80I,37744
|
|
10
|
-
src/token_estimator.py,sha256=4_4TXhefaNi7KHomwhhWbt7GB6KfubCQVBEbD3YoK5Q,3192
|
|
11
|
-
src/prompts/config.yaml.template,sha256=JlUSywkr0w2K94SzkbHJkkPuxM04gckfjyTJY5yPiHg,4827
|
|
12
|
-
src/prompts/review_prompt.md.template,sha256=hoGP4C6hQQpqDPVzwRTWuS5HCbaUyOgFllciKO8R61I,5019
|
|
13
|
-
code_review_ai_cli-2.2.4.dist-info/METADATA,sha256=3fRvsCtLrA_LsCmxsQEXFcF9Dit6HUW0kk8aRsuke6s,5040
|
|
14
|
-
code_review_ai_cli-2.2.4.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
15
|
-
code_review_ai_cli-2.2.4.dist-info/entry_points.txt,sha256=hzKqoXIQVpiCe4H-XV0DZcKwJY6Uj3M-WvBMEQtwr3o,49
|
|
16
|
-
code_review_ai_cli-2.2.4.dist-info/top_level.txt,sha256=74rtVfumQlgAPzR5_2CgYN24MB0XARCg0t-gzk6gTrM,4
|
|
17
|
-
code_review_ai_cli-2.2.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|