Skip to content

Commit 5fc466b

Browse files
PR reviewer tool: add an opt-in work time estimation (qodo-ai#2006)
* feat: add `ContributionTimeCostEstimate` * docs: mentiond `require_estimate_contribution_time_cost` for `reviewer` * feat: implement time cost estimate for `reviewer` * test: non-GFM output To ensure parity and prevent regressions in plain Markdown rendering.
1 parent d2c304e commit 5fc466b

File tree

6 files changed

+79
-1
lines changed

6 files changed

+79
-1
lines changed

docs/docs/tools/review.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,10 @@ extra_instructions = "..."
9191
<td><b>require_estimate_effort_to_review</b></td>
9292
<td>If set to true, the tool will add a section that estimates the effort needed to review the PR. Default is true.</td>
9393
</tr>
94+
<tr>
95+
<td><b>require_estimate_contribution_time_cost</b></td>
96+
<td>If set to true, the tool will add a section that estimates the time required for a senior developer to create and submit such changes. Default is false.</td>
97+
</tr>
9498
<tr>
9599
<td><b>require_can_be_split_review</b></td>
96100
<td>If set to true, the tool will add a section that checks if the PR contains several themes, and can be split into smaller PRs. Default is false.</td>

pr_agent/algo/utils.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ def convert_to_markdown_v2(output_data: dict,
148148
"Insights from user's answers": "📝",
149149
"Code feedback": "🤖",
150150
"Estimated effort to review [1-5]": "⏱️",
151+
"Contribution time cost estimate": "⏳",
151152
"Ticket compliance check": "🎫",
152153
}
153154
markdown_text = ""
@@ -207,6 +208,14 @@ def convert_to_markdown_v2(output_data: dict,
207208
markdown_text += f"### {emoji} PR contains tests\n\n"
208209
elif 'ticket compliance check' in key_nice.lower():
209210
markdown_text = ticket_markdown_logic(emoji, markdown_text, value, gfm_supported)
211+
elif 'contribution time cost estimate' in key_nice.lower():
212+
if gfm_supported:
213+
markdown_text += f"<tr><td>{emoji}&nbsp;<strong>Contribution time estimate</strong> (best, average, worst case): "
214+
markdown_text += f"{value['best_case'].replace('m', ' minutes')} | {value['average_case'].replace('m', ' minutes')} | {value['worst_case'].replace('m', ' minutes')}"
215+
markdown_text += f"</td></tr>\n"
216+
else:
217+
markdown_text += f"### {emoji} Contribution time estimate (best, average, worst case): "
218+
markdown_text += f"{value['best_case'].replace('m', ' minutes')} | {value['average_case'].replace('m', ' minutes')} | {value['worst_case'].replace('m', ' minutes')}\n\n"
210219
elif 'security concerns' in key_nice.lower():
211220
if gfm_supported:
212221
markdown_text += f"<tr><td>"
@@ -1465,4 +1474,4 @@ def format_todo_items(value: list[TodoItem] | TodoItem, git_provider, gfm_suppor
14651474
markdown_text += f"- {format_todo_item(todo_item, git_provider, gfm_supported)}\n"
14661475
else:
14671476
markdown_text += f"- {format_todo_item(value, git_provider, gfm_supported)}\n"
1468-
return markdown_text
1477+
return markdown_text

pr_agent/settings/configuration.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ require_tests_review=true
7979
require_estimate_effort_to_review=true
8080
require_can_be_split_review=false
8181
require_security_review=true
82+
require_estimate_contribution_time_cost=false
8283
require_todo_scan=false
8384
require_ticket_analysis_review=true
8485
# general options

pr_agent/settings/pr_reviewer_prompts.toml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,13 +89,24 @@ class TicketCompliance(BaseModel):
8989
requires_further_human_verification: str = Field(description="Bullet-point list of items from the 'ticket_requirements' section above that cannot be assessed through code review alone, are unclear, or need further human review (e.g., browser testing, UI checks). Leave empty if all 'ticket_requirements' were marked as fully compliant or not compliant")
9090
{%- endif %}
9191
92+
{%- if require_estimate_contribution_time_cost %}
93+
94+
class ContributionTimeCostEstimate(BaseModel):
95+
best_case: str = Field(description="An expert in the relevant technology stack, with no unforeseen issues or bugs during the work.", examples=["45m", "5h", "30h"])
96+
average_case: str = Field(description="A senior developer with only brief familiarity with this specific technology stack, and no major unforeseen issues.", examples=["45m", "5h", "30h"])
97+
worst_case: str = Field(description="A senior developer with no prior experience in this specific technology stack, requiring significant time for research, debugging, or resolving unexpected errors.", examples=["45m", "5h", "30h"])
98+
{%- endif %}
99+
92100
class Review(BaseModel):
93101
{%- if related_tickets %}
94102
ticket_compliance_check: List[TicketCompliance] = Field(description="A list of compliance checks for the related tickets")
95103
{%- endif %}
96104
{%- if require_estimate_effort_to_review %}
97105
estimated_effort_to_review_[1-5]: int = Field(description="Estimate, on a scale of 1-5 (inclusive), the time and effort required to review this PR by an experienced and knowledgeable developer. 1 means short and easy review , 5 means long and hard review. Take into account the size, complexity, quality, and the needed changes of the PR code diff.")
98106
{%- endif %}
107+
{%- if require_estimate_contribution_time_cost %}
108+
contribution_time_cost_estimate: ContributionTimeCostEstimate = Field(description="An estimate of the time required to implement the changes, based on the quantity, quality, and complexity of the contribution, as well as the context from the PR description and commit messages.")
109+
{%- endif %}
99110
{%- if require_score %}
100111
score: str = Field(description="Rate this PR on a scale of 0-100 (inclusive), where 0 means the worst possible PR code, and 100 means PR code of the highest quality, without any bugs or performance issues, that is ready to be merged immediately and run in production at scale.")
101112
{%- endif %}
@@ -170,6 +181,15 @@ review:
170181
title: ...
171182
- ...
172183
{%- endif %}
184+
{%- if require_estimate_contribution_time_cost %}
185+
contribution_time_cost_estimate:
186+
best_case: |
187+
...
188+
average_case: |
189+
...
190+
worst_case: |
191+
...
192+
{%- endif %}
173193
```
174194
175195
Answer should be a valid YAML, and nothing else. Each YAML output MUST be after a newline, with proper indent, and block scalar indicator ('|')
@@ -299,6 +319,15 @@ review:
299319
title: ...
300320
- ...
301321
{%- endif %}
322+
{%- if require_estimate_contribution_time_cost %}
323+
contribution_time_cost_estimate:
324+
best_case: |
325+
...
326+
average_case: |
327+
...
328+
worst_case: |
329+
...
330+
{%- endif %}
302331
```
303332
(replace '...' with the actual values)
304333
{%- endif %}

pr_agent/tools/pr_reviewer.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ def __init__(self, pr_url: str, is_answer: bool = False, is_auto: bool = False,
8585
"require_score": get_settings().pr_reviewer.require_score_review,
8686
"require_tests": get_settings().pr_reviewer.require_tests_review,
8787
"require_estimate_effort_to_review": get_settings().pr_reviewer.require_estimate_effort_to_review,
88+
"require_estimate_contribution_time_cost": get_settings().pr_reviewer.require_estimate_contribution_time_cost,
8889
'require_can_be_split_review': get_settings().pr_reviewer.require_can_be_split_review,
8990
'require_security_review': get_settings().pr_reviewer.require_security_review,
9091
'require_todo_scan': get_settings().pr_reviewer.get("require_todo_scan", False),

tests/unittest/test_convert_to_markdown.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,40 @@ def test_can_be_split(self):
222222

223223
assert convert_to_markdown_v2(input_data).strip() == expected_output.strip()
224224

225+
def test_contribution_time_cost_estimate(self):
226+
input_data = {
227+
'review': {
228+
'contribution_time_cost_estimate': {
229+
'best_case': '1h',
230+
'average_case': '2h',
231+
'worst_case': '30m',
232+
}
233+
}
234+
}
235+
236+
expected_output = textwrap.dedent(f"""
237+
{PRReviewHeader.REGULAR.value} 🔍
238+
239+
Here are some key observations to aid the review process:
240+
241+
<table>
242+
<tr><td>⏳&nbsp;<strong>Contribution time estimate</strong> (best, average, worst case): 1h | 2h | 30 minutes</td></tr>
243+
</table>
244+
""")
245+
assert convert_to_markdown_v2(input_data).strip() == expected_output.strip()
246+
247+
# Non-GFM branch
248+
expected_output_no_gfm = textwrap.dedent(f"""
249+
{PRReviewHeader.REGULAR.value} 🔍
250+
251+
Here are some key observations to aid the review process:
252+
253+
### ⏳ Contribution time estimate (best, average, worst case): 1h | 2h | 30 minutes
254+
255+
""")
256+
assert convert_to_markdown_v2(input_data, gfm_supported=False).strip() == expected_output_no_gfm.strip()
257+
258+
225259
# Tests that the function works correctly with an empty dictionary input
226260
def test_empty_dictionary_input(self):
227261
input_data = {}

0 commit comments

Comments
 (0)