Skip to content

Commit 84bfaa6

Browse files
authored
Merge pull request #134 from Integration-Automation/dev
Wire the prompt editors into the review, extend the chain, and cover three thin areas
2 parents a6f4ffd + d0068d2 commit 84bfaa6

21 files changed

Lines changed: 1618 additions & 352 deletions

.github/workflows/dev.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,16 @@ jobs:
7070
uses: actions/download-artifact@v4
7171
with:
7272
name: coverage-xml
73+
- name: Read package version
74+
# Without a version, SonarCloud's "previous version" new-code baseline has
75+
# nothing to anchor to and counts the whole history as new -- which is how
76+
# new_lines came to exceed the project's total line count.
77+
id: version
78+
run: echo "value=$(grep -m1 '^version = ' pyproject.toml | cut -d'"' -f2)" >> "$GITHUB_OUTPUT"
7379
- name: SonarQube Cloud scan
7480
uses: SonarSource/sonarqube-scan-action@v8.2.1
81+
with:
82+
args: -Dsonar.projectVersion=${{ steps.version.outputs.value }}
7583
env:
7684
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
7785
SONAR_HOST_URL: https://sonarcloud.io

.github/workflows/stable.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,16 @@ jobs:
7070
uses: actions/download-artifact@v4
7171
with:
7272
name: coverage-xml
73+
- name: Read package version
74+
# Without a version, SonarCloud's "previous version" new-code baseline has
75+
# nothing to anchor to and counts the whole history as new -- which is how
76+
# new_lines came to exceed the project's total line count.
77+
id: version
78+
run: echo "value=$(grep -m1 '^version = ' pyproject.toml | cut -d'"' -f2)" >> "$GITHUB_OUTPUT"
7379
- name: SonarQube Cloud scan
7480
uses: SonarSource/sonarqube-scan-action@v8.2.1
81+
with:
82+
args: -Dsonar.projectVersion=${{ steps.version.outputs.value }}
7583
env:
7684
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
7785
SONAR_HOST_URL: https://sonarcloud.io

architecture_explore.md

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -243,20 +243,33 @@ call_X_multi_file_and_send() → run_dir_files_with_package(..., True)
243243

244244
```
245245
extend_ai_gui/
246-
├── ai_gui_global_variable.py 模板檔名清單 + 檔名→模板內容對照表
246+
├── ai_gui_global_variable.py 模板檔名清單 + 檔名→內建模板內容對照表
247+
├── prompt_store.py 編輯過的 prompt 檔案解析(~/.pybreeze/prompts/)
247248
├── code_review/
248-
│ ├── code_review_thread.py SenderThread(QThread):CoT 多階段審查
249+
│ ├── cot_chain.py 接線表(純邏輯,無 Qt):哪步引用哪步
250+
│ ├── code_review_thread.py SenderThread(QThread):跑八步審查鏈
249251
│ └── cot_code_review_gui.py UI
250252
├── prompt_edit_gui/
251-
│ ├── cot_prompt_editor_widget.py 編輯 5 個 CoT 模板(QFileSystemWatcher 熱更新)
252-
│ ├── skills_prompt_editor_widget.py 編輯 2 個 Skill 模板
253+
│ ├── prompt_editor_widget.py 共用編輯器(QFileSystemWatcher 熱更新)
254+
│ ├── cot_prompt_editor_widget.py 8 個 CoT 模板的檔案清單+語言鍵
255+
│ ├── skills_prompt_editor_widget.py 2 個 Skill 模板的檔案清單+語言鍵
253256
│ ├── prompt_file_io.py 共用存檔(失敗跳警告對話框)
254-
│ ├── cot_code_review_prompt_templates/ 7 個模板常數
257+
│ ├── cot_code_review_prompt_templates/ 8 個模板常數+global_rule
255258
│ └── skills_prompt_templates/ 2 個模板常數
256259
└── skills/skills_send_gui.py 單次 prompt 發送(RequestThread)
257260
```
258261

