CI / test (push) Canceled after 0s
fix các bug theo yêu cầu https://fptsoftware362-my.sharepoint.com/❌/g/personal/nampdt_fpt_com/IQAHBJ4A9xqDTLgvt2bhukJEAdRB5LRz2hbJpTivvIiBSYM?wdExp=TEAMS-TREATMENT&web=1&isSPOFile=1&ovuser=f01e930a-b52e-42b1-b70f-a8882b5d043b%2CAnhTNM1%40fpt.com&clickparams=eyJBcHBOYW1lIjoiVGVhbXMtRGVza3RvcCIsIkFwcFZlcnNpb24iOiI0OS8yNjA4MTMxOTMxNyIsIkhhc0ZlZGVyYXRlZFVzZXIiOmZhbHNlfQ%3D%3D --------- Co-authored-by: Duy Le Huu <duylh19@fpt.com> Reviewed-on: #10 Co-authored-by: Anh Tran Nguyen Minh <anhtnm1@fpt.com>
143 lines
7.5 KiB
Python
143 lines
7.5 KiB
Python
"""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)
|