Skip to content

Commit e79cca0

Browse files
Merge branch 'master' into dependabot/pip/langchain-mistralai-1.1.1
2 parents 6aaba40 + e31ecbe commit e79cca0

File tree

8 files changed

+152
-65
lines changed

8 files changed

+152
-65
lines changed

bugbug/code_search/searchfox_api.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,13 @@ def definitions_to_results(self, commit_hash, definitions):
235235
)
236236
),
237237
)
238+
if source is None:
239+
logger.warning(
240+
"Could not extract source for %s:%d",
241+
definition["path"],
242+
definition["start"],
243+
)
244+
continue
238245
result.append(
239246
Function(
240247
definition["name"],

bugbug/tools/code_review/agent.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ def _generate_suggestions(self, patch: Patch):
183183
},
184184
context=CodeReviewContext(patch=patch),
185185
stream_mode="values",
186+
config={"recursion_limit": 50},
186187
):
187188
result = chunk
188189
except GraphRecursionError as e:

bugbug/tools/code_review/langchain_tools.py

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,24 +20,31 @@ class CodeReviewContext:
2020

2121

2222
@tool
23-
def expand_context(file_path: str, line_number: int) -> str:
24-
"""Expand the context around a specific line in a file diff.
23+
def expand_context(file_path: str, start_line: int, end_line: int) -> str:
24+
"""Show the content of a file between specified line numbers as it is before the patch.
25+
26+
Be careful to not fill your context window with too much data. Request the
27+
minimum amount of context necessary to understand the code, but do not split
28+
what you really need into multiple requests if the line range is continuous.
2529
2630
Args:
2731
file_path: The path to the file.
28-
line_number: The line number to expand context around. It should be based on the original file, not the patch.
32+
start_line: The starting line number in the original file. Minimum is 1.
33+
end_line: The ending line number in the original file. Maximum is the total number of lines in the file.
2934
3035
Returns:
31-
Lines of code around the specified line number.
36+
The content of the file between the specified line numbers.
3237
"""
3338
runtime = get_runtime(CodeReviewContext)
34-
file_content = runtime.context.patch.get_old_file(file_path)
3539

36-
# TODO: Expanding the context using an AST parser like tree-sitter to
37-
# include the whole function or class when it is relatively small.
40+
try:
41+
file_content = runtime.context.patch.get_old_file(file_path)
42+
except FileNotFoundError:
43+
return "File not found in the repository before the patch."
44+
3845
lines = file_content.splitlines()
39-
start = max(0, line_number - 20)
40-
end = min(len(lines), line_number + 20)
46+
start = max(1, start_line) - 1
47+
end = min(len(lines), end_line)
4148

4249
# Format the output with line numbers that match the original file.
4350
line_number_width = len(str(end))

bugbug/tools/core/platforms/bugzilla.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ class Bug:
177177
"""Represents a Bugzilla bug from bugzilla.mozilla.org."""
178178

179179
def __init__(self, data: dict):
180-
self.metadata = data
180+
self._metadata = data
181181

182182
@staticmethod
183183
def get(bug_id: int) -> "Bug":
@@ -197,6 +197,10 @@ def get(bug_id: int) -> "Bug":
197197

198198
return Bug(bug_data)
199199

200+
@property
201+
def summary(self) -> str:
202+
return self._metadata["summary"]
203+
200204
def to_md(self) -> str:
201205
"""Return a markdown representation of the bug."""
202-
return bug_dict_to_markdown(self.metadata)
206+
return bug_dict_to_markdown(self._metadata)

bugbug/tools/core/platforms/phabricator.py

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@
1414
import tenacity
1515
from tqdm import tqdm
1616

17-
from bugbug import bugzilla, db, phabricator, utils
17+
from bugbug import db, phabricator, utils
1818
from bugbug.tools.core.data_types import InlineComment, ReviewRequest
1919
from bugbug.tools.core.platforms.base import Patch, ReviewData
20+
from bugbug.tools.core.platforms.bugzilla import Bug
2021
from bugbug.utils import get_secret
2122

2223
logger = getLogger(__name__)
@@ -130,7 +131,7 @@ def revision_phid(self) -> str:
130131

131132
raise ValueError("Cannot determine revision PHID")
132133

133-
def _get_file(self, file_path: str, is_before_patch: bool) -> str:
134+
def _get_file_from_patch(self, file_path: str, is_before_patch: bool) -> str:
134135
for changeset in self._changesets:
135136
if changeset["fields"]["path"]["displayPath"] == file_path:
136137
break
@@ -150,8 +151,32 @@ def _get_file(self, file_path: str, is_before_patch: bool) -> str:
150151

151152
return r.text
152153

154+
def _get_file_from_repo(self, file_path: str, commit_hash: str) -> str:
155+
r = utils.get_session("hgmo").get(
156+
f"https://hg.mozilla.org/mozilla-unified/raw-file/{commit_hash}/{file_path}",
157+
headers={
158+
"User-Agent": utils.get_user_agent(),
159+
},
160+
)
161+
162+
if r.status_code == 404:
163+
raise FileNotFoundError(
164+
f"File {file_path} not found in commit {commit_hash}"
165+
)
166+
167+
r.raise_for_status()
168+
return r.text
169+
153170
def get_old_file(self, file_path: str) -> str:
154-
return self._get_file(file_path, is_before_patch=True)
171+
if file_path.startswith("b/") or file_path.startswith("a/"):
172+
file_path = file_path[2:]
173+
174+
try:
175+
return self._get_file_from_patch(file_path, is_before_patch=True)
176+
except FileNotFoundError:
177+
return self._get_file_from_repo(
178+
file_path, commit_hash=self.base_commit_hash
179+
)
155180

156181
@cached_property
157182
def _changesets(self) -> list[dict]:
@@ -251,29 +276,16 @@ def _revision_metadata(self) -> dict:
251276
return revision
252277

253278
@cached_property
254-
def _bug_metadata(self) -> dict | None:
255-
id = self.bug_id
256-
bugs = bugzilla.get(id)
257-
258-
if id not in bugs:
259-
logger.warning(
260-
"Bug %d not found in Bugzilla. This might be a private bug.", id
261-
)
262-
return None
263-
264-
return bugs[id]
279+
def bug(self) -> Bug:
280+
return Bug.get(self.bug_id)
265281

266282
@property
267283
def bug_id(self) -> int:
268284
return int(self._revision_metadata["fields"]["bugzilla.bug-id"])
269285

270-
@cached_property
286+
@property
271287
def bug_title(self) -> str:
272-
if not self._bug_metadata:
273-
# Use a placeholder when the bug metadata is not available
274-
return "--"
275-
276-
return self._bug_metadata["summary"]
288+
return self.bug.summary
277289

278290
@cached_property
279291
def patch_title(self) -> str:

requirements.txt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@ amqp==5.3.1
22
beautifulsoup4==4.14.3
33
boto3==1.41.2
44
imbalanced-learn==0.14.0
5-
langchain==1.0.8
5+
langchain==1.1.3
66
langchain-anthropic==1.2.0
77
langchain-classic==1.0.0
88
langchain-community==0.4.1
9-
langchain-google-genai==3.1.0
9+
langchain-google-genai==4.0.0
1010
langchain-mistralai==1.1.1
1111
langchain-openai==1.0.3
12-
langgraph==1.0.4
12+
langgraph==1.0.5
1313
libmozdata==0.2.12
1414
llama-cpp-python==0.2.90
1515
lmdb==1.7.5

scripts/code_review_tool_evaluator.py

Lines changed: 84 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
from bugbug.tools import code_review
3333
from bugbug.tools.code_review.utils import parse_model_output
3434
from bugbug.tools.core import llms
35+
from bugbug.tools.core.exceptions import ModelResultError
3536
from bugbug.vectordb import QdrantVectorDB
3637

3738
code_review.TARGET_SOFTWARE = "Mozilla Firefox"
@@ -221,7 +222,9 @@ def print_evaluation_matches(matching_results: list[dict]):
221222
)
222223

223224

224-
def get_tool_variants() -> list[tuple[str, code_review.CodeReviewTool]]:
225+
def get_tool_variants(
226+
variants: list[str],
227+
) -> list[tuple[str, code_review.CodeReviewTool]]:
225228
"""Returns a list of tool variants to evaluate.
226229
227230
Returns:
@@ -254,31 +257,33 @@ def get_file(commit_hash, path):
254257