259-
**CoT 審查鏈**`code_review_thread.py`):`first_summary``first_code_review``linter``code_smell_detector``total_summary`。前四階段的結果被收集起來餵給最後的 total summary。每階段都套 `build_global_rule_template()` 包一層全域規則。
262+
**CoT 審查鏈**`cot_chain.py` 定義接線,`code_review_thread.py` 執行)八個步驟:
263+
264+
```
265+
first_summary → first_code_review → judge_single_review ┐(評分前一步的審查)
266+
→ linter → code_smell_detector → step_by_step_analysis ┐(走過每條發現)
267+
→ total_summary → judge(帶 linter/code smell 脈絡評分總結)
268+
```
269+
270+
**編輯過的 prompt 會生效**`prompt_store.py`):每個模板都以程式碼常數出貨,`~/.pybreeze/prompts/<名稱>.md` 存在且非空時覆寫它。編輯器讀寫的就是這個位置,所以在編輯器裡改 prompt 會改變審查實際送出的內容 —— 這正是編輯器存在的理由。檔案缺失、空白、讀不到都退回內建版本;編輯過的 prompt 若含有鏈填不了的 placeholder,記 log 後退回內建,不讓整次審查倒在使用者無法從 UI 診斷的 `KeyError` 上。讀取不會建目錄,只有存檔才會。
271+
272+
`cot_chain.py` 用兩張表描述接線:`STEP_RESULT_KEY`(每步答案存在哪個 key)與 `STEP_ARGUMENTS`(每步的 placeholder 由哪個 key 填)。**順序即相依順序** —— 每步只能引用它上面的步驟,`test_cot_chain.py` 有結構性測試守住這件事。步驟失敗時錯誤訊息只顯示給使用者、不會被存進 results,避免後續步驟把「傳送失敗」當成審查內容引用。每步都套 `build_global_rule_template()` 包一層全域規則。
260273

261274
安全處理:送出前 `validate_url()``allow_redirects=False``stream=True` 搭配 `read_capped_text()` 限制回應大小、單一 `requests.Session` 重用 TCP/TLS 連線、`isInterruptionRequested()` 讓 widget 關閉時能中止。
262275

@@ -387,6 +400,7 @@ extend_ai_gui/
387400
|---|---|
388401
| `ssh_known_hosts` | TOFU 確認過的 SSH host key |
389402
| `prthinker_setting.json` | prthinker 後端/平台/金鑰設定 |
403+
| `prompts/*.md` | 編輯過的 CoT / Skill prompt,覆寫內建模板 |
390404
| `response_stats.txt` | AI 審查接受/拒絕統計 |
391405
| `urls.txt` | AI 審查端點歷史 |
392406

@@ -396,10 +410,10 @@ extend_ai_gui/
396410

397411
## 18. 測試與 CI
398412

