Skip to content

Commit aecf8ac

Browse files
committed
refactor: use jinja for prompts instead of inline string formatting
1 parent e2732ca commit aecf8ac

File tree

13 files changed

+281
-139
lines changed

13 files changed

+281
-139
lines changed

poetry.lock

Lines changed: 4 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ dependencies = [
3737
"python-gitlab>=5.1.0,<6.0.0",
3838
"rich>=13.9.4,<14.0.0",
3939
"pygithub>=2.6.1,<3.0.0",
40-
"httpx (>=0.28.1,<0.29.0)"
40+
"httpx (>=0.28.1,<0.29.0)",
41+
"jinja2 (>=3.1.6,<4.0.0)"
4142
]
4243

4344
[tool.poetry.group.dev.dependencies]

src/lgtm_ai/formatters/markdown.py

Lines changed: 86 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -1,143 +1,114 @@
1-
import textwrap
1+
import pathlib
2+
from typing import Any, ClassVar
23

4+
from jinja2 import Environment, FileSystemLoader
35
from lgtm_ai.ai.schemas import PublishMetadata, Review, ReviewComment, ReviewGuide, ReviewScore
46
from lgtm_ai.formatters.base import Formatter
57
from lgtm_ai.formatters.constants import CATEGORY_MAP, SCORE_MAP, SEVERITY_MAP
68
from pydantic_ai.usage import Usage
79

810

911
class MarkDownFormatter(Formatter[str]):
10-
def format_review_summary_section(self, review: Review, comments: list[ReviewComment] | None = None) -> str:
11-
header = textwrap.dedent(f"""
12-
## 🦉 lgtm Review
13-
14-
> **Score:** {self._format_score(review.review_response.score)}
15-
16-
### 🔍 Summary
17-
18-
""")
19-
summary = header + review.review_response.summary
20-
if comments:
21-
summary += f"\n\n{self.format_review_comments_section(comments)}"
12+
REVIEW_SUMMARY_TEMPLATE: ClassVar[str] = "review_summary.md.j2"
13+
REVIEW_COMMENTS_SECTION_TEMPLATE: ClassVar[str] = "review_comments_section.md.j2"
14+
REVIEW_COMMENT_TEMPLATE: ClassVar[str] = "review_comment.md.j2"
15+
REVIEW_GUIDE_TEMPLATE: ClassVar[str] = "review_guide.md.j2"
16+
SCORE_TEMPLATE: ClassVar[str] = "score.md.j2"
17+
SNIPPET_TEMPLATE: ClassVar[str] = "snippet.md.j2"
18+
USAGES_SUMMARY_TEMPLATE: ClassVar[str] = "usages_summary.md.j2"
19+
USAGE_CALL_COLLAPSIBLE_TEMPLATE: ClassVar[str] = "usage_call_collapsible.md.j2"
20+
METADATA_TEMPLATE: ClassVar[str] = "metadata.md.j2"
21+
22+
def __init__(self) -> None:
23+
template_dir = pathlib.Path(__file__).parent / "templates"
24+
self.env = Environment(
25+
loader=FileSystemLoader(template_dir),
26+
# We need to render markdown+html in these templates, not only in specific variables
27+
# but in the whole document, so we disable autoescaping
28+
autoescape=False, # noqa: S701
29+
)
2230

23-
summary += self._format_metadata(review.metadata)
24-
return summary
31+
def format_review_summary_section(self, review: Review, comments: list[ReviewComment] | None = None) -> str:
32+
template = self.env.get_template(self.REVIEW_SUMMARY_TEMPLATE)
33+
score = self._format_score(review.review_response.score)
34+
comments_section = self.format_review_comments_section(comments or [])
35+
metadata = self._format_metadata(review.metadata)
36+
return template.render(
37+
score=score,
38+
summary=review.review_response.summary,
39+
comments_section=comments_section,
40+
metadata=metadata,
41+
)
2542

2643
def format_review_comments_section(self, comments: list[ReviewComment]) -> str:
2744
if not comments:
2845
return ""
29-
lines = ["**Specific Comments:**"]
30-
for comment in comments:
31-
lines.append(f"- {self.format_review_comment(comment, with_footer=False)}")
32-
return "\n\n".join(lines)
46+
template = self.env.get_template(self.REVIEW_COMMENTS_SECTION_TEMPLATE)
47+
rendered_comments = [self.format_review_comment(comment, with_footer=False) for comment in comments]
48+
return template.render(comments=rendered_comments)
3349