255258
tool_variants = []
256259

257-
tool_variants.append(
258-
(
259-
"Claude",
260-
code_review.CodeReviewTool(
261-
llm=llms.create_anthropic_llm(),
262-
function_search=function_search,
263-
review_comments_db=review_comments_db,
264-
suggestions_feedback_db=suggestions_feedback_db,
265-
verbose=VERBOSE_CODE_REVIEW,
266-
),
260+
if "claude" in variants:
261+
tool_variants.append(
262+
(
263+
"Claude",
264+
code_review.CodeReviewTool(
265+
llm=llms.create_anthropic_llm(),
266+
function_search=function_search,
267+
review_comments_db=review_comments_db,
268+
suggestions_feedback_db=suggestions_feedback_db,
269+
verbose=VERBOSE_CODE_REVIEW,
270+
),
271+
)
267272
)
268-
)
269273

270-
tool_variants.append(
271-
(
272-
"GPT",
273-
code_review.CodeReviewTool(
274-
llm=llms.create_openai_llm(),
275-
function_search=function_search,
276-
review_comments_db=review_comments_db,
277-
suggestions_feedback_db=suggestions_feedback_db,
278-
verbose=VERBOSE_CODE_REVIEW,
279-
),
274+
if "gpt" in variants:
275+
tool_variants.append(
276+
(
277+
"GPT",
278+
code_review.CodeReviewTool(
279+
llm=llms.create_openai_llm(),
280+
function_search=function_search,
281+
review_comments_db=review_comments_db,
282+
suggestions_feedback_db=suggestions_feedback_db,
283+
verbose=VERBOSE_CODE_REVIEW,
284+
),
285+
)
280286
)
281-
)
282287

283288
return tool_variants
284289

@@ -348,25 +353,52 @@ def get_latest_evaluation_results_file(results_dir: str | None):
348353
return latests_files
349354

350355

356+
def get_ongoing_evaluation_results_file(results_dir: str | None):
357+
import glob
358+
import os
359+
360+
base_file = get_latest_evaluation_results_file(results_dir)
361+
files = [
362+
file
363+
for file in glob.glob("evaluation_results_*.csv", root_dir=results_dir)
364+
if "#" not in file and file > base_file
365+
]
366+
if not files:
367+
raise FileNotFoundError("No ongoing evaluation results file found.")
368+
369+
latests_file = max(files)
370+
if results_dir:
371+
return os.path.join(results_dir, latests_file)
372+
373+
return latests_file
374+
375+
351376
def main(args):
352377
review_platform = "phabricator"
353378
review_data: code_review.ReviewData = code_review.review_data_classes[
354379
review_platform
355380
]()
356381

357-
tool_variants = get_tool_variants()
382+
tool_variants = get_tool_variants(args.variants)
358383

359384
evaluator = FeedbackEvaluator(args.evaluation_dataset)
360385

361-
is_first_result = True
362386
result_file = os.path.join(
363387
args.results_dir,
364388
"code_review_tool_evaluator.csv",
365389
)
366-
evaluation_results_file = os.path.join(
367-
args.results_dir,
368-
f"evaluation_results_{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}.csv",
369-
)
390+
is_first_result = not os.path.exists(result_file)
391+
392+
if is_first_result:
393+
evaluation_results_file = os.path.join(
394+
args.results_dir,
395+
f"evaluation_results_{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}.csv",
396+
)
397+
seen_patches = set()
398+
else:
399+
evaluation_results_file = get_ongoing_evaluation_results_file(args.results_dir)
400+
seen_patches = set(pd.read_csv(evaluation_results_file)["diff_id"].to_list())
401+
370402
result_unique_columns = ["Review Request ID", "File", "Line", "Comment Number"]
371403
result_all_columns = result_unique_columns + [
372404
f"{title} ({variant_name})"
@@ -421,6 +453,18 @@ def main(args):
421453
)
422454

423455
for review_request_id, review_request in selected_review_requests:
456+
if review_request_id in [227266, 233414]:
457+
print(
458+
f"Skipping Review Request ID {review_request_id} because it is known to cause issues."
459+
)
460+
continue
461+
462+
if review_request.patch_id in seen_patches:
463+
print(
464+
f"Skipping Review Request ID {review_request_id} (Diff ID {review_request.patch_id}) because it was already evaluated."
465+
)
466+
continue
467+
424468
print("---------------------------------------------------------")
425469
print(f"Review Request ID: {review_request_id}")
426470
print(f"Patch ID: {review_request.patch_id}")
@@ -443,6 +487,9 @@ def main(args):
443487
except code_review.LargeDiffError:
444488
print("Skipping the patch because it is too large.")
445489
continue
490+
except ModelResultError as e:
491+
print("Error while running the tool:", e)
492+
continue
446493

447494
print_prettified_comments(comments)
448495
comment_per_line_counter = defaultdict(int)
@@ -548,6 +595,14 @@ def main(args):
548595
action="store",
549596
help="the evaluation strategy to use",
550597
)
598+
parser.add_argument(
599+
"--variant",
600+
dest="variants",
601+
action="append",
602+
help="the variants to use, use multiple times for multiple variants",
603+
choices=["claude", "gpt"],
604+
required=True,
605+
)
551606

552607
args = parser.parse_args()
553608

scripts/code_review_tool_evaluator_report.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22

33
import pandas as pd
44

5-
from scripts.code_review_tool_evaluator import get_latest_evaluation_results_file
5+
import scripts.code_review_tool_evaluator as evaluator_script
66

77
evaluation_results = pd.read_csv(
8-
get_latest_evaluation_results_file("../evaluation_results")
8+
# evaluator_script.get_latest_evaluation_results_file("../evaluation_results")
9+
evaluator_script.get_ongoing_evaluation_results_file("../evaluation_results")
910
)
1011

1112
# %%

0 commit comments

Comments
 (0)