Skip to content
Open
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
49 changes: 44 additions & 5 deletions bugbug/tools/code_review/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,25 +182,30 @@ 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
self, patch: Patch, patch_summary: str, external_context: str = ""
) -> list[GeneratedReviewComment]:
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 @@ -214,14 +219,47 @@ async def generate_review_comments(

return result["structured_response"].comments

async def run(self, patch: Patch) -> CodeReviewToolResponse:
async def run(
self,
patch: Patch,
review_context_repo: Optional[str] = None,
review_context_branch: str = "main",
extra_context_toml: Optional[str] = None,
content_overrides: Optional[dict[str, str]] = None,
) -> CodeReviewToolResponse:
Comment on lines +222 to +229

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The run method should stay as it is (i.e., run(self, patch: Patch)) to not break the interface, which we use in the extermination pipeline as well.

if self.count_tokens(patch.raw_diff) > 21000:
raise LargeDiffError("The diff is too large")

patch_summary = self.patch_summarizer.run(patch)

external_context = ""
external_content_manifest = []
if review_context_repo:
from bugbug.tools.code_review.review_context import (
external_content_manifest as build_external_content_manifest,
)
from bugbug.tools.code_review.review_context import (
format_external_content,
load_external_content_for_diff,
)

bug_id = getattr(patch, "bug_id", None)
content_items = await load_external_content_for_diff(
patch.raw_diff,
review_context_repo,
review_context_branch=review_context_branch,
bug_id=bug_id,
extra_context_toml=extra_context_toml,
content_overrides=content_overrides,
)
if content_items:
external_context = format_external_content(content_items)
external_content_manifest = build_external_content_manifest(
content_items
)

unfiltered_suggestions = await self.generate_review_comments(
patch, patch_summary
patch, patch_summary, external_context
)
if not unfiltered_suggestions:
logger.info("No suggestions were generated")
Expand All @@ -238,6 +276,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
1 change: 1 addition & 0 deletions bugbug/tools/code_review/prompts.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
<patch_summary>
{patch_summarization}
</patch_summary>
{external_context}


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