Spaces:
Sleeping
Sleeping
hari-huynh commited on
Commit ·
04417e3
1
Parent(s): 5994e0a
Add code to send mesage to specific thread in Slack
Browse files- env_self.txt +11 -0
- env_smulie.txt +11 -0
- index.py +20 -8
- prompt/code_review.toml +338 -0
- requirements.txt +1 -0
- test_logfire.py +12 -5
- tool.py +80 -110
env_self.txt
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
WORKSPACE_ID=hari-smulie
|
| 2 |
+
BITBUCKET_USERNAME=hari_huynh
|
| 3 |
+
BITBUCKET_APP_PASSWORD=ATBBCxY7PwfsMrFG3CjncNGRM7Wf3F223CF7
|
| 4 |
+
GOOGLE_API_KEY=AIzaSyD_fOfuu7stUwMxCSkUtQvgMpaPbWyt51c
|
| 5 |
+
DEFAULT_BASE_URL=https://api.bitbucket.org/2.0
|
| 6 |
+
REPO_SLUG=pr-code-review
|
| 7 |
+
SLACK_WEBHOOK_URL=https://hooks.slack.com/services/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
|
| 8 |
+
SLACK_BOT_TOKEN=xoxb-2157611119911-9373773680839-9MF0EE59Nzwp3ItE3ZCq1rHP
|
| 9 |
+
SLACK_CHANNEL=pull-request
|
| 10 |
+
LOGFIRE_API_KEY=pylf_v1_us_JPMBdvqjgCH8xFhFj2QTVgyvWB2QBpvKsX2bt4lHQhyS
|
| 11 |
+
TEAM_DEV_SLACK_ID=S0956194QAX
|
env_smulie.txt
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
WORKSPACE_ID=thuantran_smulie
|
| 2 |
+
BITBUCKET_USERNAME=hari_huynh
|
| 3 |
+
BITBUCKET_APP_PASSWORD= ATBB8eRzA8UbH6jrjTtjRDqFDDePF0B131DA
|
| 4 |
+
GOOGLE_API_KEY=AIzaSyD_fOfuu7stUwMxCSkUtQvgMpaPbWyt51c
|
| 5 |
+
DEFAULT_BASE_URL = "https://api.bitbucket.org/2.0"
|
| 6 |
+
REPO_SLUG="pr-code-review"
|
| 7 |
+
SLACK_WEBHOOK_URL = "https://hooks.slack.com/services/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"
|
| 8 |
+
SLACK_BOT_TOKEN = "xoxb-2157611119911-9373773680839-9MF0EE59Nzwp3ItE3ZCq1rHP"
|
| 9 |
+
SLACK_CHANNEL = "pull-request"
|
| 10 |
+
LOGFIRE_API_KEY=pylf_v1_us_JPMBdvqjgCH8xFhFj2QTVgyvWB2QBpvKsX2bt4lHQhyS
|
| 11 |
+
TEAM_DEV_SLACK_ID=S0956194QAX
|
index.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
from flask import Flask, request, jsonify
|
| 2 |
-
from tool import create_session, get_pull_request_overview, get_diff, send_slack_message, save_md_report
|
| 3 |
from dotenv import load_dotenv
|
| 4 |
import os
|
| 5 |
from agent.code_review import code_review_agent, read_text_file, DiffDeps
|
|
@@ -79,7 +79,7 @@ def webhook():
|
|
| 79 |
f"**🔗 Link Pull Request:** {pr_link}\n"
|
| 80 |
)
|
| 81 |
|
| 82 |
-
|
| 83 |
|
| 84 |
# Lấy diff và chạy agent
|
| 85 |
diff = get_diff(session, diff_url)
|
|
@@ -88,25 +88,37 @@ def webhook():
|
|
| 88 |
code_review_text = getattr(code_review_result, "output", None) or str(code_review_result)
|
| 89 |
|
| 90 |
# Gộp thông tin PR + kết quả review vào 1 lần gửi
|
| 91 |
-
|
| 92 |
-
f"{pr_report}\n\n"
|
| 93 |
-
f"---\n\n"
|
| 94 |
f"## 🤖 Code Review Summary\n\n"
|
| 95 |
f"{code_review_text}"
|
| 96 |
)
|
| 97 |
|
| 98 |
-
send_slack_message(
|
| 99 |
-
message_text =
|
| 100 |
webhook_url = SLACK_WEBHOOK_URL,
|
| 101 |
bot_token = SLACK_BOT_TOKEN,
|
| 102 |
channel = "pull-request",
|
| 103 |
blocks = [
|
| 104 |
{
|
| 105 |
"type": "markdown",
|
| 106 |
-
"text":
|
| 107 |
}
|
| 108 |
]
|
| 109 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
print("Sent combined PR + code review")
|
| 111 |
|
| 112 |
return jsonify({"status": "ok", "message": "Pull request processed successfully"}), 200
|
|
|
|
| 1 |
from flask import Flask, request, jsonify
|
| 2 |
+
from tool import create_session, get_pull_request_overview, get_diff, send_slack_message, save_md_report, reply_slack_thread
|
| 3 |
from dotenv import load_dotenv
|
| 4 |
import os
|
| 5 |
from agent.code_review import code_review_agent, read_text_file, DiffDeps
|
|
|
|
| 79 |
f"**🔗 Link Pull Request:** {pr_link}\n"
|
| 80 |
)
|
| 81 |
|
| 82 |
+
print(pr_report)
|
| 83 |
|
| 84 |
# Lấy diff và chạy agent
|
| 85 |
diff = get_diff(session, diff_url)
|
|
|
|
| 88 |
code_review_text = getattr(code_review_result, "output", None) or str(code_review_result)
|
| 89 |
|
| 90 |
# Gộp thông tin PR + kết quả review vào 1 lần gửi
|
| 91 |
+
code_review_message = (
|
|
|
|
|
|
|
| 92 |
f"## 🤖 Code Review Summary\n\n"
|
| 93 |
f"{code_review_text}"
|
| 94 |
)
|
| 95 |
|
| 96 |
+
resp = send_slack_message(
|
| 97 |
+
message_text = pr_report,
|
| 98 |
webhook_url = SLACK_WEBHOOK_URL,
|
| 99 |
bot_token = SLACK_BOT_TOKEN,
|
| 100 |
channel = "pull-request",
|
| 101 |
blocks = [
|
| 102 |
{
|
| 103 |
"type": "markdown",
|
| 104 |
+
"text": pr_report
|
| 105 |
}
|
| 106 |
]
|
| 107 |
)
|
| 108 |
+
|
| 109 |
+
first_message_ts = resp["response"]["ts"] # lấy ts để trả lời sau
|
| 110 |
+
|
| 111 |
+
reply_slack_thread(
|
| 112 |
+
thread_ts=first_message_ts,
|
| 113 |
+
message_text=code_review_message,
|
| 114 |
+
channel="pull-request",
|
| 115 |
+
blocks = [
|
| 116 |
+
{
|
| 117 |
+
"type": "markdown",
|
| 118 |
+
"text": pr_report
|
| 119 |
+
}
|
| 120 |
+
]
|
| 121 |
+
)
|
| 122 |
print("Sent combined PR + code review")
|
| 123 |
|
| 124 |
return jsonify({"status": "ok", "message": "Pull request processed successfully"}), 200
|
prompt/code_review.toml
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[pr_review_prompt]
|
| 2 |
+
system="""You are PR-Reviewer, a language model designed to review a Git Pull Request (PR).
|
| 3 |
+
Your task is to provide constructive and concise feedback for the PR.
|
| 4 |
+
The review should focus on new code added in the PR code diff (lines starting with '+')
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
The format we will use to present the PR code diff:
|
| 8 |
+
======
|
| 9 |
+
## File: 'src/file1.py'
|
| 10 |
+
{%- if is_ai_metadata %}
|
| 11 |
+
### AI-generated changes summary:
|
| 12 |
+
* ...
|
| 13 |
+
* ...
|
| 14 |
+
{%- endif %}
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
@@ ... @@ def func1():
|
| 18 |
+
__new hunk__
|
| 19 |
+
11 unchanged code line0
|
| 20 |
+
12 unchanged code line1
|
| 21 |
+
13 +new code line2 added
|
| 22 |
+
14 unchanged code line3
|
| 23 |
+
__old hunk__
|
| 24 |
+
unchanged code line0
|
| 25 |
+
unchanged code line1
|
| 26 |
+
-old code line2 removed
|
| 27 |
+
unchanged code line3
|
| 28 |
+
|
| 29 |
+
@@ ... @@ def func2():
|
| 30 |
+
__new hunk__
|
| 31 |
+
unchanged code line4
|
| 32 |
+
+new code line5 added
|
| 33 |
+
unchanged code line6
|
| 34 |
+
|
| 35 |
+
## File: 'src/file2.py'
|
| 36 |
+
...
|
| 37 |
+
======
|
| 38 |
+
|
| 39 |
+
- In the format above, the diff is organized into separate '__new hunk__' and '__old hunk__' sections for each code chunk. '__new hunk__' contains the updated code, while '__old hunk__' shows the removed code. If no code was removed in a specific chunk, the __old hunk__ section will be omitted.
|
| 40 |
+
- We also added line numbers for the '__new hunk__' code, to help you refer to the code lines in your suggestions. These line numbers are not part of the actual code, and should only be used for reference.
|
| 41 |
+
- Code lines are prefixed with symbols ('+', '-', ' '). The '+' symbol indicates new code added in the PR, the '-' symbol indicates code removed in the PR, and the ' ' symbol indicates unchanged code. \
|
| 42 |
+
The review should address new code added in the PR code diff (lines starting with '+').
|
| 43 |
+
{%- if is_ai_metadata %}
|
| 44 |
+
- If available, an AI-generated summary will appear and provide a high-level overview of the file changes. Note that this summary may not be fully accurate or complete.
|
| 45 |
+
{%- endif %}
|
| 46 |
+
- When quoting variables, names or file paths from the code, use backticks (`) instead of single quote (').
|
| 47 |
+
- Note that you only see changed code segments (diff hunks in a PR), not the entire codebase. Avoid suggestions that might duplicate existing functionality or questioning code elements (like variables declarations or import statements) that may be defined elsewhere in the codebase.
|
| 48 |
+
- Also note that if the code ends at an opening brace or statement that begins a new scope (like 'if', 'for', 'try'), don't treat it as incomplete. Instead, acknowledge the visible scope boundary and analyze only the code shown.
|
| 49 |
+
|
| 50 |
+
{%- if extra_instructions %}
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
Extra instructions from the user:
|
| 54 |
+
======
|
| 55 |
+
{{ extra_instructions }}
|
| 56 |
+
======
|
| 57 |
+
{% endif %}
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
The output must be a YAML object equivalent to type $PRReview, according to the following Pydantic definitions:
|
| 61 |
+
=====
|
| 62 |
+
{%- if require_can_be_split_review %}
|
| 63 |
+
class SubPR(BaseModel):
|
| 64 |
+
relevant_files: List[str] = Field(description="The relevant files of the sub-PR")
|
| 65 |
+
title: str = Field(description="Short and concise title for an independent and meaningful sub-PR, composed only from the relevant files")
|
| 66 |
+
{%- endif %}
|
| 67 |
+
|
| 68 |
+
class KeyIssuesComponentLink(BaseModel):
|
| 69 |
+
relevant_file: str = Field(description="The full file path of the relevant file")
|
| 70 |
+
issue_header: str = Field(description="One or two word title for the issue. For example: 'Possible Bug', etc.")
|
| 71 |
+
issue_content: str = Field(description="A short and concise summary of what should be further inspected and validated during the PR review process for this issue. Do not mention line numbers in this field.")
|
| 72 |
+
start_line: int = Field(description="The start line that corresponds to this issue in the relevant file")
|
| 73 |
+
end_line: int = Field(description="The end line that corresponds to this issue in the relevant file")
|
| 74 |
+
|
| 75 |
+
{%- if require_todo_scan %}
|
| 76 |
+
class TodoSection(BaseModel):
|
| 77 |
+
relevant_file: str = Field(description="The full path of the file containing the TODO comment")
|
| 78 |
+
line_number: int = Field(description="The line number where the TODO comment starts")
|
| 79 |
+
content: str = Field(description="The content of the TODO comment. Only include actual TODO comments within code comments (e.g., comments starting with '#', '//', '/*', '<!--', ...). Remove leading 'TODO' prefixes. If more than 10 words, summarize the TODO comment to a single short sentence up to 10 words.")
|
| 80 |
+
{%- endif %}
|
| 81 |
+
|
| 82 |
+
{%- if related_tickets %}
|
| 83 |
+
|
| 84 |
+
class TicketCompliance(BaseModel):
|
| 85 |
+
ticket_url: str = Field(description="Ticket URL or ID")
|
| 86 |
+
ticket_requirements: str = Field(description="Repeat, in your own words (in bullet points), all the requirements, sub-tasks, DoD, and acceptance criteria raised by the ticket")
|
| 87 |
+
fully_compliant_requirements: str = Field(description="Bullet-point list of items from the 'ticket_requirements' section above that are fulfilled by the PR code. Don't explain how the requirements are met, just list them shortly. Can be empty")
|
| 88 |
+
not_compliant_requirements: str = Field(description="Bullet-point list of items from the 'ticket_requirements' section above that are not fulfilled by the PR code. Don't explain how the requirements are not met, just list them shortly. Can be empty")
|
| 89 |
+
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")
|
| 90 |
+
{%- endif %}
|
| 91 |
+
|
| 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 |
+
|
| 100 |
+
class Review(BaseModel):
|
| 101 |
+
{%- if related_tickets %}
|
| 102 |
+
ticket_compliance_check: List[TicketCompliance] = Field(description="A list of compliance checks for the related tickets")
|
| 103 |
+
{%- endif %}
|
| 104 |
+
{%- if require_estimate_effort_to_review %}
|
| 105 |
+
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.")
|
| 106 |
+
{%- 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 %}
|
| 110 |
+
{%- if require_score %}
|
| 111 |
+
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.")
|
| 112 |
+
{%- endif %}
|
| 113 |
+
{%- if require_tests %}
|
| 114 |
+
relevant_tests: str = Field(description="yes/no question: does this PR have relevant tests added or updated ?")
|
| 115 |
+
{%- endif %}
|
| 116 |
+
{%- if question_str %}
|
| 117 |
+
insights_from_user_answers: str = Field(description="shortly summarize the insights you gained from the user's answers to the questions")
|
| 118 |
+
{%- endif %}
|
| 119 |
+
key_issues_to_review: List[KeyIssuesComponentLink] = Field("A short and diverse list (0-{{ num_max_findings }} issues) of high-priority bugs, problems or performance concerns introduced in the PR code, which the PR reviewer should further focus on and validate during the review process.")
|
| 120 |
+
{%- if require_security_review %}
|
| 121 |
+
security_concerns: str = Field(description="Does this PR code introduce vulnerabilities such as exposure of sensitive information (e.g., API keys, secrets, passwords), or security concerns like SQL injection, XSS, CSRF, and others ? Answer 'No' (without explaining why) if there are no possible issues. If there are security concerns or issues, start your answer with a short header, such as: 'Sensitive information exposure: ...', 'SQL injection: ...', etc. Explain your answer. Be specific and give examples if possible")
|
| 122 |
+
{%- endif %}
|
| 123 |
+
{%- if require_todo_scan %}
|
| 124 |
+
todo_sections: Union[List[TodoSection], str] = Field(description="A list of TODO comments found in the PR code. Return 'No' (as a string) if there are no TODO comments in the PR")
|
| 125 |
+
{%- endif %}
|
| 126 |
+
{%- if require_can_be_split_review %}
|
| 127 |
+
can_be_split: List[SubPR] = Field(min_items=0, max_items=3, description="Can this PR, which contains {{ num_pr_files }} changed files in total, be divided into smaller sub-PRs with distinct tasks that can be reviewed and merged independently, regardless of the order ? Make sure that the sub-PRs are indeed independent, with no code dependencies between them, and that each sub-PR represent a meaningful independent task. Output an empty list if the PR code does not need to be split.")
|
| 128 |
+
{%- endif %}
|
| 129 |
+
|
| 130 |
+
class PRReview(BaseModel):
|
| 131 |
+
review: Review
|
| 132 |
+
=====
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
Example output:
|
| 136 |
+
```yaml
|
| 137 |
+
review:
|
| 138 |
+
{%- if related_tickets %}
|
| 139 |
+
ticket_compliance_check:
|
| 140 |
+
- ticket_url: |
|
| 141 |
+
...
|
| 142 |
+
ticket_requirements: |
|
| 143 |
+
...
|
| 144 |
+
fully_compliant_requirements: |
|
| 145 |
+
...
|
| 146 |
+
not_compliant_requirements: |
|
| 147 |
+
...
|
| 148 |
+
overall_compliance_level: |
|
| 149 |
+
...
|
| 150 |
+
{%- endif %}
|
| 151 |
+
{%- if require_estimate_effort_to_review %}
|
| 152 |
+
estimated_effort_to_review_[1-5]: |
|
| 153 |
+
3
|
| 154 |
+
{%- endif %}
|
| 155 |
+
{%- if require_score %}
|
| 156 |
+
score: 89
|
| 157 |
+
{%- endif %}
|
| 158 |
+
relevant_tests: |
|
| 159 |
+
No
|
| 160 |
+
key_issues_to_review:
|
| 161 |
+
- relevant_file: |
|
| 162 |
+
directory/xxx.py
|
| 163 |
+
issue_header: |
|
| 164 |
+
Possible Bug
|
| 165 |
+
issue_content: |
|
| 166 |
+
...
|
| 167 |
+
start_line: 12
|
| 168 |
+
end_line: 14
|
| 169 |
+
- ...
|
| 170 |
+
security_concerns: |
|
| 171 |
+
No
|
| 172 |
+
{%- if require_todo_scan %}
|
| 173 |
+
todo_sections: |
|
| 174 |
+
No
|
| 175 |
+
{%- endif %}
|
| 176 |
+
{%- if require_can_be_split_review %}
|
| 177 |
+
can_be_split:
|
| 178 |
+
- relevant_files:
|
| 179 |
+
- ...
|
| 180 |
+
- ...
|
| 181 |
+
title: ...
|
| 182 |
+
- ...
|
| 183 |
+
{%- 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 %}
|
| 193 |
+
```
|
| 194 |
+
|
| 195 |
+
Answer should be a valid YAML, and nothing else. Each YAML output MUST be after a newline, with proper indent, and block scalar indicator ('|')
|
| 196 |
+
"""
|
| 197 |
+
|
| 198 |
+
user="""
|
| 199 |
+
{%- if related_tickets %}
|
| 200 |
+
--PR Ticket Info--
|
| 201 |
+
{%- for ticket in related_tickets %}
|
| 202 |
+
=====
|
| 203 |
+
Ticket URL: '{{ ticket.ticket_url }}'
|
| 204 |
+
|
| 205 |
+
Ticket Title: '{{ ticket.title }}'
|
| 206 |
+
|
| 207 |
+
{%- if ticket.labels %}
|
| 208 |
+
|
| 209 |
+
Ticket Labels: {{ ticket.labels }}
|
| 210 |
+
|
| 211 |
+
{%- endif %}
|
| 212 |
+
{%- if ticket.body %}
|
| 213 |
+
|
| 214 |
+
Ticket Description:
|
| 215 |
+
#####
|
| 216 |
+
{{ ticket.body }}
|
| 217 |
+
#####
|
| 218 |
+
{%- endif %}
|
| 219 |
+
|
| 220 |
+
{%- if ticket.requirements %}
|
| 221 |
+
Ticket Requirements:
|
| 222 |
+
#####
|
| 223 |
+
{{ ticket.requirements }}
|
| 224 |
+
#####
|
| 225 |
+
{%- endif %}
|
| 226 |
+
=====
|
| 227 |
+
{% endfor %}
|
| 228 |
+
{%- endif %}
|
| 229 |
+
|
| 230 |
+
|
| 231 |
+
--PR Info--
|
| 232 |
+
{%- if date %}
|
| 233 |
+
|
| 234 |
+
Today's Date: {{date}}
|
| 235 |
+
{%- endif %}
|
| 236 |
+
|
| 237 |
+
Title: '{{title}}'
|
| 238 |
+
|
| 239 |
+
Branch: '{{branch}}'
|
| 240 |
+
|
| 241 |
+
{%- if description %}
|
| 242 |
+
|
| 243 |
+
PR Description:
|
| 244 |
+
======
|
| 245 |
+
{{ description|trim }}
|
| 246 |
+
======
|
| 247 |
+
{%- endif %}
|
| 248 |
+
|
| 249 |
+
{%- if question_str %}
|
| 250 |
+
|
| 251 |
+
=====
|
| 252 |
+
Here are questions to better understand the PR. Use the answers to provide better feedback.
|
| 253 |
+
|
| 254 |
+
{{ question_str|trim }}
|
| 255 |
+
|
| 256 |
+
User answers:
|
| 257 |
+
'
|
| 258 |
+
{{ answer_str|trim }}
|
| 259 |
+
'
|
| 260 |
+
=====
|
| 261 |
+
{%- endif %}
|
| 262 |
+
|
| 263 |
+
|
| 264 |
+
The PR code diff:
|
| 265 |
+
======
|
| 266 |
+
{{ diff|trim }}
|
| 267 |
+
======
|
| 268 |
+
|
| 269 |
+
|
| 270 |
+
{%- if duplicate_prompt_examples %}
|
| 271 |
+
|
| 272 |
+
|
| 273 |
+
Example output:
|
| 274 |
+
```yaml
|
| 275 |
+
review:
|
| 276 |
+
{%- if related_tickets %}
|
| 277 |
+
ticket_compliance_check:
|
| 278 |
+
- ticket_url: |
|
| 279 |
+
...
|
| 280 |
+
ticket_requirements: |
|
| 281 |
+
...
|
| 282 |
+
fully_compliant_requirements: |
|
| 283 |
+
...
|
| 284 |
+
not_compliant_requirements: |
|
| 285 |
+
...
|
| 286 |
+
overall_compliance_level: |
|
| 287 |
+
...
|
| 288 |
+
{%- endif %}
|
| 289 |
+
{%- if require_estimate_effort_to_review %}
|
| 290 |
+
estimated_effort_to_review_[1-5]: |
|
| 291 |
+
3
|
| 292 |
+
{%- endif %}
|
| 293 |
+
{%- if require_score %}
|
| 294 |
+
score: 89
|
| 295 |
+
{%- endif %}
|
| 296 |
+
relevant_tests: |
|
| 297 |
+
No
|
| 298 |
+
key_issues_to_review:
|
| 299 |
+
- relevant_file: |
|
| 300 |
+
...
|
| 301 |
+
issue_header: |
|
| 302 |
+
...
|
| 303 |
+
issue_content: |
|
| 304 |
+
...
|
| 305 |
+
start_line: ...
|
| 306 |
+
end_line: ...
|
| 307 |
+
- ...
|
| 308 |
+
security_concerns: |
|
| 309 |
+
No
|
| 310 |
+
{%- if require_todo_scan %}
|
| 311 |
+
todo_sections: |
|
| 312 |
+
No
|
| 313 |
+
{%- endif %}
|
| 314 |
+
{%- if require_can_be_split_review %}
|
| 315 |
+
can_be_split:
|
| 316 |
+
- relevant_files:
|
| 317 |
+
- ...
|
| 318 |
+
- ...
|
| 319 |
+
title: ...
|
| 320 |
+
- ...
|
| 321 |
+
{%- 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 %}
|
| 331 |
+
```
|
| 332 |
+
(replace '...' with the actual values)
|
| 333 |
+
{%- endif %}
|
| 334 |
+
|
| 335 |
+
|
| 336 |
+
Response (should be a valid YAML, and nothing else):
|
| 337 |
+
```yaml
|
| 338 |
+
"""
|
requirements.txt
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
python-dotenv
|
| 2 |
requests
|
| 3 |
pydantic-ai
|
|
|
|
| 4 |
flask
|
| 5 |
logfire
|
|
|
|
| 1 |
python-dotenv
|
| 2 |
requests
|
| 3 |
pydantic-ai
|
| 4 |
+
pydantic-ai[logfire]
|
| 5 |
flask
|
| 6 |
logfire
|
test_logfire.py
CHANGED
|
@@ -60,7 +60,7 @@ load_dotenv()
|
|
| 60 |
# # Test async tracing
|
| 61 |
# asyncio.run(test_logfire_tracing())
|
| 62 |
|
| 63 |
-
from tool import send_slack_message, list_slack_users, get_slack_usergroups
|
| 64 |
from dotenv import load_dotenv
|
| 65 |
|
| 66 |
load_dotenv()
|
|
@@ -105,12 +105,10 @@ users = list_slack_users()
|
|
| 105 |
groups = get_slack_usergroups(SLACK_BOT_TOKEN)
|
| 106 |
|
| 107 |
# print(users)
|
| 108 |
-
print(groups)
|
| 109 |
|
| 110 |
send_slack_message(
|
| 111 |
message_text = "",
|
| 112 |
-
webhook_url = SLACK_WEBHOOK_URL,
|
| 113 |
-
bot_token = SLACK_BOT_TOKEN,
|
| 114 |
channel = "pull-request",
|
| 115 |
blocks = [
|
| 116 |
{
|
|
@@ -118,4 +116,13 @@ send_slack_message(
|
|
| 118 |
"text": "<@U099W8UMBAQ>"
|
| 119 |
}
|
| 120 |
]
|
| 121 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
# # Test async tracing
|
| 61 |
# asyncio.run(test_logfire_tracing())
|
| 62 |
|
| 63 |
+
from tool import send_slack_message, list_slack_users, get_slack_usergroups, reply_slack_thread
|
| 64 |
from dotenv import load_dotenv
|
| 65 |
|
| 66 |
load_dotenv()
|
|
|
|
| 105 |
groups = get_slack_usergroups(SLACK_BOT_TOKEN)
|
| 106 |
|
| 107 |
# print(users)
|
| 108 |
+
# print(groups)
|
| 109 |
|
| 110 |
send_slack_message(
|
| 111 |
message_text = "",
|
|
|
|
|
|
|
| 112 |
channel = "pull-request",
|
| 113 |
blocks = [
|
| 114 |
{
|
|
|
|
| 116 |
"text": "<@U099W8UMBAQ>"
|
| 117 |
}
|
| 118 |
]
|
| 119 |
+
)
|
| 120 |
+
|
| 121 |
+
resp = send_slack_message("Xin chào team! 🚀", channel="pull-request")
|
| 122 |
+
first_message_ts = resp["response"]["ts"] # lấy ts để trả lời sau
|
| 123 |
+
|
| 124 |
+
reply_slack_thread(
|
| 125 |
+
thread_ts=first_message_ts,
|
| 126 |
+
message_text="Đây là câu trả lời trong thread 🌱",
|
| 127 |
+
channel="pull-request",
|
| 128 |
+
)
|
tool.py
CHANGED
|
@@ -105,129 +105,99 @@ def save_md_report(filename, content):
|
|
| 105 |
print(f"Saved {filename} file")
|
| 106 |
|
| 107 |
|
| 108 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
message_text: str,
|
| 110 |
*,
|
| 111 |
-
webhook_url: Optional[str] = None,
|
| 112 |
-
bot_token: Optional[str] = None,
|
| 113 |
channel: Optional[str] = None,
|
| 114 |
blocks: Optional[List[Dict[str, Any]]] = None,
|
| 115 |
thread_ts: Optional[str] = None,
|
| 116 |
) -> Dict[str, Any]:
|
| 117 |
-
"""
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
2) Slack Web API (chat.postMessage) với SLACK_BOT_TOKEN + channel
|
| 121 |
-
|
| 122 |
-
Trả về dict kết quả đã chuẩn hoá, bao gồm khoá 'ok' và 'method'.
|
| 123 |
-
Ném ValueError khi thiếu thông tin cấu hình cần thiết.
|
| 124 |
-
"""
|
| 125 |
-
resolved_webhook_url = webhook_url or SLACK_WEBHOOK_URL
|
| 126 |
-
resolved_bot_token = bot_token or SLACK_BOT_TOKEN
|
| 127 |
-
resolved_channel = channel or SLACK_CHANNEL
|
| 128 |
|
| 129 |
-
|
| 130 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
if blocks is not None:
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
headers = {
|
| 153 |
-
"Authorization": f"Bearer {resolved_bot_token}",
|
| 154 |
-
"Content-Type": "application/json",
|
| 155 |
-
}
|
| 156 |
-
payload_api: Dict[str, Any] = {
|
| 157 |
-
"channel": resolved_channel,
|
| 158 |
-
"text": message_text,
|
| 159 |
-
}
|
| 160 |
-
if normalized_blocks is not None:
|
| 161 |
-
payload_api["blocks"] = normalized_blocks
|
| 162 |
-
if thread_ts is not None:
|
| 163 |
-
payload_api["thread_ts"] = thread_ts
|
| 164 |
-
|
| 165 |
-
api_resp = requests.post(api_url, headers=headers, json=payload_api, timeout=15)
|
| 166 |
-
try:
|
| 167 |
-
api_resp.raise_for_status()
|
| 168 |
-
except Exception as exc:
|
| 169 |
-
raise ValueError(
|
| 170 |
-
f"Slack webhook failed (status {response.status_code}, body={response.text!r}); "
|
| 171 |
-
f"fallback Web API also failed: {exc}"
|
| 172 |
-
) from exc
|
| 173 |
-
|
| 174 |
-
data = api_resp.json()
|
| 175 |
-
if not data.get("ok", False):
|
| 176 |
-
error_detail = data.get("error", "unknown_error")
|
| 177 |
-
raise ValueError(
|
| 178 |
-
f"Slack webhook failed (status {response.status_code}, body={response.text!r}); "
|
| 179 |
-
f"fallback Web API responded with error: {error_detail}"
|
| 180 |
-
)
|
| 181 |
-
|
| 182 |
-
return {
|
| 183 |
-
"ok": True,
|
| 184 |
-
"method": "webhook_fallback_web_api",
|
| 185 |
-
"status_code": api_resp.status_code,
|
| 186 |
-
"response": data,
|
| 187 |
-
}
|
| 188 |
-
|
| 189 |
-
# Không có fallback khả dụng, báo lỗi chi tiết
|
| 190 |
-
raise ValueError(
|
| 191 |
-
f"Slack webhook request failed with status {response.status_code}: {response.text}"
|
| 192 |
-
)
|
| 193 |
|
| 194 |
-
# Fallback: dùng Slack Web API nếu có token + channel
|
| 195 |
-
if resolved_bot_token and resolved_channel:
|
| 196 |
-
api_url = "https://slack.com/api/chat.postMessage"
|
| 197 |
-
headers = {
|
| 198 |
-
"Authorization": f"Bearer {resolved_bot_token}",
|
| 199 |
-
"Content-Type": "application/json",
|
| 200 |
-
}
|
| 201 |
-
payload: Dict[str, Any] = {
|
| 202 |
-
"channel": resolved_channel,
|
| 203 |
-
"text": message_text,
|
| 204 |
-
}
|
| 205 |
-
if normalized_blocks is not None:
|
| 206 |
-
payload["blocks"] = normalized_blocks
|
| 207 |
-
if thread_ts is not None:
|
| 208 |
-
payload["thread_ts"] = thread_ts
|
| 209 |
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 215 |
|
| 216 |
-
data = response.json()
|
| 217 |
-
if not data.get("ok", False):
|
| 218 |
-
error_detail = data.get("error", "unknown_error")
|
| 219 |
-
raise ValueError(f"Slack API responded with error: {error_detail}")
|
| 220 |
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 227 |
|
| 228 |
-
raise ValueError(
|
| 229 |
-
"Missing Slack configuration. Provide SLACK_WEBHOOK_URL or SLACK_BOT_TOKEN and SLACK_CHANNEL."
|
| 230 |
-
)
|
| 231 |
|
| 232 |
def list_slack_users(*, bot_token: Optional[str] = None, include_bots: bool = True) -> List[Dict[str, Any]]:
|
| 233 |
"""
|
|
|
|
| 105 |
print(f"Saved {filename} file")
|
| 106 |
|
| 107 |
|
| 108 |
+
def _send_via_webhook(message_text: str, blocks: Optional[List[Dict[str, Any]]] = None) -> Dict[str, Any]:
|
| 109 |
+
"""Gửi tin nhắn qua Incoming Webhook."""
|
| 110 |
+
if not SLACK_WEBHOOK_URL:
|
| 111 |
+
raise ValueError("Missing SLACK_WEBHOOK_URL for webhook message")
|
| 112 |
+
|
| 113 |
+
payload: Dict[str, Any] = {"text": message_text}
|
| 114 |
+
if blocks is not None:
|
| 115 |
+
payload["blocks"] = blocks
|
| 116 |
+
|
| 117 |
+
response = requests.post(SLACK_WEBHOOK_URL, json=payload, timeout=15)
|
| 118 |
+
if 200 <= response.status_code < 300 and response.text.strip().lower() in ("ok", ""):
|
| 119 |
+
return {
|
| 120 |
+
"ok": True,
|
| 121 |
+
"method": "webhook",
|
| 122 |
+
"status_code": response.status_code,
|
| 123 |
+
"response": response.text,
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
raise ValueError(f"Slack webhook failed (status {response.status_code}): {response.text}")
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def _send_via_api(
|
| 130 |
message_text: str,
|
| 131 |
*,
|
|
|
|
|
|
|
| 132 |
channel: Optional[str] = None,
|
| 133 |
blocks: Optional[List[Dict[str, Any]]] = None,
|
| 134 |
thread_ts: Optional[str] = None,
|
| 135 |
) -> Dict[str, Any]:
|
| 136 |
+
"""Gửi tin nhắn qua Slack Web API (chat.postMessage)."""
|
| 137 |
+
if not SLACK_BOT_TOKEN or not (channel or SLACK_CHANNEL):
|
| 138 |
+
raise ValueError("Missing SLACK_BOT_TOKEN or channel for Web API message")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
|
| 140 |
+
api_url = "https://slack.com/api/chat.postMessage"
|
| 141 |
+
headers = {
|
| 142 |
+
"Authorization": f"Bearer {SLACK_BOT_TOKEN}",
|
| 143 |
+
"Content-Type": "application/json",
|
| 144 |
+
}
|
| 145 |
+
payload: Dict[str, Any] = {
|
| 146 |
+
"channel": channel or SLACK_CHANNEL,
|
| 147 |
+
"text": message_text,
|
| 148 |
+
}
|
| 149 |
if blocks is not None:
|
| 150 |
+
payload["blocks"] = blocks
|
| 151 |
+
if thread_ts is not None:
|
| 152 |
+
payload["thread_ts"] = thread_ts
|
| 153 |
+
|
| 154 |
+
response = requests.post(api_url, headers=headers, json=payload, timeout=15)
|
| 155 |
+
try:
|
| 156 |
+
response.raise_for_status()
|
| 157 |
+
except Exception as exc:
|
| 158 |
+
raise ValueError(f"Slack API request failed: {exc}") from exc
|
| 159 |
+
|
| 160 |
+
data = response.json()
|
| 161 |
+
if not data.get("ok", False):
|
| 162 |
+
raise ValueError(f"Slack API responded with error: {data.get('error', 'unknown_error')}")
|
| 163 |
+
|
| 164 |
+
return {
|
| 165 |
+
"ok": True,
|
| 166 |
+
"method": "web_api",
|
| 167 |
+
"status_code": response.status_code,
|
| 168 |
+
"response": data,
|
| 169 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 170 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
|
| 172 |
+
def send_slack_message(
|
| 173 |
+
message_text: str,
|
| 174 |
+
*,
|
| 175 |
+
channel: Optional[str] = None,
|
| 176 |
+
blocks: Optional[List[Dict[str, Any]]] = None,
|
| 177 |
+
use_webhook: bool = False,
|
| 178 |
+
) -> Dict[str, Any]:
|
| 179 |
+
"""
|
| 180 |
+
Gửi tin nhắn mới vào Slack (không dùng thread).
|
| 181 |
+
Có thể chọn gửi qua Webhook hoặc Web API.
|
| 182 |
+
"""
|
| 183 |
+
if use_webhook:
|
| 184 |
+
return _send_via_webhook(message_text, blocks)
|
| 185 |
+
return _send_via_api(message_text, channel=channel, blocks=blocks)
|
| 186 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 187 |
|
| 188 |
+
def reply_slack_thread(
|
| 189 |
+
thread_ts: str,
|
| 190 |
+
message_text: str,
|
| 191 |
+
*,
|
| 192 |
+
channel: Optional[str] = None,
|
| 193 |
+
blocks: Optional[List[Dict[str, Any]]] = None,
|
| 194 |
+
) -> Dict[str, Any]:
|
| 195 |
+
"""
|
| 196 |
+
Trả lời một tin nhắn trong thread (yêu cầu thread_ts).
|
| 197 |
+
Bắt buộc dùng Web API (webhook không hỗ trợ thread).
|
| 198 |
+
"""
|
| 199 |
+
return _send_via_api(message_text, channel=channel, blocks=blocks, thread_ts=thread_ts)
|
| 200 |
|
|
|
|
|
|
|
|
|
|
| 201 |
|
| 202 |
def list_slack_users(*, bot_token: Optional[str] = None, include_bots: bool = True) -> List[Dict[str, Any]]:
|
| 203 |
"""
|