399-
- **單元測試** `test/test_utils/`60`test_*.py`。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。
413+
- **單元測試** `test/test_utils/`65`test_*.py`、985 個測試。純邏輯 + headless Qt widget 測試(`QT_QPA_PLATFORM=offscreen`)。涵蓋 curl/HAR 解析、SSRF 驗證、SSH 安全、process reader EOF、queue pump、語言對齊、mermaid parser、diagram 序列化、prthinker 設定等。有 hypothesis fuzz 測試(`test_fuzz_pure_logic.py`)。
400414
- **整合測試** `test/unit_test/start_automation/` — 以 `debug_mode=True` 啟動 IDE,10 秒後自動關閉,驗證啟動流程與 extend tab
401415
- **CI** `.github/workflows/{dev,stable}.yml``unit-tests` job 跑 Windows runner、Python 3.10–3.14 矩陣,3.12 那一腳額外上傳 `coverage-xml` artifact;`sonarcloud` job 跑 ubuntu、`needs: unit-tests`。每日 02:00 排程 + push/PR 觸發。`stable.yml` 另有 `publish` job 負責版號遞增與 PyPI 發布
402-
- **覆蓋率** `.coveragerc``relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 57%(`utils/``tools_gui` 95–100%,UI 層 20–45% 拉低
416+
- **覆蓋率** `.coveragerc``relative_files = True` 是必要的:報告在 Windows 產生、由 Linux 上的 scanner 讀取,路徑不能帶機器資訊。目前整體 60%(`utils/``tools_gui``dialog` 95–100%`editor_main` 58%、`menu` 54%;仍低的是 `diagram_editor` 45%`process_executor` 39%、`connect_gui` 28%
403417
- **靜態分析** SonarCloud(`sonar-project.properties`,CI-based analysis;Automatic Analysis 已關閉且必須維持關閉,兩種模式互斥)+ Codacy(`.codacy.yml`)+ Bandit(`pyproject.toml` 中排除 test、skip B101/B404)
404418
- **SonarCloud 方案限制** 該組織的方案只開放 `main` 與 PR 的分析結果。非 main 分支的分析送得出去、CE 任務也會成功,但結果讀回來是 403(組織內每個專案都只有 `main` 一條分支)。因此 `dev.yml` 只在 PR 時掃描,`stable.yml` 另外掃 push to `main`
405419

pybreeze/extend_multi_language/extend_english.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,8 @@
134134
"prthinker_setting_extra_arguments_label": "Extra command-line arguments",
135135
"prthinker_setting_source_path_label": "prthinker source folder",
136136
"prthinker_setting_stored_at_label": "Stored at:",
137+
# Prompt editors — where the edited prompts override the built-in ones from
138+
"prompt_editor_stored_at_label": "Prompt files (these override the built-in prompts):",
137139
"prthinker_choose_source_path_label": "Choose the prthinker source folder",
138140
"prthinker_need_source_path_message":
139141
"prthinker is installed from source. Choose the folder holding its "

pybreeze/extend_multi_language/extend_traditional_chinese.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,8 @@
134134
"prthinker_setting_extra_arguments_label": "額外的命令列參數",
135135
"prthinker_setting_source_path_label": "prthinker 原始碼資料夾",
136136
"prthinker_setting_stored_at_label": "設定檔位置:",
137+
# Prompt 編輯器 —— 編輯過的 prompt 會覆寫內建版本
138+
"prompt_editor_stored_at_label": "Prompt 檔案位置(會覆寫內建 prompt):",
137139
"prthinker_choose_source_path_label": "選擇 prthinker 原始碼資料夾",
138140
"prthinker_need_source_path_message":
139141
"prthinker 是從原始碼安裝的。請選擇含有 pyproject.toml 的資料夾,"

pybreeze/pybreeze_ui/extend_ai_gui/ai_gui_global_variable.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,29 +6,45 @@
66
FIRST_CODE_REVIEW_TEMPLATE
77
from pybreeze.pybreeze_ui.extend_ai_gui.prompt_edit_gui.cot_code_review_prompt_templates.first_summary_prompt import \
88
FIRST_SUMMARY_TEMPLATE
9+
from pybreeze.pybreeze_ui.extend_ai_gui.prompt_edit_gui.cot_code_review_prompt_templates.judge import \
10+
JUDGE_TEMPLATE
11+
from pybreeze.pybreeze_ui.extend_ai_gui.prompt_edit_gui.cot_code_review_prompt_templates.judge_single_review import \
12+
JUDGE_SINGLE_REVIEW_TEMPLATE
913
from pybreeze.pybreeze_ui.extend_ai_gui.prompt_edit_gui.cot_code_review_prompt_templates.linter import \
1014
LINTER_TEMPLATE
15+
from pybreeze.pybreeze_ui.extend_ai_gui.prompt_edit_gui.cot_code_review_prompt_templates.step_by_step_analysis import \
16+
STEP_BY_STEP_ANALYSIS_TEMPLATE
1117
from pybreeze.pybreeze_ui.extend_ai_gui.prompt_edit_gui.cot_code_review_prompt_templates.total_summary import \
1218
TOTAL_SUMMARY_TEMPLATE
1319
from pybreeze.pybreeze_ui.extend_ai_gui.prompt_edit_gui.skills_prompt_templates.code_explainer import \
1420
CODE_EXPLAINER_TEMPLATE
1521
from pybreeze.pybreeze_ui.extend_ai_gui.prompt_edit_gui.skills_prompt_templates.code_review import \
1622
CODE_REVIEW_SKILL_TEMPLATE
1723

24+
# The order the chain runs in. Each step may only quote steps above it, so the
25+
# order is a dependency order, not a preference: judge_single_review scores the
26+
# review written just before it, step_by_step_analysis walks the linter and code
27+
# smell findings, and judge scores the finished summary.
1828
COT_TEMPLATE_FILES = [
1929
"first_summary_prompt.md",
2030
"first_code_review.md",
31+
"judge_single_review.md",
2132
"linter.md",
2233
"code_smell_detector.md",
34+
"step_by_step_analysis.md",
2335
"total_summary.md",
36+
"judge.md",
2437
]
2538

2639
COT_TEMPLATE_RELATION = {
2740
"first_summary_prompt.md": FIRST_SUMMARY_TEMPLATE,
2841
"first_code_review.md": FIRST_CODE_REVIEW_TEMPLATE,
42+
"judge_single_review.md": JUDGE_SINGLE_REVIEW_TEMPLATE,
2943
"linter.md": LINTER_TEMPLATE,
3044
"code_smell_detector.md": CODE_SMELL_DETECTOR_TEMPLATE,
45+
"step_by_step_analysis.md": STEP_BY_STEP_ANALYSIS_TEMPLATE,
3146
"total_summary.md": TOTAL_SUMMARY_TEMPLATE,
47+
"judge.md": JUDGE_TEMPLATE,
3248
}
3349

3450
SKILLS_TEMPLATE_FILES = [

pybreeze/pybreeze_ui/extend_ai_gui/code_review/code_review_thread.py

Lines changed: 29 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
from PySide6.QtCore import QThread, Signal
66
from je_editor import language_wrapper
77

8-
from pybreeze.pybreeze_ui.extend_ai_gui.ai_gui_global_variable import COT_TEMPLATE_RELATION
9-
from pybreeze.pybreeze_ui.extend_ai_gui.prompt_edit_gui.cot_code_review_prompt_templates.global_rule import \
10-
build_global_rule_template
8+
from pybreeze.pybreeze_ui.extend_ai_gui.code_review.cot_chain import (
9+
CODE_DIFF, STEP_RESULT_KEY, build_prompt
10+
)
1111
from pybreeze.utils.logging.logger import pybreeze_logger
1212
from pybreeze.utils.network.http_client import (
1313
ResponseTooLargeError, read_capped_text, CONNECT_TIMEOUT,
@@ -40,74 +40,36 @@ def run(self):
4040
session.close()
4141

4242
def _run_templates(self, session: requests.Session, code: str) -> None:
43-
first_code_review_result = None
44-
first_summary_result = None
45-
linter_result = None
46-
code_smell_result = None
43+
# Answers accumulate here as the chain runs; a later step quotes whichever
44+
# of them its template asks for. See cot_chain for the wiring.
45+
results: dict[str, str] = {CODE_DIFF: code}
4746
for file in self.files:
4847
# Stop promptly if the widget is closing instead of firing off the
4948
# remaining per-template POSTs.
5049
if self.isInterruptionRequested():
5150
return
52-
match file:
53-
case "first_summary_prompt.md":
54-
first_summary_prompt = COT_TEMPLATE_RELATION["first_summary_prompt.md"]
55-
prompt = build_global_rule_template(
56-
prompt=first_summary_prompt.format(code_diff=code)
57-
)
58-
case "first_code_review.md":
59-
first_code_review_prompt = COT_TEMPLATE_RELATION["first_code_review.md"]
60-
prompt = build_global_rule_template(
61-
prompt=first_code_review_prompt.format(code_diff=code)
62-
)
63-
case "linter.md":
64-
linter_prompt = COT_TEMPLATE_RELATION["linter.md"]
65-
prompt = build_global_rule_template(
66-
prompt=linter_prompt.format(code_diff=code)
67-
)
68-
case "code_smell_detector.md":
69-
code_smell_detector_prompt = COT_TEMPLATE_RELATION["code_smell_detector.md"]
70-
prompt = build_global_rule_template(
71-
prompt=code_smell_detector_prompt.format(code_diff=code)
72-
)
73-
case "total_summary.md":
74-
total_summary_prompt = COT_TEMPLATE_RELATION["total_summary.md"]
75-
prompt = build_global_rule_template(
76-
prompt=total_summary_prompt.format(
77-
first_code_review=first_code_review_result,
78-
first_summary=first_summary_result,
79-
linter_result=linter_result,
80-
code_smell_result=code_smell_result,
81-
code_diff=code,
82-
)
83-
)
84-
case _:
85-
continue
86-
87-
try:
88-
# 傳送到指定 URL(重用 session 連線)
89-
resp = session.post(
90-
self.url, json={"prompt": prompt},
91-
timeout=(CONNECT_TIMEOUT, 60), allow_redirects=False, stream=True,
92-
)
93-
reply_text = read_capped_text(resp)
94-
match file:
95-
case "first_summary_prompt.md":
96-
first_summary_result = reply_text
97-
case "first_code_review.md":
98-
first_code_review_result = reply_text
99-
case "linter.md":
100-
linter_result = reply_text
101-
case "code_smell_detector.md":
102-
code_smell_result = reply_text
103-
case _:
104-
# total_summary.md has no intermediate result to store but
105-
# must still be emitted — `continue` here previously
106-
# dropped the final summary before it reached the UI.
107-
pass
108-
except (requests.RequestException, ResponseTooLargeError) as e:
109-
pybreeze_logger.error("CoT code review send failed for %s: %r", file, e)
110-
reply_text = f"{language_wrapper.language_word_dict.get('cot_gui_error_sending')} {file} {e}"
111-
51+
prompt = build_prompt(file, results)
52+
if prompt is None:
53+
continue
54+
reply_text, answered = self._ask(session, file, prompt)
55+
result_key = STEP_RESULT_KEY.get(file)
56+
# A failure message is shown but never stored: a later step must not
57+
# quote "could not send" back to the model as if it were a review.
58+
if answered and result_key is not None:
59+
results[result_key] = reply_text
11260
# 發送訊號更新 UI
11361
self.update_response.emit(file, reply_text)
62+
63+
def _ask(self, session: requests.Session, file: str, prompt: str) -> tuple[str, bool]:
64+
"""Send one step's prompt; return its answer and whether it arrived."""
65+
try:
66+
# 傳送到指定 URL(重用 session 連線)
67+
resp = session.post(
68+
self.url, json={"prompt": prompt},
69+
timeout=(CONNECT_TIMEOUT, 60), allow_redirects=False, stream=True,
70+
)
71+
return read_capped_text(resp), True
72+
except (requests.RequestException, ResponseTooLargeError) as error:
73+
pybreeze_logger.error("CoT code review send failed for %s: %r", file, error)
74+
word = language_wrapper.language_word_dict
75+
return f"{word.get('cot_gui_error_sending')} {file} {error}", False

0 commit comments

Comments
 (0)