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>
This commit was merged in pull request #10.
This commit is contained in:
@@ -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)
|
||||
@@ -132,6 +132,58 @@ def _matches_query(query: str, title: str, messages: List[Dict[str, Any]]) -> bo
|
||||
return False
|
||||
|
||||
|
||||
def history_dirs() -> list:
|
||||
"""Các cặp ``(project_id, thư mục lịch sử)`` của MỌI project, cộng thư mục
|
||||
mặc định cho hội thoại chưa thuộc project nào.
|
||||
|
||||
Có hàm này vì lịch sử KHÔNG nằm chung một chỗ, mà nằm trong thư mục làm việc
|
||||
của từng project. Ai chỉ gọi ``list_conversations()`` một lần sẽ chỉ thấy
|
||||
hội thoại của project đang mở — hoặc, nếu gọi không tham số, không thấy cái
|
||||
nào cả. Đó chính là hai lỗi đã xảy ra: khung "Tất cả project…" hiện nhóm
|
||||
rỗng cho mọi project trừ một, và mọi dòng project đều đếm "0 đoạn chat".
|
||||
"""
|
||||
from ..config import HISTORY_DIR
|
||||
from .projects import list_projects, project_history_dir
|
||||
|
||||
pairs = [("default", HISTORY_DIR)]
|
||||
for project in list_projects():
|
||||
pairs.append((project.project_id, project_history_dir(project)))
|
||||
return pairs
|
||||
|
||||
|
||||
def list_conversations_by_project(pairs, query: str = "") -> List[Dict[str, Any]]:
|
||||
"""Gộp lịch sử hội thoại của NHIỀU project. ``pairs`` là các cặp
|
||||
``(project_id, directory)``.
|
||||
|
||||
Lịch sử KHÔNG nằm chung một chỗ: ``WorkspaceTab`` đặt
|
||||
``config._project_history_dir`` thành ``<workspace của project>/.cowork_history``
|
||||
mỗi lần người dùng chọn project khác, nên ``config.history_dir()`` chỉ trả về
|
||||
thư mục của project ĐANG mở. Một lần gọi :func:`list_conversations` vì thế
|
||||
chỉ thấy được hội thoại của project đó — khung "Tất cả project…" dựng đủ
|
||||
tiêu đề nhóm cho mọi project nhưng mọi nhóm trừ một đều rỗng.
|
||||
|
||||
Thư mục là chủ sở hữu có thẩm quyền: hội thoại nằm trong thư mục làm việc của
|
||||
project nào thì thuộc project đó, kể cả khi trường ``project_id`` ghi trong
|
||||
file đã cũ (project bị đổi thư mục chẳng hạn).
|
||||
"""
|
||||
seen: set = set()
|
||||
items: List[Dict[str, Any]] = []
|
||||
for project_id, directory in pairs:
|
||||
if directory is None:
|
||||
continue
|
||||
for meta in list_conversations(directory, query=query):
|
||||
key = str(meta["path"])
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
if project_id:
|
||||
meta["project_id"] = project_id
|
||||
items.append(meta)
|
||||
# Cùng thứ tự mà list_conversations dùng: ghim lên đầu, rồi mới nhất trước.
|
||||
items.sort(key=lambda d: (not d["pinned"], -d["mtime"]))
|
||||
return items
|
||||
|
||||
|
||||
def list_conversations(directory: Optional[Path] = None, query: str = "") -> List[Dict[str, Any]]:
|
||||
"""List saved conversations, most recent first (pinned always on top).
|
||||
|
||||
|
||||
@@ -110,6 +110,16 @@ def _slugify(name: str) -> str:
|
||||
return s or "project"
|
||||
|
||||
|
||||
#: Lich su hoi thoai cua mot project nam TRONG thu muc lam viec cua no, de chia
|
||||
#: se thu muc do la chia se ca lich su (may khac xem va tiep tuc duoc).
|
||||
HISTORY_SUBDIR = ".cowork_history"
|
||||
|
||||
|
||||
def project_history_dir(project) -> Path:
|
||||
"""Thư mục lịch sử hội thoại của một project."""
|
||||
return project.workspace_dir() / HISTORY_SUBDIR
|
||||
|
||||
|
||||
def new_project(name: str, description: str = "", instructions: str = "",
|
||||
output_dir: str = "", directory: Path = None) -> Project:
|
||||
"""Create + persist a new project with a unique id derived from the name."""
|
||||
|
||||
+13
-8
@@ -258,9 +258,13 @@ def dependencies_met(task: Dict[str, Any], directory: Path = None) -> bool:
|
||||
def depends_cycle_error(tasks: List[Dict[str, Any]], task_id: str,
|
||||
depends_on: List[str]) -> Optional[str]:
|
||||
"""Validate a proposed depends_on list: no self-wait, no wait-cycle
|
||||
(A waits B while B — directly or transitively — waits A)."""
|
||||
(A waits B while B — directly or transitively — waits A).
|
||||
|
||||
Trả về KHOÁ i18n chứ không phải câu đã dịch: tầng này không biết người dùng
|
||||
đang chọn ngôn ngữ nào, nên nơi hiển thị mới là nơi gọi ``tr()``.
|
||||
"""
|
||||
if task_id in (depends_on or []):
|
||||
return "A task cannot wait for itself."
|
||||
return "schedtask.err_self_wait"
|
||||
by_id = {t["task_id"]: t for t in tasks}
|
||||
# DFS from each proposed prerequisite through ITS prerequisites.
|
||||
for start in depends_on or []:
|
||||
@@ -268,7 +272,7 @@ def depends_cycle_error(tasks: List[Dict[str, Any]], task_id: str,
|
||||
while stack:
|
||||
cur = stack.pop()
|
||||
if cur == task_id:
|
||||
return "This would create a circular wait between tasks."
|
||||
return "schedtask.err_wait_cycle"
|
||||
if cur in seen:
|
||||
continue
|
||||
seen.add(cur)
|
||||
@@ -280,22 +284,23 @@ def depends_cycle_error(tasks: List[Dict[str, Any]], task_id: str,
|
||||
def chain_error(tasks: List[Dict[str, Any]], task_id: str,
|
||||
next_task_id: Optional[str]) -> Optional[str]:
|
||||
"""Validate assigning ``next_task_id`` as ``task_id``'s next task.
|
||||
Returns an error string (self-link / circular chain / unknown id), or
|
||||
None when the assignment is safe."""
|
||||
Returns an i18n KEY for the problem (self-link / circular chain / unknown
|
||||
id), or None when the assignment is safe. Khoá chứ không phải câu đã dịch —
|
||||
xem ``depends_cycle_error``."""
|
||||
if not next_task_id:
|
||||
return None
|
||||
if next_task_id == task_id:
|
||||
return "A task cannot chain to itself."
|
||||
return "schedtask.err_self_chain"
|
||||
by_id = {t["task_id"]: t for t in tasks}
|
||||
if next_task_id not in by_id:
|
||||
return "Next task does not exist."
|
||||
return "schedtask.err_next_missing"
|
||||
# Walk forward from the proposed next task; reaching task_id again means
|
||||
# the new edge would close a cycle.
|
||||
seen = {task_id}
|
||||
cur = next_task_id
|
||||
while cur:
|
||||
if cur in seen:
|
||||
return "This would create a circular task chain."
|
||||
return "schedtask.err_chain_cycle"
|
||||
seen.add(cur)
|
||||
cur = (by_id.get(cur) or {}).get("dependency", {}).get("next_task_id")
|
||||
return None
|
||||
|
||||
Reference in New Issue
Block a user