feat(help): Trợ lý Hỗ trợ trả lời từ sổ tay và UI đang chạy, không từ phỏng đoán
Prompt cũ chỉ liệt kê TÊN các màn hình, nên model không có cách nào biết trên mỗi màn có gì và nó lấp khoảng trống bằng thứ nghe hợp lý: người dùng thật đã được hướng dẫn vào "Dashboard → Add Project" và "Settings → Project Settings → New Project". Không một thứ nào trong đó tồn tại. Người dùng đi tìm rồi mới phát hiện ra — câu trả lời trôi chảy mà sai còn tệ hơn câu "tôi không biết". Ba thứ ghép thêm vào prompt: - docs/help/app_guide.md — sổ tay viết tay, bám mã nguồn thật, có test chốt rằng nó nhắc đủ mọi màn trong docs/screens/manifest.json; - luật chống bịa, kèm ví dụ few-shot nêu đúng câu trả lời sai đã xảy ra cạnh câu đúng, và một ví dụ dạy nó NÓI KHÔNG BIẾT; - ngữ cảnh sống: màn hình đang mở và nhãn các nút/tab ĐANG hiện. Ngữ cảnh sống đọc từ cây widget thật, KHÔNG từ docs/screens/controls.json: file đó trích tự động nhưng đã cũ — 5/41 file trong đó không còn tồn tại và nó không có file nào trong presentation/ (chưa sinh lại sau refactor R08). Nạp nó vào prompt là dạy trợ lý về nút của những file đã bị xoá. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
committed by
thanhnv
co-authored by
Claude Opus 5
parent
33876f96f6
commit
9725cce2c4
@@ -58,10 +58,12 @@ _KIND_PROMPTS = {
|
||||
"allow. Reply strictly with the requested JSON verdict; err on the side of "
|
||||
"blocking anything that could exfiltrate data or damage the system."),
|
||||
"help": ("You are the in-app HELP assistant for this desktop application. Your ONLY job "
|
||||
"is to help the user understand and use THIS app — its screens and features "
|
||||
"(Dashboard, Schedule, Workspace with Cowork chat and the Co4E flow studio, "
|
||||
"Monitoring, Connectors, Settings), how to get things done in it, and how to "
|
||||
"troubleshoot using it. Be concise, friendly and practical.\n"
|
||||
"is to help the user understand and use THIS app: which screen they are on, what "
|
||||
"they can do there, and how to get things done. Be concise, friendly and practical.\n"
|
||||
"A handbook of this app's REAL screens and buttons is appended below, together with "
|
||||
"the screen the user currently has open. Answer from those two, never from how other "
|
||||
"software you know is laid out. If the handbook does not cover something, say so "
|
||||
"instead of guessing a menu path.\n"
|
||||
"STRICT RULES:\n"
|
||||
"- Answer ONLY questions about using this app. If asked to do anything else "
|
||||
"(write code for other purposes, do general research, chit-chat, run tasks, "
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
"""Kiến thức về chính ứng dụng, nạp cho Trợ lý Hỗ trợ trong app.
|
||||
|
||||
Trước khi có file này, prompt hệ thống của agent ``help``
|
||||
(``core/admin_agents.py::_KIND_PROMPTS``) chỉ là một đoạn văn liệt kê tên các
|
||||
màn hình. Model không có cách nào biết trên mỗi màn có gì, nên nó lấp khoảng
|
||||
trống bằng thứ nghe hợp lý: người dùng thật đã được hướng dẫn vào
|
||||
"Dashboard → Add Project" và "Settings → Project Settings → New Project" — cả
|
||||
hai đều không tồn tại. Câu trả lời trôi chảy mà sai còn tệ hơn câu "tôi không
|
||||
biết", vì người dùng đi tìm rồi mới phát hiện ra.
|
||||
|
||||
Ba thứ được ghép thêm vào prompt:
|
||||
|
||||
* **Sổ tay** (``docs/help/app_guide.md``) — viết tay, bám theo mã nguồn thật, và
|
||||
có test chốt rằng danh sách màn hình trong đó khớp ``docs/screens/manifest.json``.
|
||||
* **Luật chống bịa**, kèm ví dụ chính câu trả lời sai đã xảy ra.
|
||||
* **Ngữ cảnh sống** — màn hình đang mở và các nút/tab ĐANG hiện trên đó, đọc từ
|
||||
cây widget thật (``PageRegistryMixin.help_context``).
|
||||
|
||||
Vì sao ngữ cảnh sống đọc từ widget chứ không từ ``docs/screens/controls.json``:
|
||||
file đó được trích tự động nhưng đã cũ — 5/41 file trong đó không còn tồn tại,
|
||||
và nó không có file nào trong ``presentation/`` (chưa sinh lại sau refactor R08).
|
||||
Nạp nó vào prompt là dạy trợ lý về nút của những file đã bị xoá. Cây widget thật
|
||||
thì không bao giờ cũ được.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
|
||||
#: docs/help/app_guide.md — core/ nằm sâu 1 cấp so với gốc gói.
|
||||
_GUIDE = Path(__file__).resolve().parent.parent / "docs" / "help" / "app_guide.md"
|
||||
|
||||
#: Trần số nhãn thao tác đưa vào prompt. Một màn đông như Co4E có thể có hàng
|
||||
#: chục nút; dồn hết vào chỉ làm loãng phần còn lại của prompt mà không thêm
|
||||
#: thông tin — những nút đầu tiên là những nút người dùng nhìn thấy trước.
|
||||
_MAX_ACTIONS = 24
|
||||
|
||||
#: Luật chống bịa. Đặt SAU sổ tay trong prompt vì đây là thứ cuối cùng model đọc
|
||||
#: trước khi trả lời, và nó phải thắng mọi phỏng đoán.
|
||||
_GROUNDING = """
|
||||
LUẬT TRẢ LỜI VỀ ỨNG DỤNG NÀY — ưu tiên cao hơn mọi kiến thức có sẵn của bạn:
|
||||
|
||||
- CHỈ mô tả màn hình, nút và menu có trong sổ tay ở trên, hoặc trong danh sách
|
||||
nút đang hiện ở phần ngữ cảnh phía dưới. Hai nguồn đó là nguồn duy nhất.
|
||||
- KHÔNG suy ra tên nút hay đường dẫn menu từ các phần mềm khác bạn từng biết.
|
||||
Ứng dụng này không có "Add Project", không có "Project Settings", và Cài đặt
|
||||
không quản lý project.
|
||||
- Không có trong hai nguồn trên thì trả lời thẳng là bạn không chắc, rồi chỉ
|
||||
người dùng tới màn hình gần nhất có liên quan. Đoán một đường dẫn menu là câu
|
||||
trả lời tệ hơn "tôi không biết".
|
||||
- Khi hướng dẫn thao tác, nêu đúng đường đi: màn hình -> sub-tab -> tên nút y
|
||||
như trong sổ tay.
|
||||
- Trả lời ngắn. Ba bước đúng hơn mười bước trong đó có hai bước bịa.
|
||||
|
||||
VÍ DỤ — lỗi dưới đây ĐÃ xảy ra với người dùng thật, đừng lặp lại:
|
||||
|
||||
Hỏi: "Tôi tạo dự án mới thế nào?"
|
||||
SAI: "Vào Dashboard, nhấn Add Project, hoặc Settings -> Project Settings ->
|
||||
New Project. Điền Tên, Owner, Ngày bắt đầu/Kết thúc, Màu nhãn."
|
||||
Không một thứ nào trong câu đó tồn tại. Người dùng đã đi tìm và không thấy.
|
||||
ĐÚNG: "Vào Workspace ▸ Project, bấm Project mới ở hàng tiêu đề. Điền Tên, Mô
|
||||
tả, Hướng dẫn rồi bấm Lưu project. Tên phải khác các project đã có."
|
||||
|
||||
Hỏi: "Đổi API key ở đâu?"
|
||||
ĐÚNG: "Nút Cài đặt ở thanh trên, rồi vào mục Nhà cung cấp AI."
|
||||
|
||||
Hỏi: "Có xuất báo cáo PDF được không?"
|
||||
ĐÚNG: "Sổ tay không nói tới chỗ nào xuất PDF nên tôi không chắc app có chức
|
||||
năng đó. Gần nhất là Workspace ▸ Thư mục, nó xem được tệp PDF sẵn có."
|
||||
Nói không biết là câu trả lời đúng ở đây. Đoán một đường dẫn menu thì không.
|
||||
"""
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def app_guide() -> str:
|
||||
"""Nội dung sổ tay. Thiếu file thì trả chuỗi rỗng, không ném lỗi.
|
||||
|
||||
Trợ lý thiếu sổ tay vẫn phải mở được — nó chỉ kém hữu ích đi, còn ném lỗi ở
|
||||
đây thì hỏng luôn cả khung chat.
|
||||
"""
|
||||
try:
|
||||
return _GUIDE.read_text(encoding="utf-8").strip()
|
||||
except OSError:
|
||||
return ""
|
||||
|
||||
|
||||
def screen_context(screen: str = "", actions=()) -> str:
|
||||
"""Ngữ cảnh sống: màn hình đang mở, và những gì bấm được trên đó.
|
||||
|
||||
``actions`` là nhãn của các nút và tab ĐANG hiện. Model không nhìn được màn
|
||||
hình, nên không có phần này thì "ở đây làm được gì" là câu nó buộc phải
|
||||
đoán — và đoán chính là cách nó bịa ra nút "Add Project".
|
||||
"""
|
||||
screen = (screen or "").strip()
|
||||
# ``a is not None`` phải kiểm TRƯỚC khi str(): ``str(None)`` ra chuỗi "None",
|
||||
# khác rỗng, nên nó lọt qua bộ lọc và thành một "nút" tên None trong prompt.
|
||||
labels = [str(a).strip() for a in (actions or ())
|
||||
if a is not None and str(a).strip()]
|
||||
if not screen and not labels:
|
||||
return ""
|
||||
|
||||
parts = []
|
||||
if screen:
|
||||
parts.append("MÀN HÌNH NGƯỜI DÙNG ĐANG MỞ: " + screen)
|
||||
if labels:
|
||||
danh_sach = "\n".join("- " + label for label in labels[:_MAX_ACTIONS])
|
||||
parts.append(
|
||||
"NÚT VÀ TAB ĐANG HIỆN TRÊN MÀN ĐÓ (đọc từ giao diện đang chạy, nên "
|
||||
"đây là danh sách CHÍNH XÁC — người dùng hỏi về một nút không có "
|
||||
"trong danh sách này thì nói thẳng là màn này không có nút đó):\n"
|
||||
+ danh_sach)
|
||||
parts.append("Câu hỏi kiểu 'tôi đang ở đâu' hay 'ở đây làm được gì' là hỏi "
|
||||
"về chính màn hình này.")
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
def build_prompt(base_prompt: str, context: str = "") -> str:
|
||||
"""Prompt hệ thống đầy đủ cho Trợ lý Hỗ trợ.
|
||||
|
||||
Thứ tự có chủ ý: vai trò -> sổ tay -> luật chống bịa -> ngữ cảnh sống. Luật
|
||||
đứng sau sổ tay để nó là thứ cuối cùng model đọc về cách dùng sổ tay, còn
|
||||
ngữ cảnh đứng cuối vì nó đổi theo từng lượt hỏi và phải nằm sát câu hỏi nhất.
|
||||
|
||||
``context`` là khối đã được :func:`screen_context` định dạng sẵn — chỗ gọi
|
||||
nằm ở tầng Qt và nó dựng khối này qua ``PageRegistryMixin.help_context``.
|
||||
"""
|
||||
guide = app_guide()
|
||||
parts = [(base_prompt or "").strip()]
|
||||
if guide:
|
||||
parts += ["=== SỔ TAY ỨNG DỤNG ===", guide, _GROUNDING.strip()]
|
||||
ctx = (context or "").strip()
|
||||
if ctx:
|
||||
parts.append(ctx)
|
||||
return "\n\n".join(p for p in parts if p)
|
||||
|
||||
|
||||
def greeting(user_name: str = "") -> str:
|
||||
"""Câu chào mở đầu của khung trợ lý, có tên người dùng nếu biết."""
|
||||
from ..i18n import tr
|
||||
|
||||
name = (user_name or "").strip() or tr("help_agent.default_user")
|
||||
return tr("help_agent.greeting", name=name)
|
||||
@@ -0,0 +1,157 @@
|
||||
# Cowork-Local BamBOO — sổ tay màn hình và thao tác
|
||||
|
||||
Tài liệu này được nạp thẳng vào prompt hệ thống của **Trợ lý Hỗ trợ trong ứng dụng**
|
||||
(`core/admin_agents.py`, agent `help`). Nó là nguồn sự thật duy nhất mà trợ lý được phép
|
||||
dựa vào khi trả lời "màn này là gì / tôi làm được gì ở đây".
|
||||
|
||||
**Luật khi sửa file này:** chỉ ghi những gì THẬT SỰ có trong ứng dụng. Một nút không tồn
|
||||
tại ở đây sẽ trở thành một nút không tồn tại mà trợ lý bảo người dùng đi tìm. Danh sách
|
||||
màn hình phải khớp `docs/screens/manifest.json` — có test chốt việc đó
|
||||
(`tests/ui/test_help_knowledge.py`).
|
||||
|
||||
---
|
||||
|
||||
## 1. Bố cục chung
|
||||
|
||||
| Vùng | Có gì |
|
||||
|---|---|
|
||||
| **Thanh menu trái** | 4 màn chính; bộ chọn project; mục **GẦN ĐÂY** với link **Tất cả project…**; nút thu gọn menu. Kéo cạnh phải để đổi bề rộng (tối thiểu 132px, không kéo mất được) |
|
||||
| **Thanh trên** | Đổi giao diện Sáng/Tối, đổi ngôn ngữ (EN / JP / VN), nút Cài đặt |
|
||||
| **Thanh dưới** | Dòng trạng thái |
|
||||
| **Góc dưới phải** | Trợ lý Hỗ trợ (biểu tượng robot) — chính là tôi |
|
||||
|
||||
Bốn màn chính trên thanh menu: **Dashboard**, **Schedule Task**, **Workspace**, **Monitoring**.
|
||||
|
||||
⚠️ Ứng dụng **không có** màn "Project Settings", **không có** nút "Add Project" ở Dashboard.
|
||||
Mọi việc quản lý project nằm ở **Workspace ▸ Project**.
|
||||
|
||||
---
|
||||
|
||||
## 2. Workspace — màn chính, nơi app mở lên
|
||||
|
||||
Workspace có 5 sub-tab, chọn ở thanh menu trái: **Project**, **Cowork**, **Co4E**,
|
||||
**Thư mục**, **GraphRAG**.
|
||||
|
||||
⚠️ Cowork và GraphRAG **chỉ hiện khi đã chọn một project**. Chưa có project nào thì chỉ
|
||||
thấy sub-tab Project.
|
||||
|
||||
### 2.1 Workspace ▸ Project — quản lý project
|
||||
|
||||
Bên trái là danh sách project, mỗi dòng hiện tên và số liệu ("2 đoạn chat · 3 task").
|
||||
Bên phải là biểu mẫu của project đang chọn.
|
||||
|
||||
**Tạo project mới:** nút **Project mới** ở hàng tiêu đề, phía trên danh sách project.
|
||||
|
||||
**Sửa project đang có:** biểu mẫu mở ra ở chế độ **chỉ xem**. Bấm **Sửa project** (nút
|
||||
vàng) mới gõ được; nút **Lưu project** chuyển sang xanh lá. Lưu xong tự khoá lại.
|
||||
|
||||
**Bấm chuột phải vào một project** trong danh sách: **Mở** / **Sửa** / **Xoá**.
|
||||
|
||||
Các ô trong biểu mẫu:
|
||||
|
||||
| Ô | Ý nghĩa |
|
||||
|---|---|
|
||||
| Tên | Bắt buộc khác nhau giữa các project — trùng tên sẽ bị báo lỗi và không lưu |
|
||||
| Mô tả | Chú thích ngắn, hiện làm tooltip trong danh sách |
|
||||
| Hướng dẫn | Chỉ dẫn chung áp cho MỌI đoạn chat trong project này |
|
||||
| Thư mục làm việc | Thư mục sandbox của project. Nút Chọn thư mục để đổi, nút Mở thư mục để mở trong Explorer |
|
||||
|
||||
**Xoá project:** nút Xoá dưới danh sách, hoặc chuột phải ▸ Xoá. Có hỏi xác nhận.
|
||||
|
||||
### 2.2 Workspace ▸ Cowork — trò chuyện với agent
|
||||
|
||||
Khung chat của project đang chọn. Có ô soạn tin, đính kèm tệp, chọn thư mục output,
|
||||
và bảng **Lịch sử** hội thoại.
|
||||
|
||||
Link **Tất cả project…** ở mục GẦN ĐÂY trên thanh menu mở đúng khung này kèm bảng Lịch sử
|
||||
— nơi có tìm kiếm, lọc, ghim, đổi tên và xoá nhiều đoạn chat cùng lúc. Đây cũng là màn
|
||||
hình ứng dụng mở lên mặc định.
|
||||
|
||||
### 2.3 Workspace ▸ Co4E — xưởng luồng công việc
|
||||
|
||||
Canvas dạng đồ thị: kéo thả node, nối thành luồng, gán agent và skill cho từng bước, rồi
|
||||
chạy. Có bảng thuộc tính node bên phải và khung chat riêng. Luồng lưu chung cho cả máy
|
||||
(không thuộc một project).
|
||||
|
||||
### 2.4 Workspace ▸ Thư mục — duyệt và sửa tệp
|
||||
|
||||
Hai cột: cây thư mục và khung xem/sửa. Xem được tài liệu Office và PDF, sửa được tệp mã
|
||||
nguồn có tô màu cú pháp. Có **AI Edit**: nhờ AI sửa nội dung tệp đang mở.
|
||||
|
||||
### 2.5 Workspace ▸ GraphRAG — bộ nhớ mã nguồn
|
||||
|
||||
Dựng đồ thị tri thức từ thư mục làm việc của project, rồi hỏi đáp trên đó.
|
||||
|
||||
---
|
||||
|
||||
## 3. Dashboard — thống kê sử dụng
|
||||
|
||||
Biểu đồ và thẻ số liệu: token đã dùng, chi phí ước tính, thói quen sử dụng theo thời gian.
|
||||
Chọn được khoảng thời gian, nguồn (một task/phiên hoặc tất cả), và đơn vị tiền tệ.
|
||||
|
||||
⚠️ Đây là màn **chỉ xem số liệu**. Không tạo project, không tạo task ở đây.
|
||||
|
||||
---
|
||||
|
||||
## 4. Schedule Task — lịch trình
|
||||
|
||||
Hai cách nhìn: **Kanban** (theo cột trạng thái) và **Lịch** (theo ngày). Tạo và sửa task
|
||||
định kỳ; task chạy nền kể cả khi màn này không mở.
|
||||
|
||||
---
|
||||
|
||||
## 5. Monitoring — giám sát
|
||||
|
||||
Màn này giữ dải tab riêng, có 8 mục:
|
||||
|
||||
| Mục | Nội dung |
|
||||
|---|---|
|
||||
| Tổng quan | Tóm tắt trạng thái hệ thống |
|
||||
| Trạng thái Agent | Agent nào đang chạy, đã chạy gì |
|
||||
| Công cụ | Bật/tắt công cụ, quản lý **Connectors (MCP)** |
|
||||
| Nhật ký hành động | Lịch sử thao tác |
|
||||
| Lịch sử gọi MCP | Từng lượt gọi máy chủ MCP |
|
||||
| Sự kiện bảo mật | Cảnh báo và lệnh bị chặn |
|
||||
| Agents Admin | Cấu hình các agent quản trị, gồm cả Trợ lý Hỗ trợ này |
|
||||
| Icon | Bảng tra biểu tượng |
|
||||
|
||||
⚠️ **Connectors (MCP) nằm ở Monitoring ▸ Công cụ**, không nằm trong Cài đặt.
|
||||
|
||||
---
|
||||
|
||||
## 6. Cài đặt (nút ở thanh trên)
|
||||
|
||||
Hộp thoại 6 mục, chọn ở cột trái:
|
||||
|
||||
| Mục | Nội dung |
|
||||
|---|---|
|
||||
| Chung | Ngôn ngữ, giao diện, khay hệ thống, thư mục dùng chung |
|
||||
| Nhà cung cấp AI | Chọn provider và model, nhập API key |
|
||||
| Sandbox Security Layer | Chặn mạng, hỏi trước khi chạy lệnh, AI kiểm lệnh. **Khoá bằng mật khẩu** — phải bấm Unlock trước khi sửa được. Mật khẩu đặt qua biến môi trường `COWORK_SANDBOX_PASSWORD` |
|
||||
| Parameter | Giới hạn token, số tệp đính kèm, giới hạn tài nguyên |
|
||||
| Auto Model Routing | Tự chọn model theo chi phí/chất lượng |
|
||||
| Giới thiệu | Tên, phiên bản, tác giả |
|
||||
|
||||
⚠️ Cài đặt **không có** mục quản lý project.
|
||||
|
||||
---
|
||||
|
||||
## 7. Những chỗ người dùng hay hỏi
|
||||
|
||||
**"Tôi tạo project ở đâu?"** → Workspace ▸ Project, nút **Project mới**.
|
||||
Không phải Dashboard, không phải Cài đặt.
|
||||
|
||||
**"Sao tôi không sửa được project?"** → Biểu mẫu mặc định chỉ xem. Bấm **Sửa project**
|
||||
(nút vàng) trước.
|
||||
|
||||
**"Sao không thấy tab Cowork?"** → Phải chọn một project trước; Cowork và GraphRAG bị ẩn
|
||||
khi chưa có project.
|
||||
|
||||
**"Đổi API key ở đâu?"** → Cài đặt ▸ Nhà cung cấp AI.
|
||||
|
||||
**"Thêm MCP server ở đâu?"** → Monitoring ▸ Công cụ ▸ Connectors (MCP).
|
||||
|
||||
**"Đổi ngôn ngữ / giao diện?"** → Thanh trên cùng, hoặc Cài đặt ▸ Chung.
|
||||
|
||||
**"Mật khẩu Sandbox Security là gì?"** → Không có mật khẩu mặc định. Quản trị viên đặt qua
|
||||
biến môi trường `COWORK_SANDBOX_PASSWORD`. Chưa đặt thì nhóm thiết lập đó luôn khoá.
|
||||
@@ -0,0 +1,186 @@
|
||||
"""Trợ lý Hỗ trợ phải trả lời từ sổ tay, không phải từ trí nhớ về phần mềm khác.
|
||||
|
||||
Người dùng thật đã được trợ lý hướng dẫn vào "Dashboard → Add Project" và
|
||||
"Settings → Project Settings → New Project". Cả hai đều không tồn tại. Prompt cũ
|
||||
chỉ liệt kê TÊN các màn hình, nên model không có cách nào biết trên mỗi màn có
|
||||
gì và nó lấp khoảng trống bằng thứ nghe hợp lý.
|
||||
|
||||
Nhóm bài quan trọng nhất ở đây là nhóm CHỐNG TRÔI: sổ tay viết tay sẽ lỗi thời
|
||||
khi ai đó đổi UI, và một sổ tay sai còn tệ hơn không có sổ tay.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from cowork_local.core import help_knowledge
|
||||
|
||||
REPO = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def guide() -> str:
|
||||
return help_knowledge.app_guide()
|
||||
|
||||
|
||||
# ---- sổ tay có thật và được nạp ------------------------------------------
|
||||
|
||||
def test_so_tay_ton_tai_va_doc_duoc(guide):
|
||||
assert guide, "docs/help/app_guide.md rỗng hoặc không đọc được"
|
||||
assert len(guide) > 1500, "sổ tay quá ngắn để trả lời được câu hỏi thật"
|
||||
|
||||
|
||||
def test_thieu_so_tay_thi_khong_lam_hong_tro_ly(monkeypatch):
|
||||
"""Trợ lý thiếu sổ tay chỉ kém hữu ích đi, không được ném lỗi làm hỏng khung chat."""
|
||||
monkeypatch.setattr(help_knowledge, "_GUIDE", Path("khong-ton-tai.md"))
|
||||
help_knowledge.app_guide.cache_clear()
|
||||
try:
|
||||
assert help_knowledge.app_guide() == ""
|
||||
assert help_knowledge.build_prompt("vai tro").strip() == "vai tro"
|
||||
finally:
|
||||
help_knowledge.app_guide.cache_clear()
|
||||
|
||||
|
||||
# ---- CHỐNG TRÔI: sổ tay phải khớp màn hình thật ---------------------------
|
||||
|
||||
def _nav_screens() -> set[str]:
|
||||
"""Tên các màn hình thật, trích tự động từ docs/screens/manifest.json."""
|
||||
data = json.loads((REPO / "docs" / "screens" / "manifest.json").read_text(encoding="utf-8"))
|
||||
return {e["nav"].strip() for e in data if e.get("nav", "").strip()}
|
||||
|
||||
|
||||
def test_so_tay_nhac_du_moi_man_hinh_that(guide):
|
||||
"""Thiếu một màn là trợ lý sẽ không biết gì về màn đó."""
|
||||
thieu = [s for s in _nav_screens() if s.lower() not in guide.lower()]
|
||||
assert not thieu, f"sổ tay chưa nhắc tới: {sorted(thieu)}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bia", [
|
||||
"Add Project", # trợ lý từng bịa ra nút này ở Dashboard
|
||||
"Project Settings", # ...và mục này trong Cài đặt
|
||||
"New Project", # tên tiếng Anh của nút không tồn tại
|
||||
])
|
||||
def test_so_tay_khong_gioi_thieu_thu_khong_co(guide, bia):
|
||||
"""Sổ tay chỉ được PHỦ ĐỊNH mấy tên này, không được dùng như hướng dẫn.
|
||||
|
||||
Cho phép chúng xuất hiện trong mục "những chỗ hay hỏi" kèm chữ 'không có',
|
||||
vì nói thẳng "app không có nút đó" là đúng việc cần làm.
|
||||
"""
|
||||
for dong in guide.splitlines():
|
||||
if bia.lower() in dong.lower():
|
||||
assert "không có" in dong.lower() or "⚠️" in dong, (
|
||||
f"dòng nhắc {bia!r} mà không phủ định: {dong.strip()!r}")
|
||||
|
||||
|
||||
def test_so_tay_chi_dung_cho_tao_project(guide):
|
||||
"""Câu hỏi hay nhất — và là câu trợ lý từng trả lời sai."""
|
||||
assert "Workspace ▸ Project" in guide
|
||||
assert "Project mới" in guide
|
||||
|
||||
|
||||
# ---- ghép prompt ---------------------------------------------------------
|
||||
|
||||
def test_prompt_co_du_ba_phan(guide):
|
||||
prompt = help_knowledge.build_prompt(
|
||||
"VAI TRO", help_knowledge.screen_context("Workspace ▸ Project"))
|
||||
|
||||
assert "VAI TRO" in prompt
|
||||
assert "SỔ TAY ỨNG DỤNG" in prompt
|
||||
assert "Workspace ▸ Project" in prompt
|
||||
|
||||
|
||||
def test_luat_chong_bia_dung_SAU_so_tay(guide):
|
||||
"""Luật phải là thứ cuối cùng model đọc về cách dùng sổ tay."""
|
||||
prompt = help_knowledge.build_prompt("VAI TRO")
|
||||
|
||||
assert prompt.index("SỔ TAY ỨNG DỤNG") < prompt.index("LUẬT TRẢ LỜI")
|
||||
|
||||
|
||||
def test_man_hinh_dang_mo_dung_cuoi_cung():
|
||||
"""Nó đổi theo từng lượt hỏi nên đứng cuối, sát câu hỏi nhất."""
|
||||
prompt = help_knowledge.build_prompt(
|
||||
"VAI TRO", help_knowledge.screen_context("Monitoring"))
|
||||
|
||||
assert prompt.index("LUẬT TRẢ LỜI") < prompt.index("MÀN HÌNH NGƯỜI DÙNG ĐANG MỞ")
|
||||
|
||||
|
||||
def test_khong_biet_man_hinh_thi_khong_bia_ra_mot_cai():
|
||||
assert help_knowledge.screen_context("") == ""
|
||||
assert help_knowledge.screen_context("", []) == ""
|
||||
assert "MÀN HÌNH NGƯỜI DÙNG ĐANG MỞ" not in help_knowledge.build_prompt("VAI TRO")
|
||||
|
||||
|
||||
# ---- danh sách nút đang hiện, đọc từ giao diện đang chạy -----------------
|
||||
|
||||
def test_liet_ke_dung_cac_nut_dang_hien():
|
||||
"""Không có phần này thì "ở đây làm được gì" là câu model buộc phải đoán."""
|
||||
ctx = help_knowledge.screen_context("Workspace ▸ Project",
|
||||
["Project mới", "Sửa project", "Lưu project"])
|
||||
|
||||
assert "Project mới" in ctx
|
||||
assert "Sửa project" in ctx
|
||||
assert "CHÍNH XÁC" in ctx, "phải nói rõ đây là danh sách đầy đủ, không phải gợi ý"
|
||||
|
||||
|
||||
def test_nhan_rong_va_trung_khong_lam_ban_prompt():
|
||||
ctx = help_knowledge.screen_context("X", [" ", "", "Lưu", None])
|
||||
|
||||
assert ctx.count("- ") == 1
|
||||
|
||||
|
||||
def test_cat_bot_khi_qua_nhieu_nut():
|
||||
"""Màn đông như Co4E có hàng chục nút; dồn hết vào chỉ làm loãng prompt."""
|
||||
ctx = help_knowledge.screen_context("Co4E", [f"nut-{i}" for i in range(200)])
|
||||
|
||||
assert ctx.count("- ") == help_knowledge._MAX_ACTIONS
|
||||
|
||||
|
||||
def test_chi_co_nut_ma_khong_biet_man_hinh_thi_van_dung_duoc():
|
||||
ctx = help_knowledge.screen_context("", ["Lưu"])
|
||||
|
||||
assert "Lưu" in ctx
|
||||
|
||||
|
||||
# ---- ví dụ few-shot chốt đúng lỗi đã xảy ra ------------------------------
|
||||
|
||||
def test_luat_co_vi_du_cau_tra_loi_sai_that(guide):
|
||||
"""Nêu tên đúng thứ nó từng bịa thì hiệu quả hơn một luật chung chung."""
|
||||
prompt = help_knowledge.build_prompt("VAI TRO")
|
||||
|
||||
assert "Add Project" in prompt
|
||||
assert "SAI:" in prompt and "ĐÚNG:" in prompt
|
||||
|
||||
|
||||
def test_luat_day_cach_noi_khong_biet(guide):
|
||||
"""Nói không biết là câu trả lời đúng, và phải có ví dụ cho nó."""
|
||||
prompt = help_knowledge.build_prompt("VAI TRO")
|
||||
|
||||
assert "không chắc" in prompt
|
||||
|
||||
|
||||
# ---- màn hình đang mở, lấy từ cửa sổ thật --------------------------------
|
||||
|
||||
def test_cua_so_bao_dung_ten_man_hinh_dang_mo(qapp, tmp_path):
|
||||
from cowork_local.presentation.shell.bootstrap import build_config, build_context
|
||||
from cowork_local.presentation.shell.main_window import MainWindow
|
||||
|
||||
config_path = tmp_path / "config.json"
|
||||
build_config(config_path)
|
||||
win = MainWindow(build_context(config_path))
|
||||
try:
|
||||
nhan = win.current_screen_label()
|
||||
|
||||
assert nhan, "cửa sổ không nói được nó đang mở màn nào"
|
||||
assert "▸" in nhan, f"màn Workspace phải kèm sub-tab, nhận được {nhan!r}"
|
||||
# Tên trong nhãn phải là tên THẬT, để trợ lý nhắc lại được cho người dùng
|
||||
assert nhan.split("▸")[-1].strip() in help_knowledge.app_guide()
|
||||
|
||||
# Ngữ cảnh sống phải đi qua được toàn bộ đường: widget -> prompt
|
||||
ctx = win.help_context()
|
||||
assert nhan in ctx
|
||||
assert "VAI TRO" in help_knowledge.build_prompt("VAI TRO", ctx)
|
||||
finally:
|
||||
win.close()
|
||||
@@ -23,7 +23,7 @@ from PySide6.QtWidgets import (
|
||||
QVBoxLayout, QWidget,
|
||||
)
|
||||
|
||||
from ..core import admin_agents
|
||||
from ..core import admin_agents, help_knowledge
|
||||
from ..core.worker import AgentWorker
|
||||
from ..i18n import tr
|
||||
from .icons import icon
|
||||
@@ -217,8 +217,7 @@ class HelpAgentWidget(QWidget):
|
||||
# ---- greeting / labels ------------------------------------------------
|
||||
def _greeting(self) -> str:
|
||||
"""Câu chào mở đầu, có tên người dùng nếu biết."""
|
||||
name = self._user_name or tr("help_agent.default_user")
|
||||
return tr("help_agent.greeting", name=name)
|
||||
return help_knowledge.greeting(self._user_name)
|
||||
|
||||
# ---- construction -----------------------------------------------------
|
||||
def _build_edge_tab(self) -> None:
|
||||
@@ -464,11 +463,12 @@ class HelpAgentWidget(QWidget):
|
||||
agent = admin_agents.ensure_help_agent(
|
||||
admin_agents.agents_admin_dir(self.ctx.config.shared_dir))
|
||||
history = list(self._history)
|
||||
sys_prompt = help_knowledge.build_prompt(agent.effective_prompt(), getattr(self.parent(), "help_context", lambda: "")())
|
||||
|
||||
def job(worker):
|
||||
"""Chạy nền: gọi provider của Help Agent kèm prompt hệ thống của nó."""
|
||||
provider = admin_agents.build_agent_provider(self.ctx, agent)
|
||||
messages = [{"role": "system", "content": agent.effective_prompt()}] + history
|
||||
messages = [{"role": "system", "content": sys_prompt}] + history
|
||||
result = provider.chat(messages, tools=None, cancel=worker.is_cancelled)
|
||||
content = result.get("content", "") if isinstance(result, dict) else str(result)
|
||||
return {"content": provider.strip_think(content) or ""}
|
||||
|
||||
Reference in New Issue
Block a user