Delta team/fix comment ui #10
@@ -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