3450
def format_review_comment(self, comment: ReviewComment, *, with_footer: bool = True) -> str:
35-
header_section = "\n\n".join(
36-
[
37-
f"#### 🦉 {CATEGORY_MAP[comment.category]} {comment.category}",
38-
f"> **Severity:** {comment.severity} {SEVERITY_MAP[comment.severity]}",
39-
]
40-
)
41-
comment_section = (
42-
f"\n{self._format_snippet(comment)}\n{comment.comment}" if comment.quote_snippet else comment.comment
51+
template = self.env.get_template(self.REVIEW_COMMENT_TEMPLATE)
52+
header_category = CATEGORY_MAP[comment.category]
53+
severity_icon = SEVERITY_MAP[comment.severity]
54+
snippet = self._format_snippet(comment) if comment.quote_snippet else None
55+
return template.render(
56+
category=header_category,
57+
category_key=comment.category,
58+
severity=comment.severity,
59+
severity_icon=severity_icon,
60+
snippet=snippet,
61+
comment=comment.comment,
62+
with_footer=with_footer,
63+
new_path=comment.new_path,
64+
line_number=comment.line_number,
65+
relative_line_number=comment.relative_line_number,
4366
)
4467

45-
footer_section = (
46-
textwrap.dedent(f"""
47-
48-
<details><summary>More information about this comment</summary>
49-
50-
- **File**: `{comment.new_path}`
51-
- **Line**: `{comment.line_number}`
52-
- **Relative line**: `{comment.relative_line_number}`
53-
54-
</details>
55-
""")
56-
if with_footer
57-
else ""
58-
)
59-
60-
return f"{header_section}\n\n{comment_section}\n\n{footer_section}"
61-
6268
def format_guide(self, guide: ReviewGuide) -> str:
63-
header = textwrap.dedent("""
64-
## 🦉 lgtm Reviewer Guide
65-
66-
""")
67-
68-
summary = guide.guide_response.summary
69-
# Format key changes as a markdown table
70-
key_changes = ["| File Name | Description |", "| ---- | ---- |"] + [
71-
f"| {change.file_name} | {change.description} |" for change in guide.guide_response.key_changes
72-
]
73-
74-
# Format checklist items as a checklist
75-
checklist = [f"- [ ] {item.description}" for item in guide.guide_response.checklist]
76-
77-
# Format references as a list
78-
if guide.guide_response.references:
79-
references = [f"- [{item.title}]({item.url})" for item in guide.guide_response.references]
80-
else:
81-
references = []
82-
83-
# Combine all sections
84-
85-
summary = (
86-
header
87-
+ "### 🔍 Summary\n\n"
88-
+ summary
89-
+ "\n\n### 🔑 Key Changes\n\n"
90-
+ "\n".join(key_changes)
91-
+ "\n\n### ✅ Reviewer Checklist\n\n"
92-
+ "\n".join(checklist)
69+
template = self.env.get_template(self.REVIEW_GUIDE_TEMPLATE)
70+
key_changes = guide.guide_response.key_changes
71+
checklist = guide.guide_response.checklist
72+
references = guide.guide_response.references
73+
metadata = self._format_metadata(guide.metadata)
74+
return template.render(
75+
summary=guide.guide_response.summary,
76+
key_changes=key_changes,
77+
checklist=checklist,
78+
references=references,
79+
metadata=metadata,
9380
)
94-
if references:
95-
summary += "\n\n### 📚 References\n\n" + "\n".join(references)
96-
97-
summary += self._format_metadata(guide.metadata)
98-
return summary
9981

10082
def _format_score(self, score: ReviewScore) -> str:
101-
return f"{score} {SCORE_MAP[score]}"
83+
template = self.env.get_template(self.SCORE_TEMPLATE)
84+
return template.render(score=score, score_icon=SCORE_MAP[score])
10285

10386
def _format_snippet(self, comment: ReviewComment) -> str:
104-
return f"\n\n```{comment.programming_language.lower()}\n{comment.quote_snippet}\n```\n\n"
87+
template = self.env.get_template(self.SNIPPET_TEMPLATE)
88+
return template.render(language=comment.programming_language.lower(), snippet=comment.quote_snippet)
10589

106-
def _format_usages_summary(self, usages: list[Usage]) -> str:
107-
formatted_usage_calls = []
108-
for i, usage in enumerate(usages):
109-
formatted_usage_calls += [self._format_usage_call_collapsible(usage, i)]
110-
111-
return f"""
112-
<details><summary>Usage summary</summary>
113-
{"\n".join(formatted_usage_calls)}
114-
**Total tokens**: `{sum([usage.total_tokens or 0 for usage in usages])}`
115-
</details>
116-
"""
90+
def _format_usages_summary(self, usages: list[Any]) -> str:
91+
template = self.env.get_template(self.USAGES_SUMMARY_TEMPLATE)
92+
formatted_usage_calls = [self._format_usage_call_collapsible(usage, i) for i, usage in enumerate(usages)]
93+
total_tokens = sum([usage.total_tokens or 0 for usage in usages])
94+
return template.render(usages=formatted_usage_calls, total_tokens=total_tokens)
11795

11896
def _format_usage_call_collapsible(self, usage: Usage, index: int) -> str:
119-
return f"""
120-
<details><summary>Call {index + 1}</summary>
121-
122-
- **Request count**: `{usage.requests}`
123-
- **Request tokens**: `{usage.request_tokens}`
124-
- **Response tokens**: `{usage.response_tokens}`
125-
- **Total tokens**: `{usage.total_tokens}`
126-
</details>
127-
"""
97+
template = self.env.get_template(self.USAGE_CALL_COLLAPSIBLE_TEMPLATE)
98+
return template.render(
99+
index=index + 1,
100+
requests=usage.requests,
101+
request_tokens=usage.request_tokens,
102+
response_tokens=usage.response_tokens,
103+
total_tokens=usage.total_tokens,
104+
)
128105

129106
def _format_metadata(self, metadata: PublishMetadata) -> str:
130-
return textwrap.dedent(f"""
131-
132-
<details><summary>More information</summary>
133-
134-
- **Id**: `{metadata.uuid}`
135-
- **Model**: `{metadata.model_name}`
136-
- **Created at**: `{metadata.created_at}`
137-
138-
{self._format_usages_summary(metadata.usages)}
139-
140-
> See the [📚 lgtm-ai repository](https://github.com/elementsinteractive/lgtm-ai) for more information about lgtm.
141-
142-
</details>
143-
""")
107+
template = self.env.get_template(self.METADATA_TEMPLATE)
108+
usages_summary = self._format_usages_summary(metadata.usages)
109+
return template.render(
110+
uuid=metadata.uuid,
111+
model_name=metadata.model_name,
112+
created_at=metadata.created_at,
113+
usages_summary=usages_summary,
114+
)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<details><summary>More information</summary>
2+
3+
- **Id**: `{{ uuid }}`
4+
- **Model**: `{{ model_name }}`
5+
- **Created at**: `{{ created_at }}`
6+
7+
8+
{{ usages_summary | safe }}
9+
10+
11+
> See the [📚 lgtm-ai repository](https://github.com/elementsinteractive/lgtm-ai) for more information about lgtm.
12+
13+
</details>
14+
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#### 🦉 {{ category }} {{ category_key }}
2+
3+
> **Severity:** {{ severity }} {{ severity_icon }}
4+
{% if snippet %}
5+
6+
7+
{{ snippet }}
8+
9+
10+
{{ comment }}
11+
{% else %}
12+
{{ comment }}
13+
14+
{% endif %}
15+
16+
17+
{% if with_footer %}
18+
<details><summary>More information about this comment</summary>
19+
- **File**: `{{ new_path }}`
20+
- **Line**: `{{ line_number }}`
21+
- **Relative line**: `{{ relative_line_number }}`
22+
</details>
23+
{% endif %}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{% if comments %}**Specific Comments:**
2+
3+
{% for comment in comments %}- {{ comment }}{%- endfor %}
4+
{% endif %}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
2+
## 🦉 lgtm Reviewer Guide
3+
4+
### 🔍 Summary
5+
6+
{{ summary }}
7+
8+
### 🔑 Key Changes
9+
10+
| File Name | Description |
11+
| ---- | ---- |
12+
{% for change in key_changes -%}
13+
| {{ change.file_name }} | {{ change.description }} |
14+
{% endfor %}
15+
16+
### ✅ Reviewer Checklist
17+
18+
{% for item in checklist %}
19+
- [ ] {{ item.description }}
20+
{% endfor %}
21+
22+
{% if references %}
23+
### 📚 References
24+
{% for ref in references %}
25+
- [{{ ref.title }}]({{ ref.url }})
26+
{% endfor %}
27+
{% endif %}
28+
29+
{{ metadata | safe }}
30+
31+
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
2+
## 🦉 lgtm Review
3+
4+
> **Score:** {{ score }}
5+
6+
### 🔍 Summary
7+
8+
{{ summary }}
9+
10+
{% if comments_section %}
11+
{{ comments_section }}
12+
{% endif %}
13+
14+
{{ metadata | safe }}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{{ score }} {{ score_icon }}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
2+
```{{ language }}
3+
{{ snippet }}
4+
```

0 commit comments

Comments
 (0)