-
Notifications
You must be signed in to change notification settings - Fork 8
refactor: use jinja for comments instead of inline string formatting #84
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,143 +1,92 @@ | ||
| import textwrap | ||
| import pathlib | ||
| from typing import ClassVar | ||
|
|
||
| from lgtm_ai.ai.schemas import PublishMetadata, Review, ReviewComment, ReviewGuide, ReviewScore | ||
| from jinja2 import Environment, FileSystemLoader | ||
| from lgtm_ai.ai.schemas import PublishMetadata, Review, ReviewComment, ReviewGuide | ||
| from lgtm_ai.formatters.base import Formatter | ||
| from lgtm_ai.formatters.constants import CATEGORY_MAP, SCORE_MAP, SEVERITY_MAP | ||
| from pydantic_ai.usage import Usage | ||
|
|
||
|
|
||
| class MarkDownFormatter(Formatter[str]): | ||
| def format_review_summary_section(self, review: Review, comments: list[ReviewComment] | None = None) -> str: | ||
| header = textwrap.dedent(f""" | ||
| ## 🦉 lgtm Review | ||
|
|
||
| > **Score:** {self._format_score(review.review_response.score)} | ||
|
|
||
| ### 🔍 Summary | ||
|
|
||
| """) | ||
| summary = header + review.review_response.summary | ||
| if comments: | ||
| summary += f"\n\n{self.format_review_comments_section(comments)}" | ||
| REVIEW_SUMMARY_TEMPLATE: ClassVar[str] = "review_summary.md.j2" | ||
| REVIEW_COMMENTS_SECTION_TEMPLATE: ClassVar[str] = "review_comments_section.md.j2" | ||
| REVIEW_COMMENT_TEMPLATE: ClassVar[str] = "review_comment.md.j2" | ||
| REVIEW_GUIDE_TEMPLATE: ClassVar[str] = "review_guide.md.j2" | ||
| SNIPPET_TEMPLATE: ClassVar[str] = "snippet.md.j2" | ||
| USAGES_SUMMARY_TEMPLATE: ClassVar[str] = "usages_summary.md.j2" | ||
| METADATA_TEMPLATE: ClassVar[str] = "metadata.md.j2" | ||
|
|
||
| def __init__(self) -> None: | ||
| template_dir = pathlib.Path(__file__).parent / "templates" | ||
| self.env = Environment(loader=FileSystemLoader(template_dir), autoescape=True) | ||
|
|
||
| summary += self._format_metadata(review.metadata) | ||
| return summary | ||
| def format_review_summary_section(self, review: Review, comments: list[ReviewComment] | None = None) -> str: | ||
| template = self.env.get_template(self.REVIEW_SUMMARY_TEMPLATE) | ||
| comments_section = self.format_review_comments_section(comments or []) | ||
| metadata = self._format_metadata(review.metadata) | ||
| return template.render( | ||
| score=review.review_response.score, | ||
| score_icon=SCORE_MAP[review.review_response.score], | ||
| summary=review.review_response.summary, | ||
| comments_section=comments_section, | ||
| metadata=metadata, | ||
| ) | ||
|
|
||
| def format_review_comments_section(self, comments: list[ReviewComment]) -> str: | ||
| if not comments: | ||
| return "" | ||
| lines = ["**Specific Comments:**"] | ||
| for comment in comments: | ||
| lines.append(f"- {self.format_review_comment(comment, with_footer=False)}") | ||
| return "\n\n".join(lines) | ||
| template = self.env.get_template(self.REVIEW_COMMENTS_SECTION_TEMPLATE) | ||
| rendered_comments = [self.format_review_comment(comment, with_footer=False) for comment in comments] | ||
| return template.render(comments=rendered_comments) | ||
|
|
||
| def format_review_comment(self, comment: ReviewComment, *, with_footer: bool = True) -> str: | ||
| header_section = "\n\n".join( | ||
| [ | ||
| f"#### 🦉 {CATEGORY_MAP[comment.category]} {comment.category}", | ||
| f"> **Severity:** {comment.severity} {SEVERITY_MAP[comment.severity]}", | ||
| ] | ||
| template = self.env.get_template(self.REVIEW_COMMENT_TEMPLATE) | ||
| header_category = CATEGORY_MAP[comment.category] | ||
| severity_icon = SEVERITY_MAP[comment.severity] | ||
| snippet = self._format_snippet(comment) if comment.quote_snippet else None | ||
| return template.render( | ||
| category=header_category, | ||
| category_key=comment.category, | ||
| severity=comment.severity, | ||
| severity_icon=severity_icon, | ||
| snippet=snippet, | ||
| comment=comment.comment, | ||
| with_footer=with_footer, | ||
| new_path=comment.new_path, | ||
| line_number=comment.line_number, | ||
| relative_line_number=comment.relative_line_number, | ||
| ) | ||
| comment_section = ( | ||
| f"\n{self._format_snippet(comment)}\n{comment.comment}" if comment.quote_snippet else comment.comment | ||
| ) | ||
|
|
||
| footer_section = ( | ||
| textwrap.dedent(f""" | ||
|
|
||
| <details><summary>More information about this comment</summary> | ||
|
|
||
| - **File**: `{comment.new_path}` | ||
| - **Line**: `{comment.line_number}` | ||
| - **Relative line**: `{comment.relative_line_number}` | ||
|
|
||
| </details> | ||
| """) | ||
| if with_footer | ||
| else "" | ||
| ) | ||
|
|
||
| return f"{header_section}\n\n{comment_section}\n\n{footer_section}" | ||
|
|
||
| def format_guide(self, guide: ReviewGuide) -> str: | ||
| header = textwrap.dedent(""" | ||
| ## 🦉 lgtm Reviewer Guide | ||
|
|
||
| """) | ||
|
|
||
| summary = guide.guide_response.summary | ||
| # Format key changes as a markdown table | ||
| key_changes = ["| File Name | Description |", "| ---- | ---- |"] + [ | ||
| f"| {change.file_name} | {change.description} |" for change in guide.guide_response.key_changes | ||
| ] | ||
|
|
||
| # Format checklist items as a checklist | ||
| checklist = [f"- [ ] {item.description}" for item in guide.guide_response.checklist] | ||
|
|
||
| # Format references as a list | ||
| if guide.guide_response.references: | ||
| references = [f"- [{item.title}]({item.url})" for item in guide.guide_response.references] | ||
| else: | ||
| references = [] | ||
|
|
||
| # Combine all sections | ||
|
|
||
| summary = ( | ||
| header | ||
| + "### 🔍 Summary\n\n" | ||
| + summary | ||
| + "\n\n### 🔑 Key Changes\n\n" | ||
| + "\n".join(key_changes) | ||
| + "\n\n### ✅ Reviewer Checklist\n\n" | ||
| + "\n".join(checklist) | ||
| template = self.env.get_template(self.REVIEW_GUIDE_TEMPLATE) | ||
| key_changes = guide.guide_response.key_changes | ||
| checklist = guide.guide_response.checklist | ||
| references = guide.guide_response.references | ||
| metadata = self._format_metadata(guide.metadata) | ||
| return template.render( | ||
| summary=guide.guide_response.summary, | ||
| key_changes=key_changes, | ||
| checklist=checklist, | ||
| references=references, | ||
| metadata=metadata, | ||
| ) | ||
| if references: | ||
| summary += "\n\n### 📚 References\n\n" + "\n".join(references) | ||
|
|
||
| summary += self._format_metadata(guide.metadata) | ||
| return summary | ||
|
|
||
| def _format_score(self, score: ReviewScore) -> str: | ||
| return f"{score} {SCORE_MAP[score]}" | ||
|
|
||
| def _format_snippet(self, comment: ReviewComment) -> str: | ||
| return f"\n\n```{comment.programming_language.lower()}\n{comment.quote_snippet}\n```\n\n" | ||
| template = self.env.get_template(self.SNIPPET_TEMPLATE) | ||
| return template.render(language=comment.programming_language.lower(), snippet=comment.quote_snippet) | ||
|
|
||
| def _format_usages_summary(self, usages: list[Usage]) -> str: | ||
| formatted_usage_calls = [] | ||
| for i, usage in enumerate(usages): | ||
| formatted_usage_calls += [self._format_usage_call_collapsible(usage, i)] | ||
|
|
||
| return f""" | ||
| <details><summary>Usage summary</summary> | ||
| {"\n".join(formatted_usage_calls)} | ||
| **Total tokens**: `{sum([usage.total_tokens or 0 for usage in usages])}` | ||
| </details> | ||
| """ | ||
|
|
||
| def _format_usage_call_collapsible(self, usage: Usage, index: int) -> str: | ||
| return f""" | ||
| <details><summary>Call {index + 1}</summary> | ||
|
|
||
| - **Request count**: `{usage.requests}` | ||
| - **Request tokens**: `{usage.request_tokens}` | ||
| - **Response tokens**: `{usage.response_tokens}` | ||
| - **Total tokens**: `{usage.total_tokens}` | ||
| </details> | ||
| """ | ||
| template = self.env.get_template(self.USAGES_SUMMARY_TEMPLATE) | ||
| total_tokens = sum(usage.total_tokens or 0 for usage in usages) | ||
| return template.render(usages=usages, total_tokens=total_tokens) | ||
|
|
||
| def _format_metadata(self, metadata: PublishMetadata) -> str: | ||
| return textwrap.dedent(f""" | ||
|
|
||
| <details><summary>More information</summary> | ||
|
|
||
| - **Id**: `{metadata.uuid}` | ||
| - **Model**: `{metadata.model_name}` | ||
| - **Created at**: `{metadata.created_at}` | ||
|
|
||
| {self._format_usages_summary(metadata.usages)} | ||
|
|
||
| > See the [📚 lgtm-ai repository](https://github.com/elementsinteractive/lgtm-ai) for more information about lgtm. | ||
|
|
||
| </details> | ||
| """) | ||
| template = self.env.get_template(self.METADATA_TEMPLATE) | ||
| usages_summary = self._format_usages_summary(metadata.usages) | ||
| return template.render( | ||
| uuid=metadata.uuid, | ||
| model_name=metadata.model_name, | ||
| created_at=metadata.created_at, | ||
| usages_summary=usages_summary, | ||
| ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| <details><summary>More information</summary> | ||
|
|
||
| - **Id**: `{{ uuid }}` | ||
| - **Model**: `{{ model_name }}` | ||
| - **Created at**: `{{ created_at }}` | ||
|
|
||
|
|
||
| {{ usages_summary | safe }} | ||
|
|
||
|
|
||
| > See the [📚 lgtm-ai repository](https://github.com/elementsinteractive/lgtm-ai) for more information about lgtm. | ||
|
|
||
| </details> | ||
|
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| #### 🦉 {{ category }} {{ category_key }} | ||
|
|
||
| > **Severity:** {{ severity }} {{ severity_icon }} | ||
|
|
||
| {% if snippet %} | ||
scastlara marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| {{ snippet | safe }} | ||
scastlara marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| {% endif %} | ||
| {{ comment | safe }} | ||
|
|
||
| {% if with_footer %} | ||
| <details><summary>More information about this comment</summary> | ||
|
|
||
| - **File**: `{{ new_path }}` | ||
| - **Line**: `{{ line_number }}` | ||
| - **Relative line**: `{{ relative_line_number }}` | ||
| </details> | ||
| {% endif %} | ||
5 changes: 5 additions & 0 deletions
5
src/lgtm_ai/formatters/templates/review_comments_section.md.j2
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| {% if comments %}**Specific Comments:** | ||
|
|
||
| {% for comment in comments %}- {{ comment | safe }} | ||
| {% endfor %} | ||
| {% endif %} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
|
|
||
| ## 🦉 lgtm Reviewer Guide | ||
|
|
||
| ### 🔍 Summary | ||
|
|
||
| {{ summary | safe }} | ||
|
|
||
| ### 🔑 Key Changes | ||
|
|
||
| | File Name | Description | | ||
| | ---- | ---- | | ||
| {% for change in key_changes -%} | ||
| | {{ change.file_name }} | {{ change.description }} | | ||
| {% endfor %} | ||
|
|
||
| ### ✅ Reviewer Checklist | ||
|
|
||
| {% for item in checklist %} | ||
| - [ ] {{ item.description }} | ||
| {% endfor %} | ||
|
|
||
| {% if references %} | ||
| ### 📚 References | ||
| {% for ref in references %} | ||
| - [{{ ref.title }}]({{ ref.url }}) | ||
| {% endfor %} | ||
| {% endif %} | ||
|
|
||
| {{ metadata | safe }} | ||
|
|
||
|
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
|
|
||
| ## 🦉 lgtm Review | ||
|
|
||
| > **Score:** {{ score }} {{ score_icon }} | ||
|
|
||
| ### 🔍 Summary | ||
|
|
||
| {{ summary | safe }} | ||
|
|
||
| {% if comments_section %} | ||
| {{ comments_section | safe}} | ||
| {% endif %} | ||
|
|
||
| {{ metadata | safe }} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| ```{{ language }} | ||
| {{ snippet | safe }} | ||
| ``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| <details><summary>Usage summary</summary> | ||
| {% for usage in usages %} | ||
|
|
||
| <details><summary>Call {{ loop.index }}</summary> | ||
|
|
||
| - **Request count**: `{{ usage.requests }}` | ||
| - **Request tokens**: `{{ usage.request_tokens }}` | ||
| - **Response tokens**: `{{ usage.response_tokens }}` | ||
| - **Total tokens**: `{{ usage.total_tokens }}` | ||
| </details> | ||
|
|
||
| {% endfor %} | ||
| **Total tokens**: `{{ total_tokens }}` | ||
| </details> |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.