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:
2026-09-10 01:34:36 +09:00
committed by thanhnv
co-authored by Claude Opus 5
parent 33876f96f6
commit 9725cce2c4
5 changed files with 495 additions and 8 deletions
+6 -4
View File
@@ -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, "
+142
View File
@@ -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)
+157
View File
@@ -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á.
+186
View File
@@ -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()
+4 -4
View File
@@ -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 ""}