diff --git a/core/admin_agents.py b/core/admin_agents.py index 092228f..f4e470c 100644 --- a/core/admin_agents.py +++ b/core/admin_agents.py @@ -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, " diff --git a/core/help_knowledge.py b/core/help_knowledge.py new file mode 100644 index 0000000..6613ac2 --- /dev/null +++ b/core/help_knowledge.py @@ -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) diff --git a/docs/help/app_guide.md b/docs/help/app_guide.md new file mode 100644 index 0000000..f1edb7c --- /dev/null +++ b/docs/help/app_guide.md @@ -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á. diff --git a/tests/ui/test_help_knowledge.py b/tests/ui/test_help_knowledge.py new file mode 100644 index 0000000..785e050 --- /dev/null +++ b/tests/ui/test_help_knowledge.py @@ -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() diff --git a/ui/help_agent_widget.py b/ui/help_agent_widget.py index 423367b..a0fe815 100644 --- a/ui/help_agent_widget.py +++ b/ui/help_agent_widget.py @@ -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 ""}