Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 38 additions & 7 deletions bugbug/tools/code_review/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ def __init__(
target_software: str = "Mozilla Firefox",
todo_enabled: bool = True,
skills: Optional[list[Skill]] = None,
review_context_repo: Optional[str] = None,
review_context_branch: str = "main",
extra_context_toml: Optional[str] = None,
content_overrides: Optional[dict[str, str]] = None,
) -> None:
super().__init__()

Expand Down Expand Up @@ -126,6 +130,11 @@ def __init__(

self.verbose = verbose

self._review_context_repo = review_context_repo
self._review_context_branch = review_context_branch
self._extra_context_toml = extra_context_toml
self._content_overrides = content_overrides

@property
def _agent_model_name(self) -> str:
model = self._agent_model
Expand Down Expand Up @@ -182,25 +191,45 @@ def create(cls, **kwargs):
def count_tokens(self, text):
return len(self._tokenizer.encode(text))

def generate_initial_prompt(self, patch: Patch, patch_summary: str) -> str:
def generate_initial_prompt(
self, patch: Patch, patch_summary: str, external_context: str = ""
) -> str:
created_before = patch.date_created if self.is_experiment_env else None

return FIRST_MESSAGE_TEMPLATE.format(
patch=format_patch_set(patch.patch_set),
patch_summarization=patch_summary,
external_context=external_context,
comment_examples=self._get_comment_examples(patch, created_before),
approved_examples=self._get_generated_examples(patch, created_before),
)

async def generate_review_comments(
self, patch: Patch, patch_summary: str
) -> list[GeneratedReviewComment]:
) -> tuple[list[GeneratedReviewComment], list[dict]]:
external_context = ""
manifest: list[dict] = []
if self._review_context_repo:
from bugbug.tools.code_review.review_context import (
load_external_context_for_review,
)

external_context, manifest = await load_external_context_for_review(
patch,
self._review_context_repo,
review_context_branch=self._review_context_branch,
extra_context_toml=self._extra_context_toml,
content_overrides=self._content_overrides,
)

try:
async for chunk in self.agent.astream(
{
"messages": [
HumanMessage(
self.generate_initial_prompt(patch, patch_summary)
self.generate_initial_prompt(
patch, patch_summary, external_context
)
),
]
},
Expand All @@ -212,17 +241,18 @@ async def generate_review_comments(
except GraphRecursionError as e:
raise ModelResultError("The model could not complete the review") from e

return result["structured_response"].comments
return result["structured_response"].comments, manifest

async def run(self, patch: Patch) -> CodeReviewToolResponse:
if self.count_tokens(patch.raw_diff) > 21000:
raise LargeDiffError("The diff is too large")

patch_summary = self.patch_summarizer.run(patch)

unfiltered_suggestions = await self.generate_review_comments(
patch, patch_summary
)
(
unfiltered_suggestions,
external_content_manifest,
) = await self.generate_review_comments(patch, patch_summary)
if not unfiltered_suggestions:
logger.info("No suggestions were generated")

Expand All @@ -238,6 +268,7 @@ async def run(self, patch: Patch) -> CodeReviewToolResponse:
details={
"model": self._agent_model_name,
"num_unfiltered_suggestions": len(unfiltered_suggestions),
"external_content": external_content_manifest,
},
)

Expand Down
44 changes: 44 additions & 0 deletions bugbug/tools/code_review/data_types.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import re

import httpx
import tenacity
from pydantic import BaseModel, Field, PrivateAttr

from bugbug.tools.core.connection import get_http_client
Expand Down Expand Up @@ -82,3 +83,46 @@ async def load(self) -> str:

self._cached_body = _strip_frontmatter(response.text)
return self._cached_body


class ExternalContentLoadError(Exception):
"""Raised when an ExternalContent body cannot be loaded."""


@tenacity.retry(
stop=tenacity.stop_after_attempt(3),
wait=tenacity.wait_exponential(multiplier=1, min=1),
retry=tenacity.retry_if_exception_type(httpx.TransportError),
reraise=True,
)
async def _fetch_url(url: str) -> httpx.Response:
response = await get_http_client().get(url, timeout=30)
response.raise_for_status()
return response


class ExternalContent(BaseModel):
"""An external file fetched and injected as context for the review."""

name: str = Field(description="A unique identifier for this content item.")
url: str = Field(description="HTTPS URL of the file to fetch.")
description: str = Field(
description="Short description of what this content provides."
)

_cached_body: str | None = PrivateAttr(default=None)

async def load(self) -> str:
"""Return the content body, fetching and caching it on first use."""
if self._cached_body is not None:
return self._cached_body

try:
response = await _fetch_url(self.url)
except httpx.HTTPError as e:
raise ExternalContentLoadError(
f"Could not load content '{self.name}' from {self.url}"
) from e

self._cached_body = _strip_frontmatter(response.text)
return self._cached_body
4 changes: 4 additions & 0 deletions bugbug/tools/code_review/prompts.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@
{patch_summarization}
</patch_summary>

<external-resources>
{external_context}
Comment thread
padenot marked this conversation as resolved.
</external-resources>


Here are examples of good code review comments to guide your style and approach:

Expand Down
Loading