diff --git a/biz/llm/client/base.py b/biz/llm/client/base.py index 69ea86910..bcfc2acf4 100644 --- a/biz/llm/client/base.py +++ b/biz/llm/client/base.py @@ -13,8 +13,9 @@ def ping(self) -> bool: try: result = self.completions(messages=[{"role": "user", "content": '请仅返回 "ok"。'}]) return result and result.strip() == "ok" - except Exception: - logger.error("尝试连接LLM失败, {e}") + + except Exception as e: + logger.error(f'尝试连接LLM失败, {e}') return False @abstractmethod diff --git a/biz/queue/worker.py b/biz/queue/worker.py index a58cdef34..12f774510 100644 --- a/biz/queue/worker.py +++ b/biz/queue/worker.py @@ -27,6 +27,7 @@ def handle_push_event(webhook_data: dict, gitlab_token: str, gitlab_url: str, gi score = 0 additions = 0 deletions = 0 + project_name = webhook_data['project']['name'] if push_review_enabled: # 获取PUSH的changes changes = handler.get_push_changes() @@ -38,7 +39,7 @@ def handle_push_event(webhook_data: dict, gitlab_token: str, gitlab_url: str, gi if len(changes) > 0: commits_text = ';'.join(commit.get('message', '').strip() for commit in commits) - review_result = CodeReviewer().review_and_strip_code(str(changes), commits_text) + review_result = CodeReviewer().review_and_strip_code(str(changes), commits_text, project_name) score = CodeReviewer.parse_review_score(review_text=review_result) for item in changes: additions += item['additions'] @@ -46,19 +47,21 @@ def handle_push_event(webhook_data: dict, gitlab_token: str, gitlab_url: str, gi # 将review结果提交到Gitlab的 notes handler.add_push_notes(f'Auto Review Result: \n{review_result}') - event_manager['push_reviewed'].send(PushReviewEntity( - project_name=webhook_data['project']['name'], - author=webhook_data['user_username'], - branch=webhook_data.get('ref', '').replace('refs/heads/', ''), - updated_at=int(datetime.now().timestamp()), # 当前时间 - commits=commits, - score=score, - review_result=review_result, - url_slug=gitlab_url_slug, - webhook_data=webhook_data, - additions=additions, - deletions=deletions, - )) + event_manager['push_reviewed'].send( + PushReviewEntity( + project_name=project_name, + author=webhook_data['user_username'], + branch=webhook_data.get('ref', '').replace('refs/heads/', ''), + updated_at=int(datetime.now().timestamp()), # 当前时间 + commits=commits, + score=score, + review_result=review_result, + url_slug=gitlab_url_slug, + webhook_data=webhook_data, + additions=additions, + deletions=deletions, + ) + ) except Exception as e: error_message = f'服务出现未知错误: {str(e)}\n{traceback.format_exc()}' @@ -133,7 +136,7 @@ def handle_merge_request_event(webhook_data: dict, gitlab_token: str, gitlab_url # review 代码 commits_text = ';'.join(commit['title'] for commit in commits) - review_result = CodeReviewer().review_and_strip_code(str(changes), commits_text) + review_result = CodeReviewer().review_and_strip_code(str(changes), commits_text, project_name) # 将review结果提交到Gitlab的 notes handler.add_merge_request_notes(f'Auto Review Result: \n{review_result}') @@ -177,6 +180,10 @@ def handle_github_push_event(webhook_data: dict, github_token: str, github_url: score = 0 additions = 0 deletions = 0 + + # 获取项目名称 + project_name = webhook_data['repository']['name'] + if push_review_enabled: # 获取PUSH的changes changes = handler.get_push_changes() @@ -188,7 +195,7 @@ def handle_github_push_event(webhook_data: dict, github_token: str, github_url: if len(changes) > 0: commits_text = ';'.join(commit.get('message', '').strip() for commit in commits) - review_result = CodeReviewer().review_and_strip_code(str(changes), commits_text) + review_result = CodeReviewer().review_and_strip_code(str(changes), commits_text, project_name) score = CodeReviewer.parse_review_score(review_text=review_result) for item in changes: additions += item.get('additions', 0) @@ -196,19 +203,21 @@ def handle_github_push_event(webhook_data: dict, github_token: str, github_url: # 将review结果提交到GitHub的 notes handler.add_push_notes(f'Auto Review Result: \n{review_result}') - event_manager['push_reviewed'].send(PushReviewEntity( - project_name=webhook_data['repository']['name'], - author=webhook_data['sender']['login'], - branch=webhook_data['ref'].replace('refs/heads/', ''), - updated_at=int(datetime.now().timestamp()), # 当前时间 - commits=commits, - score=score, - review_result=review_result, - url_slug=github_url_slug, - webhook_data=webhook_data, - additions=additions, - deletions=deletions, - )) + event_manager['push_reviewed'].send( + PushReviewEntity( + project_name=project_name, + author=webhook_data['sender']['login'], + branch=webhook_data['ref'].replace('refs/heads/', ''), + updated_at=int(datetime.now().timestamp()), # 当前时间 + commits=commits, + score=score, + review_result=review_result, + url_slug=github_url_slug, + webhook_data=webhook_data, + additions=additions, + deletions=deletions, + ) + ) except Exception as e: error_message = f'服务出现未知错误: {str(e)}\n{traceback.format_exc()}' @@ -273,7 +282,7 @@ def handle_github_pull_request_event(webhook_data: dict, github_token: str, gith # review 代码 commits_text = ';'.join(commit['title'] for commit in commits) - review_result = CodeReviewer().review_and_strip_code(str(changes), commits_text) + review_result = CodeReviewer().review_and_strip_code(str(changes), commits_text, project_name) # 将review结果提交到GitHub的 notes handler.add_pull_request_notes(f'Auto Review Result: \n{review_result}') diff --git a/biz/utils/code_reviewer.py b/biz/utils/code_reviewer.py index a277ac59e..d8e3e7d00 100644 --- a/biz/utils/code_reviewer.py +++ b/biz/utils/code_reviewer.py @@ -1,7 +1,7 @@ import abc import os import re -from typing import Dict, Any, List +from typing import Dict, Any, List, Optional import yaml from jinja2 import Template @@ -16,11 +16,20 @@ class BaseReviewer(abc.ABC): def __init__(self, prompt_key: str): self.client = Factory().getClient() - self.prompts = self._load_prompts(prompt_key, os.getenv("REVIEW_STYLE", "professional")) + self.prompts = self._load_prompts(prompt_key) - def _load_prompts(self, prompt_key: str, style="professional") -> Dict[str, Any]: + def _load_prompts( + self, prompt_key: str, style: Optional[str] = None, prompt_templates_file: Optional[str] = None + ) -> Dict[str, Any]: """加载提示词配置""" - prompt_templates_file = "conf/prompt_templates.yml" + if not style: + # 如果未提供, 从环境变量中获取审查风格,默认为 "professional" + style = os.getenv("REVIEW_STYLE", "professional") + + if not prompt_templates_file: + # 如果未提供, 使用默认的提示词配置文件路径 + prompt_templates_file = "conf/prompt_templates.yml" + try: # 在打开 YAML 文件时显式指定编码为 UTF-8,避免使用系统默认的 GBK 编码。 with open(prompt_templates_file, "r", encoding="utf-8") as file: @@ -60,12 +69,13 @@ class CodeReviewer(BaseReviewer): def __init__(self): super().__init__("code_review_prompt") - def review_and_strip_code(self, changes_text: str, commits_text: str = "") -> str: + def review_and_strip_code(self, changes_text: str, commits_text: str = "", project_name: str = "") -> str: """ Review判断changes_text超出取前REVIEW_MAX_TOKENS个token,超出则截断changes_text, 调用review_code方法,返回review_result,如果review_result是markdown格式,则去掉头尾的``` - :param changes_text: - :param commits_text: + :param changes_text: 代码变更内容 + :param commits_text: 提交信息 + :param project_name: 项目名称 :return: """ # 如果超长,取前REVIEW_MAX_TOKENS个token @@ -80,20 +90,27 @@ def review_and_strip_code(self, changes_text: str, commits_text: str = "") -> st if tokens_count > review_max_tokens: changes_text = truncate_text_by_tokens(changes_text, review_max_tokens) - review_result = self.review_code(changes_text, commits_text).strip() + review_result = self.review_code(changes_text, commits_text, project_name).strip() if review_result.startswith("```markdown") and review_result.endswith("```"): return review_result[11:-3].strip() return review_result - def review_code(self, diffs_text: str, commits_text: str = "") -> str: + def review_code(self, diffs_text: str, commits_text: str = "", project_name: str = "") -> str: """Review 代码并返回结果""" + normalized_project_name = project_name.replace("-", "_") if project_name else project_name + project_prompts_path = os.getenv(f"{normalized_project_name.upper()}_PROMPT", None) + + # 按需重新加载 prompts 配置, 同时也可以支持项目级别提示词的热加载 + prompts = ( + self._load_prompts(prompt_key="code_review_prompt", prompt_templates_file=project_prompts_path) + if project_prompts_path + else self.prompts + ) messages = [ - self.prompts["system_message"], + prompts["system_message"], { "role": "user", - "content": self.prompts["user_message"]["content"].format( - diffs_text=diffs_text, commits_text=commits_text - ), + "content": prompts["user_message"]["content"].format(diffs_text=diffs_text, commits_text=commits_text), }, ] return self.call_llm(messages) diff --git a/doc/faq.md b/doc/faq.md index 8826b18e7..185dae66f 100644 --- a/doc/faq.md +++ b/doc/faq.md @@ -160,3 +160,34 @@ WORKER_QUEUE=gitlab_test_cn GITHUB_ACCESS_TOKEN=your-access-token #替换为你的Access Token ``` +### 同一个实例中是否支持给多个项目配置Review的提示词模板? + + +同一个实例中,可以配置多个项目Review的提示词模板。具体操作如下: +#### 1. 创建项目提示词文件 + +首先,为特定项目创建自定义提示词文件,例如 `conf/myproject_prompt.yml`。 + +提示词文件格式可参考 [prompt_templates.yml](./conf/prompt_templates.yml) + +#### 2. 配置环境变量 + +在 `.env` 文件中添加以下内容: + +```shell +# 配置规则:项目名全大写 + _PROMPT = 提示词文件路径 +PROJECT_NAME_PROMPT=conf/project_prompt.yml +``` + +例如,如果您的项目名为 `payment_service`,则应在 .env 文件中添加: + +```shell +PAYMENT_SERVICE_PROMPT=conf/payment_service_prompt.yml +``` + +#### 3. 注意事项 + +- 如果未配置项目专属提示词,系统将使用默认提示词模板 +- 项目提示词支持热加载,修改提示词文件后无需重启服务 +- 环境变量配置后需要重启服务才能生效 +- 项目名称中包含 `-` 时,请使用 `_` 替换